@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 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
- You can install and use NSS locally in a project with:
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
- # As a normal dependency:
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
- # or as a development dependency:
36
- npm install @caboodle-tech/node-simple-server --save-dev
33
+ ```javascript
34
+ import NodeSimpleServer from '@caboodle-tech/node-simple-server';
37
35
  ```
38
36
 
39
- Depending on how you use and incorporate NSS into your project will determine the best dependency strategy to use.
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
- npm install --global @caboodle-tech/node-simple-server
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
- // A bare minimum callback to handle most development changes.
81
- function watcherCallback(event, path, extension) {
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
- function websocketCallback(messageObject, pageId) {
108
- // Interpret and do what you need to with the message:
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
- // Respond to the page that sent the message if you like:
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
- function websocketCallback(messageObject, pageId) {
126
- // Interpret and do what you need to with the message:
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
- // Respond to the page that sent the message if you like:
132
- Server.message(pageId, 'Route specific messaged received!');
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
- // A bare minimum watcher options object; use for development, omit for production.
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, // Just send everything to a single function.
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
- // eslint-disable-next-line no-underscore-dangle
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.4';
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:${port}`,
134
- `http://127.0.0.1:${port}`
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]}:${port}`);
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
- if (dayNum.length < 10) {
226
- dayNum = `0${dayNum}`;
227
- }
228
- if (hour.length < 10) {
229
- hour = `0${hour}`;
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).mtime;
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, no-template-curly-in-string
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
- // eslint-disable-next-line consistent-return
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(this.#OPS.port);
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 (returnInstead) {
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.substr(1, pattern.length - 2);
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
- // Get the file.
673
- const file = Fs.readFileSync(systemPath, { encoding: 'binary' });
674
-
675
- // Output the file to the browser.
676
- resp.writeHead(HTTPStatus.ok, this.#getHeaders({
677
- contentType: ContentTypes[ext] || this.#OPS.contentType,
678
- file: systemPath
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
- let html = file.toString();
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
- html = start + this.#handlers.liveReloading + end;
689
- resp.write(html, 'utf-8');
714
+ body = Buffer.from(start + this.#handlers.liveReloading + end, 'utf8');
690
715
  } else {
691
- resp.write(file, 'binary');
716
+ body = Buffer.from(html, 'utf8');
692
717
  }
718
+ resp.write(body);
693
719
  } else {
694
- resp.write(file, 'binary');
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
- let cleanURL = request.url.substr(1, request.url.indexOf('/ws?id=') - 1);
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.substr(idStartIndex).replace('?id=', '');
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
- // eslint-disable-next-line no-param-reassign
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.route;
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
- // Create the HTTP server.
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
- // eslint-disable-next-line no-useless-return
958
+
931
959
  return;
932
960
  }
933
961
  });
934
- // Capture server errors and respond as needed.
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
- // Attempt to start the server now.
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].forEach((socket) => {
1003
- socket.send('close');
1004
- socket.close();
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
- // eslint-disable-next-line no-param-reassign
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 alterCatachAlls = ['all', 'raw'];
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 (alterCatachAlls.includes(key)) {
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
- // eslint-disable-next-line no-param-reassign
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
- // eslint-disable-next-line no-param-reassign
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 ({}).toString.call(unknown).match(/\s([^\]]+)/)[1].toLowerCase();
1181
+ return {}.toString.call(unknown).match(/\s([^\]]+)/)[1].toLowerCase();
1149
1182
  } catch (e) { return undefined; }
1150
1183
  }
1151
1184
 
package/bin/print.js CHANGED
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-console */
2
-
3
1
  class Print {
4
2
 
5
3
  #enabled = true;
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
+ };