@caboodle-tech/node-simple-server 4.2.4 → 4.3.0
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/README.md +49 -38
- package/bin/nss.js +101 -68
- package/bin/print.js +0 -2
- package/changelogs/v4.md +36 -0
- package/eslint/html-rules.js +24 -0
- package/eslint/js-rules.js +143 -0
- package/eslint/json-rules.js +21 -0
- package/eslint.config.js +44 -0
- package/examples/controllers/prod-website.js +2 -3
- package/examples/controllers/website.js +2 -3
- package/examples/controllers/websocket.js +4 -10
- package/examples/www-production/js/main.js +2 -3
- package/examples/www-website/character-test.html +252 -0
- package/examples/www-website/index.html +4 -1
- package/examples/www-website/js/main.js +2 -3
- package/examples/www-websockets/js/main.js +9 -7
- package/handlers/dir-listing.html +9 -9
- package/handlers/forbidden.html +5 -5
- package/handlers/live-reloading.html +1 -1
- package/handlers/not-found.html +5 -5
- package/handlers/websocket-only.html +1 -1
- package/package.json +11 -6
- package/.eslintrc.json +0 -50
package/README.md
CHANGED
|
@@ -16,36 +16,42 @@ A small but effective node based server for development sites, customizable live
|
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
19
|
-
### Manually:
|
|
20
|
-
|
|
21
|
-
Node Simple Server (NSS) can be manually incorporated into your development process/ application. Extract the `nss` folder from the [latest release](https://github.com/caboodle-tech/node-simple-server/releases/) and then `import` the server module into your code, similar to:
|
|
22
|
-
|
|
23
|
-
```javascript
|
|
24
|
-
import NodeSimpleServer from './nss.js';
|
|
25
|
-
```
|
|
26
|
-
|
|
27
19
|
### Locally:
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
Like any other Node based application you can install NSS automatically with `pnpm` or `npm` and then reference it in your code. This is the recommended way to install and use NSS.
|
|
30
22
|
|
|
31
23
|
```bash
|
|
32
|
-
#
|
|
24
|
+
# Install using pnpm:
|
|
25
|
+
pnpm add @caboodle-tech/node-simple-server
|
|
26
|
+
|
|
27
|
+
# Or install using npm:
|
|
33
28
|
npm install @caboodle-tech/node-simple-server
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then use in your code with:
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
```javascript
|
|
34
|
+
import NodeSimpleServer from '@caboodle-tech/node-simple-server';
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
### Manually:
|
|
38
|
+
|
|
39
|
+
Node Simple Server (NSS) can be manually incorporated into your development process/ application. Extract the `nss` folder from the [latest release](https://github.com/caboodle-tech/node-simple-server/releases/) and then `import` the server module into your code, similar to:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import NodeSimpleServer from './nss.js';
|
|
43
|
+
```
|
|
40
44
|
|
|
41
45
|
### Globally:
|
|
42
46
|
|
|
43
47
|
You can install and use NSS globally with:
|
|
44
48
|
|
|
45
49
|
```bash
|
|
46
|
-
|
|
50
|
+
pnpm install -g @caboodle-tech/node-simple-server
|
|
47
51
|
```
|
|
48
52
|
|
|
53
|
+
Depending on how you use and incorporate NSS into your project will determine the best dependency strategy to use.
|
|
54
|
+
|
|
49
55
|
## Usage
|
|
50
56
|
|
|
51
57
|
NSS is designed to be controlled and/or wrapped by another application. The bare minimum code needed to use NSS in your application is:
|
|
@@ -77,8 +83,12 @@ const serverOptions = {
|
|
|
77
83
|
// Get a new instance of NSS.
|
|
78
84
|
const Server = new NodeSimpleServer(serverOptions);
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
/**
|
|
87
|
+
* A bare minimum callback to handle most development changes. You register only for
|
|
88
|
+
* the events you need; unhandled events (e.g. unlink/unlinkDir if not listed) are ignored.
|
|
89
|
+
*/
|
|
90
|
+
const watcherCallback = (event, path, statsOrDetails) => {
|
|
91
|
+
const extension = statsOrDetails.ext;
|
|
82
92
|
if (extension === 'css') {
|
|
83
93
|
Server.reloadAllStyles();
|
|
84
94
|
return;
|
|
@@ -95,7 +105,11 @@ function watcherCallback(event, path, extension) {
|
|
|
95
105
|
if (event === 'change') {
|
|
96
106
|
Server.reloadSinglePage(path);
|
|
97
107
|
}
|
|
98
|
-
|
|
108
|
+
// When a file or directory is removed, reload so the browser does not show stale content.
|
|
109
|
+
if (event === 'unlink' || event === 'unlinkDir') {
|
|
110
|
+
Server.reloadAllPages();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
99
113
|
|
|
100
114
|
/**
|
|
101
115
|
* A bare minimum callback to handle all websocket messages from the frontend. By
|
|
@@ -104,15 +118,12 @@ function watcherCallback(event, path, extension) {
|
|
|
104
118
|
*
|
|
105
119
|
* NSS_WS.send([string|int|bool|object]) // Pathname lookup will be used.
|
|
106
120
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const datatype = messageObject.type
|
|
121
|
+
const websocketCallback = (messageObject, pageId) => {
|
|
122
|
+
const datatype = messageObject.type;
|
|
110
123
|
const data = messageObject.data;
|
|
111
|
-
console.log(`Received ${datatype} data from page ${pageId}: ${data}`)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Server.message(pageId, 'Messaged received!');
|
|
115
|
-
}
|
|
124
|
+
console.log(`Received ${datatype} data from page ${pageId}: ${data}`);
|
|
125
|
+
Server.message(pageId, 'Message received!');
|
|
126
|
+
};
|
|
116
127
|
Server.addWebsocketCallback('.*', websocketCallback);
|
|
117
128
|
|
|
118
129
|
/**
|
|
@@ -122,21 +133,21 @@ Server.addWebsocketCallback('.*', websocketCallback);
|
|
|
122
133
|
*
|
|
123
134
|
* NSS_WS.send([string|int|bool|object], [route string]) // Route lookup will be used.
|
|
124
135
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const datatype = messageObject.type
|
|
136
|
+
const websocketCallbackRoute = (messageObject, pageId) => {
|
|
137
|
+
const datatype = messageObject.type;
|
|
128
138
|
const data = messageObject.data;
|
|
129
|
-
console.log(`Route received ${datatype} data from page ${pageId}: ${data}`)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
Server.addWebsocketCallback('api/search', websocketCallback);
|
|
139
|
+
console.log(`Route received ${datatype} data from page ${pageId}: ${data}`);
|
|
140
|
+
Server.message(pageId, 'Route specific message received!');
|
|
141
|
+
};
|
|
142
|
+
Server.addWebsocketCallback('api/search', websocketCallbackRoute);
|
|
135
143
|
|
|
136
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Watcher options: the events you register are your intent. Only registered events
|
|
146
|
+
* are handled; others (e.g. unlink, unlinkDir) are ignored unless you add them.
|
|
147
|
+
*/
|
|
137
148
|
const watcherOptions = {
|
|
138
149
|
events: {
|
|
139
|
-
all: watcherCallback,
|
|
150
|
+
all: watcherCallback,
|
|
140
151
|
},
|
|
141
152
|
};
|
|
142
153
|
|
|
@@ -147,7 +158,7 @@ Server.start();
|
|
|
147
158
|
Server.watch(websiteRoot, watcherOptions);
|
|
148
159
|
```
|
|
149
160
|
|
|
150
|
-
The `options` object **required** by the `watch` method must include an `events` property with at least one watched event. The demo code above used `all` to capture any event. This object takes a lot of settings and is explained below in the **Watch Options** table.
|
|
161
|
+
The `options` object **required** by the `watch` method must include an `events` property with at least one watched event. The demo code above used `all` to capture any event. You express intent by which events you register: only those are handled; others are ignored. If your app has a build step (e.g. source → output), register for `unlink` and `unlinkDir` in `events` to react when files or directories are removed (e.g. to keep your output in sync). This object takes a lot of settings and is explained below in the **Watch Options** table.
|
|
151
162
|
|
|
152
163
|
NSS uses `process.cwd()` as the live servers root if omitted and is pre-configured with several additional default settings. You can change these by providing your own `options` object when instantiating the server. How this looks in code is shown below, the following table **Server Options** explains all available options.
|
|
153
164
|
|
|
@@ -203,7 +214,7 @@ const Server = new NodeSimpleServer(options);
|
|
|
203
214
|
|
|
204
215
|
#### **events**
|
|
205
216
|
|
|
206
|
-
- Set to an object that can have any combination of these properties: `all`, `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `ready`, `raw`, `error`. Any property set on `events` should point to a callback function that will handle that event.
|
|
217
|
+
- Set to an object that can have any combination of these properties: `all`, `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `ready`, `raw`, `error`. Any property set on `events` should point to a callback function that will handle that event. Only the events you register are handled; others are ignored. Use `unlink` and `unlinkDir` when you need to react to file or directory deletion (e.g. to sync your build output or trigger a reload).
|
|
207
218
|
|
|
208
219
|
#### **persistent** default: true
|
|
209
220
|
|
package/bin/nss.js
CHANGED
|
@@ -10,9 +10,8 @@ import ContentTypes from '../handlers/js/content-types.js';
|
|
|
10
10
|
import HTTPStatus from '../handlers/js/http-status.js';
|
|
11
11
|
import Print from './print.js';
|
|
12
12
|
|
|
13
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
14
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
|
|
14
|
+
|
|
16
15
|
const __dirname = Path.dirname(__filename);
|
|
17
16
|
const APP_ROOT = Path.join(__dirname, '../');
|
|
18
17
|
|
|
@@ -44,7 +43,7 @@ class NodeSimpleServer {
|
|
|
44
43
|
map: {}
|
|
45
44
|
};
|
|
46
45
|
|
|
47
|
-
#VERSION = '4.2.
|
|
46
|
+
#VERSION = '4.2.8';
|
|
48
47
|
|
|
49
48
|
#watching = [];
|
|
50
49
|
|
|
@@ -125,16 +124,25 @@ class NodeSimpleServer {
|
|
|
125
124
|
* Get an array of all the IP addresses you can reach this server at either from
|
|
126
125
|
* the machine itself or on the LAN.
|
|
127
126
|
*
|
|
127
|
+
* @param {number} [port] The port number to use in the addresses. If not provided,
|
|
128
|
+
* will use the port currently in use by the server, or the
|
|
129
|
+
* configured port if the server hasn't started yet.
|
|
128
130
|
* @return {Array} An array of loop back ip addresses and LAN addresses to this server.
|
|
129
131
|
*/
|
|
130
132
|
getAddresses(port) {
|
|
133
|
+
/*
|
|
134
|
+
* Use provided port, or fall back to portInUse (actual port server is using),
|
|
135
|
+
* or finally to the configured port. Check for undefined/null specifically to
|
|
136
|
+
* allow 0 if explicitly provided (though unlikely).
|
|
137
|
+
*/
|
|
138
|
+
const actualPort = port !== undefined && port !== null ? port : this.#OPS.portInUse || this.#OPS.port;
|
|
131
139
|
const locals = this.#getLocalAddresses();
|
|
132
140
|
const addresses = [
|
|
133
|
-
`http://localhost:${
|
|
134
|
-
`http://127.0.0.1:${
|
|
141
|
+
`http://localhost:${actualPort}`,
|
|
142
|
+
`http://127.0.0.1:${actualPort}`
|
|
135
143
|
];
|
|
136
144
|
Object.keys(locals).forEach((key) => {
|
|
137
|
-
addresses.push(`http://${locals[key]}:${
|
|
145
|
+
addresses.push(`http://${locals[key]}:${actualPort}`);
|
|
138
146
|
});
|
|
139
147
|
return addresses;
|
|
140
148
|
}
|
|
@@ -156,8 +164,8 @@ class NodeSimpleServer {
|
|
|
156
164
|
files.push(item);
|
|
157
165
|
}
|
|
158
166
|
});
|
|
159
|
-
files.sort((a, b) => a.localeCompare(b));
|
|
160
|
-
dirs.sort((a, b) => a.localeCompare(b));
|
|
167
|
+
files.sort((a, b) => { return a.localeCompare(b); });
|
|
168
|
+
dirs.sort((a, b) => { return a.localeCompare(b); });
|
|
161
169
|
// Build the innerHTML for the directory and files unordered list.
|
|
162
170
|
let fileHtml = '';
|
|
163
171
|
let dirHtml = '';
|
|
@@ -222,18 +230,11 @@ class NodeSimpleServer {
|
|
|
222
230
|
let hour = timestamp.getUTCHours();
|
|
223
231
|
let minute = timestamp.getUTCMinutes();
|
|
224
232
|
let second = timestamp.getUTCSeconds();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (minute.length < 10) {
|
|
232
|
-
minute = `0${minute}`;
|
|
233
|
-
}
|
|
234
|
-
if (second.length < 10) {
|
|
235
|
-
second = `0${second}`;
|
|
236
|
-
}
|
|
233
|
+
// Convert to string and pad with leading zero if needed
|
|
234
|
+
dayNum = String(dayNum).padStart(2, '0');
|
|
235
|
+
hour = String(hour).padStart(2, '0');
|
|
236
|
+
minute = String(minute).padStart(2, '0');
|
|
237
|
+
second = String(second).padStart(2, '0');
|
|
237
238
|
return `${dayStr}, ${dayNum} ${monStr} ${timestamp.getUTCFullYear()} ${hour}:${minute}:${second} GMT`;
|
|
238
239
|
}
|
|
239
240
|
|
|
@@ -288,7 +289,7 @@ class NodeSimpleServer {
|
|
|
288
289
|
// Standard headers that should always be set for NSS.
|
|
289
290
|
let mtime = new Date().toUTCString();
|
|
290
291
|
if (settings?.file) {
|
|
291
|
-
mtime = Fs.statSync(settings.file)
|
|
292
|
+
({ mtime } = Fs.statSync(settings.file));
|
|
292
293
|
}
|
|
293
294
|
const nssHeaders = {
|
|
294
295
|
'Cache-Control': 'public, max-age=0',
|
|
@@ -324,7 +325,7 @@ class NodeSimpleServer {
|
|
|
324
325
|
* like directory listing, page not found, access denied, and so on.
|
|
325
326
|
*/
|
|
326
327
|
#loadHandlers() {
|
|
327
|
-
// eslint-disable-next-line max-len
|
|
328
|
+
// eslint-disable-next-line max-len
|
|
328
329
|
const internalError = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>500 Internal Server Error</title><style>body,html{margin:0;padding:0}body{padding:15px}</style></head><body><h1>500 Internal Server Error</h1><p>Could not locate source file.</p><hr><p><i>Node Simple Server (NSS) {{version}} Server <script type="text/javascript">document.write(`${document.location.protocol}//${document.location.hostname}`);</script> Port <script type="text/javascript">document.write(document.location.port)</script></i></p>{{liveReloading}}</body></html>';
|
|
329
330
|
|
|
330
331
|
const dirListingSrc = Path.join(APP_ROOT, 'handlers', 'dir-listing.html');
|
|
@@ -377,18 +378,32 @@ class NodeSimpleServer {
|
|
|
377
378
|
* Print the addresses the server is listening on to the console; this is useful for users who
|
|
378
379
|
* are not sure what address to use to access the server.
|
|
379
380
|
*
|
|
381
|
+
* @param {number|boolean} [portOrReturn] If a number, the port to use in the addresses.
|
|
382
|
+
* If a boolean, whether to return the message instead of printing.
|
|
380
383
|
* @param {boolean} [returnInstead=false] If true the function will return the message string instead.
|
|
381
384
|
*/
|
|
382
|
-
|
|
383
|
-
printListeningAddresses(returnInstead = false) {
|
|
385
|
+
|
|
386
|
+
printListeningAddresses(portOrReturn, returnInstead = false) {
|
|
387
|
+
// Handle backward compatibility: if first arg is boolean, it's returnInstead
|
|
388
|
+
let port;
|
|
389
|
+
let shouldReturn = returnInstead;
|
|
390
|
+
if (typeof portOrReturn === 'boolean') {
|
|
391
|
+
shouldReturn = portOrReturn;
|
|
392
|
+
port = undefined;
|
|
393
|
+
} else if (typeof portOrReturn === 'number') {
|
|
394
|
+
port = portOrReturn;
|
|
395
|
+
} else {
|
|
396
|
+
port = undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
384
399
|
let message = 'Node Simple Server live @:\n';
|
|
385
|
-
const addresses = this.getAddresses(
|
|
400
|
+
const addresses = this.getAddresses(port);
|
|
386
401
|
addresses.forEach((address) => {
|
|
387
402
|
message += ` ${address}\n`;
|
|
388
403
|
});
|
|
389
404
|
message += '\n';
|
|
390
405
|
|
|
391
|
-
if (
|
|
406
|
+
if (shouldReturn) {
|
|
392
407
|
return message;
|
|
393
408
|
}
|
|
394
409
|
Print.notice(message);
|
|
@@ -408,7 +423,7 @@ class NodeSimpleServer {
|
|
|
408
423
|
}
|
|
409
424
|
if (pattern[0] === '/' && pattern[pattern.length - 1] === '/') {
|
|
410
425
|
// eslint-disable-next-line no-param-reassign
|
|
411
|
-
pattern = pattern.
|
|
426
|
+
pattern = pattern.slice(1, -1);
|
|
412
427
|
}
|
|
413
428
|
return new RegExp(`^${pattern}$`);
|
|
414
429
|
} catch (e) {
|
|
@@ -482,8 +497,8 @@ class NodeSimpleServer {
|
|
|
482
497
|
pattern = this.makeRegex(pattern);
|
|
483
498
|
}
|
|
484
499
|
// See if the pattern is a page id first.
|
|
485
|
-
if (this.#sockets.map[original]) {
|
|
486
|
-
this.#sockets.map[original].send('reload');
|
|
500
|
+
if (this.#sockets.map[`S${original}`]) {
|
|
501
|
+
this.#sockets.map[`S${original}`].send('reload');
|
|
487
502
|
return;
|
|
488
503
|
}
|
|
489
504
|
// See if the pattern matches a specific URL and reload all those pages.
|
|
@@ -528,8 +543,8 @@ class NodeSimpleServer {
|
|
|
528
543
|
pattern = this.makeRegex(pattern) || original;
|
|
529
544
|
}
|
|
530
545
|
// See if the pattern is a page id first.
|
|
531
|
-
if (this.#sockets.map[original]) {
|
|
532
|
-
this.#sockets.map[original].send('refreshCSS');
|
|
546
|
+
if (this.#sockets.map[`S${original}`]) {
|
|
547
|
+
this.#sockets.map[`S${original}`].send('refreshCSS');
|
|
533
548
|
return;
|
|
534
549
|
}
|
|
535
550
|
// See if the pattern matches a specific URL and reload all those pages.
|
|
@@ -669,29 +684,40 @@ class NodeSimpleServer {
|
|
|
669
684
|
return;
|
|
670
685
|
}
|
|
671
686
|
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
contentType
|
|
678
|
-
|
|
679
|
-
|
|
687
|
+
const contentType = ContentTypes[ext] || this.#OPS.contentType;
|
|
688
|
+
const isText = contentType.startsWith('text/') ||
|
|
689
|
+
contentType === 'application/json' ||
|
|
690
|
+
contentType === 'application/ld+json' ||
|
|
691
|
+
contentType === 'application/manifest+json' ||
|
|
692
|
+
contentType === 'application/xhtml+xml';
|
|
693
|
+
|
|
694
|
+
// Read with no encoding: returns Buffer of raw bytes (Node.js fs docs). Sending that
|
|
695
|
+
// Buffer preserves the file bytes; do not use 'binary' (alias for latin1) which
|
|
696
|
+
// misinterprets UTF-8 multi-byte sequences.
|
|
697
|
+
const file = Fs.readFileSync(systemPath);
|
|
698
|
+
|
|
699
|
+
// Output the file to the browser. Only set charset for text types.
|
|
700
|
+
const headerSettings = { contentType, file: systemPath };
|
|
701
|
+
if (isText) {
|
|
702
|
+
headerSettings.charset = 'utf-8';
|
|
703
|
+
}
|
|
704
|
+
resp.writeHead(HTTPStatus.ok, this.#getHeaders(headerSettings));
|
|
680
705
|
|
|
681
|
-
// If needed inject NSS's WebSocket at the end of the page.
|
|
706
|
+
// If needed inject NSS's WebSocket at the end of the page, decode to string, modify, re-encode.
|
|
682
707
|
if (this.#reload.includes(ext)) {
|
|
683
|
-
|
|
708
|
+
const html = file.toString('utf8');
|
|
684
709
|
const last = html.lastIndexOf('</body>');
|
|
710
|
+
let body;
|
|
685
711
|
if (last && last > 0) {
|
|
686
712
|
const start = html.substring(0, last);
|
|
687
713
|
const end = html.substring(last);
|
|
688
|
-
|
|
689
|
-
resp.write(html, 'utf-8');
|
|
714
|
+
body = Buffer.from(start + this.#handlers.liveReloading + end, 'utf8');
|
|
690
715
|
} else {
|
|
691
|
-
|
|
716
|
+
body = Buffer.from(html, 'utf8');
|
|
692
717
|
}
|
|
718
|
+
resp.write(body);
|
|
693
719
|
} else {
|
|
694
|
-
resp.write(file
|
|
720
|
+
resp.write(file);
|
|
695
721
|
}
|
|
696
722
|
resp.end();
|
|
697
723
|
}
|
|
@@ -753,7 +779,8 @@ class NodeSimpleServer {
|
|
|
753
779
|
*/
|
|
754
780
|
#socketListener(socket, request) {
|
|
755
781
|
// Strip the page ID and /ws tag off the url to get the actual url.
|
|
756
|
-
|
|
782
|
+
const wsIndex = request.url.indexOf('/ws?id=');
|
|
783
|
+
let cleanURL = wsIndex > 0 ? request.url.slice(1, wsIndex) : '';
|
|
757
784
|
if (!cleanURL) {
|
|
758
785
|
cleanURL = this.#OPS.indexPage;
|
|
759
786
|
}
|
|
@@ -779,17 +806,16 @@ class NodeSimpleServer {
|
|
|
779
806
|
|
|
780
807
|
let pageId;
|
|
781
808
|
if (idStartIndex !== -1) {
|
|
782
|
-
pageId = request.url.
|
|
809
|
+
pageId = request.url.slice(idStartIndex).replace('?id=', '');
|
|
783
810
|
} else {
|
|
784
811
|
pageId = 'unknown';
|
|
785
812
|
}
|
|
786
813
|
|
|
787
|
-
// eslint-disable-next-line no-param-reassign
|
|
788
814
|
socket.nssUid = pageId;
|
|
789
815
|
|
|
790
816
|
// Overwrite the default send method to NSS's standard.
|
|
791
817
|
const originalSend = socket.send.bind(socket);
|
|
792
|
-
|
|
818
|
+
|
|
793
819
|
socket.send = (message) => {
|
|
794
820
|
originalSend(JSON.stringify({
|
|
795
821
|
message,
|
|
@@ -835,7 +861,7 @@ class NodeSimpleServer {
|
|
|
835
861
|
// Pull out specific route if there is one.
|
|
836
862
|
let route = null;
|
|
837
863
|
if ('route' in msgObj) {
|
|
838
|
-
route = msgObj
|
|
864
|
+
({ route } = msgObj);
|
|
839
865
|
}
|
|
840
866
|
|
|
841
867
|
// See if the message belongs to a callback and send it there.
|
|
@@ -866,7 +892,7 @@ class NodeSimpleServer {
|
|
|
866
892
|
for (let i = 0; i < connections.length; i++) {
|
|
867
893
|
if (connections[i].nssUid === pageId) {
|
|
868
894
|
connections.splice(i, 1);
|
|
869
|
-
delete this.#sockets.map[pageId];
|
|
895
|
+
delete this.#sockets.map[`S${pageId}`];
|
|
870
896
|
break;
|
|
871
897
|
}
|
|
872
898
|
}
|
|
@@ -910,9 +936,11 @@ class NodeSimpleServer {
|
|
|
910
936
|
return;
|
|
911
937
|
}
|
|
912
938
|
|
|
913
|
-
|
|
939
|
+
/*
|
|
940
|
+
* Create the HTTP server. Capture connection upgrade requests so we don't
|
|
941
|
+
* break WebSocket connections.
|
|
942
|
+
*/
|
|
914
943
|
this.#server = Http.createServer(this.#serverListener.bind(this));
|
|
915
|
-
// Capture connection upgrade requests so we don't break WebSocket connections.
|
|
916
944
|
// eslint-disable-next-line no-unused-vars
|
|
917
945
|
this.#server.on('upgrade', (request, socket) => {
|
|
918
946
|
/*
|
|
@@ -927,13 +955,15 @@ class NodeSimpleServer {
|
|
|
927
955
|
* requests so they get passed on to the this.#socket.connection listener.
|
|
928
956
|
*/
|
|
929
957
|
if (request.headers.upgrade === 'websocket') {
|
|
930
|
-
|
|
958
|
+
|
|
931
959
|
return;
|
|
932
960
|
}
|
|
933
961
|
});
|
|
934
|
-
|
|
962
|
+
/*
|
|
963
|
+
* Capture server errors and respond as needed. The port we tried to use is
|
|
964
|
+
* taken, increment and try to start again.
|
|
965
|
+
*/
|
|
935
966
|
this.#server.on('error', (error) => {
|
|
936
|
-
// The port we tried to use is taken, increment and try to start again.
|
|
937
967
|
if (error.code === 'EADDRINUSE') {
|
|
938
968
|
if (port) {
|
|
939
969
|
// Stop trying new ports after 100 attempts.
|
|
@@ -955,7 +985,7 @@ class NodeSimpleServer {
|
|
|
955
985
|
Print.error(`Server Error:\n${error}`);
|
|
956
986
|
});
|
|
957
987
|
|
|
958
|
-
|
|
988
|
+
/* Attempt to start the server now. */
|
|
959
989
|
// eslint-disable-next-line no-param-reassign
|
|
960
990
|
port = port || this.#OPS.port;
|
|
961
991
|
this.#server.listen(port, () => {
|
|
@@ -997,12 +1027,14 @@ class NodeSimpleServer {
|
|
|
997
1027
|
// If any back-end files are being watched for changes stop monitoring them.
|
|
998
1028
|
this.watchEnd();
|
|
999
1029
|
// Close all socket connections; these would force the server to stay up.
|
|
1000
|
-
const keys = Object.keys(this.#sockets);
|
|
1030
|
+
const keys = Object.keys(this.#sockets.routes);
|
|
1001
1031
|
keys.forEach((key) => {
|
|
1002
|
-
this.#sockets.routes[key]
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1032
|
+
if (this.#sockets.routes[key]) {
|
|
1033
|
+
this.#sockets.routes[key].forEach((socket) => {
|
|
1034
|
+
socket.send('close');
|
|
1035
|
+
socket.close();
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1006
1038
|
});
|
|
1007
1039
|
// Now gracefully close SERVER and SOCKET.
|
|
1008
1040
|
this.#server.close();
|
|
@@ -1064,7 +1096,7 @@ class NodeSimpleServer {
|
|
|
1064
1096
|
* directory to NSS's root if the setting is missing.
|
|
1065
1097
|
*/
|
|
1066
1098
|
if (!options.cwd) {
|
|
1067
|
-
|
|
1099
|
+
|
|
1068
1100
|
options.cwd = this.#OPS.root;
|
|
1069
1101
|
}
|
|
1070
1102
|
// Convert paths to array if it's not already.
|
|
@@ -1076,9 +1108,10 @@ class NodeSimpleServer {
|
|
|
1076
1108
|
// Start watching the path(s).
|
|
1077
1109
|
const watcher = Chokidar.watch(paths, options);
|
|
1078
1110
|
this.#watching.push(watcher);
|
|
1111
|
+
|
|
1079
1112
|
// Prepare to modify some of the standard Chokidar listeners.
|
|
1080
1113
|
const alterAddUpdates = ['add', 'addDir', 'change'];
|
|
1081
|
-
const
|
|
1114
|
+
const alterCatchAlls = ['all', 'raw'];
|
|
1082
1115
|
const alterUnlinks = ['unlink', 'unlinkDir'];
|
|
1083
1116
|
// Hookup requested listeners; they are case sensitive so type them right in your code!
|
|
1084
1117
|
Object.keys(options.events).forEach((key) => {
|
|
@@ -1088,12 +1121,12 @@ class NodeSimpleServer {
|
|
|
1088
1121
|
* Chokidar provides paths in the correct OS format but NSS will change
|
|
1089
1122
|
* all backslashes (\) into forward slashes (/).
|
|
1090
1123
|
*/
|
|
1091
|
-
if (
|
|
1124
|
+
if (alterCatchAlls.includes(key)) {
|
|
1092
1125
|
watcher.on(key, (evt, path, statsOrDetails = {}) => {
|
|
1093
1126
|
// Capture the call and alter the path before passing it on.
|
|
1094
1127
|
const altPath = path.replace(/\\/g, '/');
|
|
1095
1128
|
// Since we're messing with the path already grab the extension for the user.
|
|
1096
|
-
|
|
1129
|
+
|
|
1097
1130
|
statsOrDetails.ext = Path.extname(altPath).replace('.', '');
|
|
1098
1131
|
options.events[key](evt, altPath, statsOrDetails);
|
|
1099
1132
|
});
|
|
@@ -1102,7 +1135,7 @@ class NodeSimpleServer {
|
|
|
1102
1135
|
// Capture the call and alter the path before passing it on.
|
|
1103
1136
|
const altPath = path.replace(/\\/g, '/');
|
|
1104
1137
|
// Since we're messing with the path already grab the extension for the user.
|
|
1105
|
-
|
|
1138
|
+
|
|
1106
1139
|
statsOrDetails.ext = Path.extname(altPath).replace('.', '');
|
|
1107
1140
|
options.events[key](altPath, statsOrDetails);
|
|
1108
1141
|
});
|
|
@@ -1145,7 +1178,7 @@ class NodeSimpleServer {
|
|
|
1145
1178
|
*/
|
|
1146
1179
|
whatIs(unknown) {
|
|
1147
1180
|
try {
|
|
1148
|
-
return
|
|
1181
|
+
return {}.toString.call(unknown).match(/\s([^\]]+)/)[1].toLowerCase();
|
|
1149
1182
|
} catch (e) { return undefined; }
|
|
1150
1183
|
}
|
|
1151
1184
|
|
package/bin/print.js
CHANGED
package/changelogs/v4.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
### NSS 4.2.8 (unreleased)
|
|
2
|
+
|
|
3
|
+
- fix: Socket map key on close and in reloadSinglePage/reloadSingleStyles use `S${pageId}` consistently.
|
|
4
|
+
- fix: Serve file bodies as raw bytes (read with no encoding); only decode/re-encode when injecting live-reload script. Fixes UTF-8 mojibake (e.g. entities, em dashes, Arabic, CJK) that occurred when using `encoding: 'binary'` (latin1).
|
|
5
|
+
- fix: Example controllers no longer call printListeningAddresses() after start(); start() already prints once.
|
|
6
|
+
- refactor: README and example code use const arrow functions instead of function declarations; watcher examples clarify intent via events (e.g. unlink/unlinkDir).
|
|
7
|
+
- docs: Example www-website includes character-test.html demo (entities, emojis, RTL/LTR, UTF-8).
|
|
8
|
+
|
|
9
|
+
### NSS 4.2.7 (5 January 2026)
|
|
10
|
+
|
|
11
|
+
- build: Migrate ESLint configuration to flat config format
|
|
12
|
+
- build: Add custom ESLint rules for HTML, JavaScript, and JSON
|
|
13
|
+
- deps: Update chokidar from ^3.5.3 to ^5.0.0
|
|
14
|
+
- deps: Update ws from ^8.13.0 to ^8.19.0
|
|
15
|
+
- deps: Replace eslint-config-airbnb-base with @html-eslint/eslint-plugin and eslint-plugin-jsonc
|
|
16
|
+
- deps: Update ESLint from ^8.2.0 to ^9.39.2
|
|
17
|
+
- deps: Add engines field specifying Node.js >=14.0.0 requirement
|
|
18
|
+
- feat: Enhance getAddresses() to handle optional port parameter with fallback logic
|
|
19
|
+
- feat: Enhance printListeningAddresses() to accept port parameter while maintaining backward compatibility
|
|
20
|
+
- fix: Replace deprecated substr() with slice() method
|
|
21
|
+
- fix: Use object destructuring for better code quality (prefer-destructuring rule)
|
|
22
|
+
- fix: Update arrow functions to use block statements (arrow-body-style rule)
|
|
23
|
+
- refactor: Replace manual string padding with padStart() method
|
|
24
|
+
- refactor: Remove unused eslint-disable directives
|
|
25
|
+
- style: Fix HTML meta tags formatting (remove self-closing, sort attributes)
|
|
26
|
+
- style: Remove unnecessary type attributes from script tags
|
|
27
|
+
- style: Fix SVG attribute ordering in HTML handlers
|
|
28
|
+
- docs: Update README.md with pnpm installation instructions
|
|
29
|
+
- docs: Improve installation documentation structure
|
|
30
|
+
- docs: Update watcher callback example to reflect Chokidar v4+ API changes
|
|
31
|
+
|
|
32
|
+
### NSS 4.2.5 (23 July 2024)
|
|
33
|
+
|
|
34
|
+
- chore: Document changes since 4.2.2 here in the changelog:
|
|
35
|
+
- Allow users to control address printing instead or printing it automatically.
|
|
36
|
+
|
|
1
37
|
### NSS 4.2.2 (24 January 2024)
|
|
2
38
|
|
|
3
39
|
- feat: Add WebSocket to connection with backend routes.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caboodle Tech's opinionated rules for linting HTML with ESLint. Feel free to adapt or modify
|
|
3
|
+
* these rules to suit your needs.
|
|
4
|
+
*
|
|
5
|
+
* ESLint Plugin: {@link https://www.npmjs.com/package/@html-eslint/eslint-plugin @html-eslint/eslint-plugin}
|
|
6
|
+
* HTML ESLint Parser: {@link https://html-eslint.org/docs/rules HTML ESLint Docs}
|
|
7
|
+
*/
|
|
8
|
+
export default {
|
|
9
|
+
'@html-eslint/indent': ['error', 4],
|
|
10
|
+
'@html-eslint/no-duplicate-attrs': 'error',
|
|
11
|
+
'@html-eslint/no-duplicate-id': 'error',
|
|
12
|
+
'@html-eslint/no-extra-spacing-attrs': 'error',
|
|
13
|
+
'@html-eslint/no-inline-styles': 'warn',
|
|
14
|
+
'@html-eslint/no-multiple-empty-lines': ['error', { max: 1 }],
|
|
15
|
+
'@html-eslint/no-obsolete-tags': 'error',
|
|
16
|
+
'@html-eslint/no-script-style-type': 'error',
|
|
17
|
+
'@html-eslint/no-trailing-spaces': 'error',
|
|
18
|
+
'@html-eslint/require-button-type': 'warn',
|
|
19
|
+
'@html-eslint/require-closing-tags': 'error',
|
|
20
|
+
'@html-eslint/require-doctype': 'error',
|
|
21
|
+
'@html-eslint/require-li-container': 'warn',
|
|
22
|
+
'@html-eslint/require-meta-viewport': 'error',
|
|
23
|
+
'@html-eslint/sort-attrs': 'warn'
|
|
24
|
+
};
|