@caboodle-tech/node-simple-server 1.3.0 → 2.0.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.
Files changed (40) hide show
  1. package/.eslintrc.json +35 -23
  2. package/COPYING.txt +7 -0
  3. package/LICENSE.txt +22 -0
  4. package/README.md +73 -40
  5. package/bin/nss.js +1039 -0
  6. package/changelogs/v1.md +11 -0
  7. package/changelogs/v2.md +28 -0
  8. package/examples/README.md +15 -0
  9. package/examples/controllers/website.js +60 -0
  10. package/examples/controllers/websocket.js +83 -0
  11. package/examples/run.js +17 -0
  12. package/examples/www-website/assets/fonts/roboto/LICENSE.txt +202 -0
  13. package/examples/www-website/assets/fonts/roboto/roboto-regular.ttf +0 -0
  14. package/examples/www-website/assets/fonts/roboto/roboto-regular.woff +0 -0
  15. package/examples/www-website/assets/fonts/roboto/roboto-regular.woff2 +0 -0
  16. package/examples/www-website/assets/imgs/logo.png +0 -0
  17. package/examples/www-website/css/main.css +96 -0
  18. package/examples/www-website/css/normalize.css +349 -0
  19. package/examples/www-website/index.html +47 -0
  20. package/examples/www-website/js/main.js +33 -0
  21. package/examples/www-websockets/assets/fonts/roboto/LICENSE.txt +202 -0
  22. package/examples/www-websockets/assets/fonts/roboto/roboto-regular.ttf +0 -0
  23. package/examples/www-websockets/assets/fonts/roboto/roboto-regular.woff +0 -0
  24. package/examples/www-websockets/assets/fonts/roboto/roboto-regular.woff2 +0 -0
  25. package/examples/www-websockets/assets/imgs/logo.png +0 -0
  26. package/examples/www-websockets/css/main.css +148 -0
  27. package/examples/www-websockets/css/normalize.css +349 -0
  28. package/examples/www-websockets/index.html +45 -0
  29. package/examples/www-websockets/js/main.js +63 -0
  30. package/{sources → handlers}/dir-listing.html +68 -5
  31. package/handlers/forbidden.html +43 -0
  32. package/{js → handlers/js}/content-types.js +9 -1
  33. package/{js → handlers/js}/http-status.js +2 -1
  34. package/handlers/live-reloading.html +209 -0
  35. package/handlers/not-found.html +43 -0
  36. package/package.json +13 -10
  37. package/LICENSE +0 -21
  38. package/server.js +0 -866
  39. package/sources/socket.html +0 -204
  40. /package/{sources → handlers}/favicon.ico +0 -0
package/server.js DELETED
@@ -1,866 +0,0 @@
1
- /* eslint-disable no-console */
2
- /* eslint-disable func-names */
3
- /* eslint-disable no-shadow */
4
-
5
- const Chokidar = require('chokidar');
6
- const FS = require('fs');
7
- const Http = require('http');
8
- const OS = require('os');
9
- const Path = require('path');
10
- const WebSocket = require('ws');
11
- const HTTPStatus = require('./js/http-status');
12
- const ContentTypes = require('./js/content-types');
13
-
14
- /**
15
- * The Node Simple Server (NSS) application.
16
- *
17
- * @param {Object} options The settings to use, uses defaults set in OP if missing.
18
- * @return {Object} A new NSS object with public methods exposed.
19
- */
20
- function NodeSimpleServer(options) {
21
-
22
- /* NSS global variables. */
23
- const CONNECTIONS = {};
24
- const OP = {
25
- callbacks: [],
26
- contentType: 'text/html',
27
- dirListing: false,
28
- indexPage: 'index.html',
29
- port: 5000,
30
- root: Path.normalize(`${process.cwd()}/`),
31
- running: false
32
- };
33
- const RELOAD = ['.html', '.htm'];
34
- const SEP = Path.sep;
35
- let SERVER = null;
36
- let SOCKET = null;
37
- const VERSION = '1.3.0'; // Update on releases.
38
- const WATCHING = [];
39
-
40
- /**
41
- * Get an array of all the IP addresses you can reach this server at either from
42
- * the machine itself or on the local area network (LAN).
43
- *
44
- * @return {Array} An array of loop back ip addresses and LAN addresses to this server.
45
- */
46
- const getAddresses = function (port) {
47
- const locals = getLocalAddresses();
48
- const addresses = [
49
- `http://localhost:${port}`,
50
- `http://127.0.0.1:${port}`
51
- ];
52
- Object.keys(locals).forEach((key) => {
53
- addresses.push(`http://${locals[key]}:${port}`);
54
- });
55
- return addresses;
56
- };
57
-
58
- /**
59
- * Get the contents of a directory for displaying in the directory listing page.
60
- *
61
- * @param {String} location The directory to search.
62
- * @return {Array} The html for files [0] and directories [1] found.
63
- */
64
- const getDirList = function (location) {
65
- // Get all directories and files at this location.
66
- const files = [];
67
- const dirs = [];
68
- FS.readdirSync(location).forEach((item) => {
69
- if (FS.lstatSync(Path.join(location, item)).isDirectory()) {
70
- dirs.push(item);
71
- } else {
72
- files.push(item);
73
- }
74
- });
75
- files.sort((a, b) => a.localeCompare(b));
76
- dirs.sort((a, b) => a.localeCompare(b));
77
- // Build the innerHTML for the directory and files unordered list.
78
- let fileHtml = '';
79
- let dirHtml = '';
80
- // Replace files in the directory listing template.
81
- files.forEach((file) => {
82
- const relative = Path.join(location, file).replace(OP.root, '');
83
- fileHtml += `<li><a href="${relative}">${file}</a></li>`;
84
- });
85
- // Add the go back link (parent directory) for nested directories.
86
- if (location.replace(OP.root, '').length > 0) {
87
- dirHtml += '<li><a href="../">../</a></li>';
88
- }
89
- // Replace directories in the directory listing template.
90
- dirs.forEach((dir) => {
91
- const relative = Path.join(location, dir).replace(OP.root, '');
92
- dirHtml += `<li><a href="${relative}">${dir}</a></li>`;
93
- });
94
- return [fileHtml, dirHtml];
95
- };
96
-
97
- /**
98
- * Determine a files last modified time and report it in the format expected
99
- * by the HTTP Last-Modified header. {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified| See standard}.
100
- *
101
- * @param {String} [date] A time string representing the last modification of this file.
102
- * @return {String} <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
103
- */
104
- const getLastModified = function (date) {
105
- // Use the current time if no time was passed in.
106
- let timestamp = new Date();
107
- if (date) {
108
- timestamp = new Date(Date.parse(date));
109
- }
110
- // Build and return the timestamp.
111
- const dayAry = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
112
- const monthAry = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
113
- const dayStr = dayAry[timestamp.getUTCDay()];
114
- const monStr = monthAry[timestamp.getUTCMonth()];
115
- let dayNum = timestamp.getUTCDate();
116
- let hour = timestamp.getUTCHours();
117
- let minute = timestamp.getUTCMinutes();
118
- let second = timestamp.getUTCSeconds();
119
- if (dayNum.length < 10) {
120
- dayNum = `0${dayNum}`;
121
- }
122
- if (hour.length < 10) {
123
- hour = `0${hour}`;
124
- }
125
- if (minute.length < 10) {
126
- minute = `0${minute}`;
127
- }
128
- if (second.length < 10) {
129
- second = `0${second}`;
130
- }
131
- return `${dayStr}, ${dayNum} ${monStr} ${timestamp.getUTCFullYear()} ${hour}:${minute}:${second} GMT`;
132
- };
133
-
134
- /**
135
- * Get all IP addresses that are considered external on this machine; the server will
136
- * attempt to listen to the requested port on these addresses as well, allowing you to
137
- * view the site from other devices on the same network. {@link https://github.com/nisaacson/interface-addresses| Source of code}.
138
- *
139
- * @author Noah Isaacson
140
- * @return {Object} An object of interface names [key] and their IPv4 IP addresses [value].
141
- */
142
- const getLocalAddresses = function () {
143
- const addresses = {};
144
- const interfaces = OS.networkInterfaces();
145
- Object.keys(interfaces).filter((key) => {
146
- const items = interfaces[key];
147
- return items.forEach((item) => {
148
- const family = item.family;
149
- if (family !== 'IPv4') {
150
- return false;
151
- }
152
- const internal = item.internal;
153
- if (internal) {
154
- return false;
155
- }
156
- const address = item.address;
157
- addresses[key] = address;
158
- return true;
159
- });
160
- });
161
- return addresses;
162
- };
163
-
164
- /**
165
- * Create the HTTP headers object for the specified file if any.
166
- *
167
- * @param {String} contentType The content type to use otherwise use OP.contentType.
168
- * @param {String} file The file to build headers for; defaults will be used if missing.
169
- * @return {Object} The HTTP header object.
170
- */
171
- const getHeaders = function (contentType, file) {
172
- // Create the header object and set the Content-Type.
173
- const headers = {};
174
- contentType = contentType || OP.contentType;
175
- headers['Content-Type'] = `${contentType}; charset=UTF-8`;
176
- // Standard headers that should always be set for NSS.
177
- let mtime = '';
178
- if (file) {
179
- mtime = FS.statSync(file).mtime;
180
- }
181
- const nssHeaders = {
182
- 'Cache-Control': 'public, max-age=0',
183
- 'Last-Modified': getLastModified(mtime),
184
- // eslint-disable-next-line quote-props
185
- 'Vary': 'Origin',
186
- 'X-Powered-By': `NodeSimpleServer ${VERSION}`
187
- };
188
- // Combine the headers and return the header object.
189
- return Object.assign(headers, nssHeaders);
190
- };
191
-
192
- /**
193
- * Returns an array of watcher objects showing you which directories and files are
194
- * actively being watched for changes.
195
- *
196
- * @return {Array} An array of watcher objects; 1 object per call to watch().
197
- */
198
- const getWatched = function () {
199
- const watched = [];
200
- WATCHING.forEach((watcher) => {
201
- watched.push(watcher.getWatched());
202
- });
203
- return watched;
204
- };
205
-
206
- /**
207
- * Initialize the application and change default settings as requested.
208
- *
209
- * @param {Object} options The settings you would like to use instead of NSS's defaults.
210
- */
211
- const initialize = function (options) {
212
- options = options || {};
213
- if (options.disableAutoRestart) {
214
- OP.disableAutoRestart = true;
215
- }
216
- if (options.contentType) {
217
- OP.contentType = options.contentType;
218
- }
219
- if (options.dirListing) {
220
- OP.dirListing = options.dirListing;
221
- }
222
- if (options.port) {
223
- OP.port = options.port;
224
- }
225
- if (options.root) {
226
- OP.root = options.root;
227
- if (OP.root[OP.root.length - 1] !== SEP) {
228
- OP.root += SEP;
229
- }
230
- }
231
- };
232
-
233
- /**
234
- * Converts a regular expression (regex) string into an actual RegExp object.
235
- *
236
- * @param {String} pattern A string of text or a regex expressed as a string; don't forget to
237
- * escape characters that should be interpreted literally.
238
- * @return {RegExp|null} A RegExp object if the string could be converted, null otherwise.
239
- */
240
- const makeRegex = function (pattern) {
241
- try {
242
- if (/\[|\]|\(|\)|\{|\}|\*|\$|\^/.test(pattern)) {
243
- return new RegExp(pattern);
244
- }
245
- if (pattern[0] === '/' && pattern[pattern.length - 1] === '/') {
246
- pattern = pattern.substr(1, pattern.length - 2);
247
- }
248
- return new RegExp(`^${pattern}$`);
249
- } catch (e) {
250
- return null;
251
- }
252
- };
253
-
254
- /**
255
- * Message a font-end page via the WebSocket connection if the page is currently connected.
256
- *
257
- * @param {String} pattern A RegExp object to check the page URL's against, a string
258
- * representing a regular expression to check the page URL's against,
259
- * or a font-end pages ID.
260
- * @param {String|*} msg The message you would like to send, usually a stringified JSON object.
261
- * @return {Boolean} True if the page is connected and the message was sent, false otherwise.
262
- */
263
- const message = function (pattern, msg) {
264
- const original = pattern.toString();
265
- const regex = makeRegex(pattern);
266
- let result = false;
267
- // Attempt to find the requested page and message it.
268
- const keys = Object.keys(CONNECTIONS);
269
- for (let i = 0; i < keys.length; i++) {
270
- if (regex != null) {
271
- if (regex.test(keys[i])) {
272
- CONNECTIONS[keys[i]].forEach((socket) => {
273
- socket.send(msg);
274
- });
275
- result = true;
276
- }
277
- } else if (original === keys[i]) {
278
- CONNECTIONS[keys[i]].forEach((socket) => {
279
- socket.send(msg);
280
- });
281
- result = true;
282
- }
283
- }
284
- return result;
285
- };
286
-
287
- /**
288
- * Register a back-end function to call when a front-end page messages in. This will
289
- * allow you to have two way communications with a page as long as you registered
290
- * a function on the front-end as well.
291
- *
292
- * @param {String} pattern A RegExp object to check the page URL's against or a string
293
- * representing a regular expression to check the page URL's against.
294
- * @param {Function} callback The function to call if this page (url) messages.
295
- * @return {Boolean} True is the function was registered, false otherwise.
296
- */
297
- const registerCallback = function (pattern, callback) {
298
- const regex = makeRegex(pattern);
299
- if (regex !== null && typeof callback === 'function') {
300
- OP.callbacks.push([regex, callback]);
301
- return true;
302
- }
303
- return false;
304
- };
305
-
306
- /**
307
- * Send the reload message to all connected pages.
308
- */
309
- const reloadPages = function () {
310
- // Send the reload message to all connections.
311
- const keys = Object.keys(CONNECTIONS);
312
- keys.forEach((key) => {
313
- CONNECTIONS[key].forEach((socket) => {
314
- socket.send('reload');
315
- });
316
- });
317
- };
318
-
319
- /**
320
- * Reload a single page or single group of font-end pages matching a specified pattern.
321
- *
322
- * @param {RegExp|String} pattern A RegExp object to check the page URL's against, a string
323
- * representing a regular expression to check the page URL's
324
- * against, or a string representing the pages front-end ID.
325
- * @return {null} Used only as a short circuit.
326
- */
327
- const reloadSinglePage = function (pattern) {
328
- const original = pattern.toString();
329
- if (whatIs(pattern) !== 'regexp') {
330
- pattern = makeRegex(pattern);
331
- }
332
- // See if the pattern matches a specific URL and reload all those pages.
333
- const keys = Object.keys(CONNECTIONS);
334
- if (pattern != null) {
335
- for (let i = 0; i < keys.length; i++) {
336
- if (pattern.test(keys[i])) {
337
- CONNECTIONS[keys[i]].forEach((socket) => {
338
- socket.send('reload');
339
- });
340
- return;
341
- }
342
- }
343
- }
344
- // See if the pattern was a particular page ID.
345
- for (let i = 0; i < keys.length; i++) {
346
- const key = keys[i];
347
- for (let s = 0; s < CONNECTIONS[key].length; s++) {
348
- const socket = CONNECTIONS[key][s];
349
- if (socket.nssUID === original) {
350
- socket.send('reload');
351
- return;
352
- }
353
- }
354
- }
355
- };
356
-
357
- /**
358
- * Send the refreshCSS message to all connected pages; this reloads only the CSS
359
- * and not the whole page.
360
- */
361
- const reloadStyles = function () {
362
- // Send the refreshCSS message to all connections.
363
- const keys = Object.keys(CONNECTIONS);
364
- keys.forEach((key) => {
365
- CONNECTIONS[key].forEach((socket) => {
366
- socket.send('refreshCSS');
367
- });
368
- });
369
- };
370
-
371
- /**
372
- * Reload the stylesheets for a single page or single group of pages matching a
373
- * specified pattern.
374
- *
375
- * @param {RegExp|String} pattern A RegExp object to check the page URL's against, a string
376
- * representing a regular expression to check the page URL's
377
- * against, or a string representing the pages front-end ID.
378
- * @return {null} Used only as a short circuit.
379
- */
380
- const reloadSingleStyles = function (pattern) {
381
- const original = pattern.toString();
382
- if (whatIs(pattern) !== 'regexp') {
383
- pattern = makeRegex(pattern) || original;
384
- }
385
- // See if the pattern matches a specific URL and reload all those pages.
386
- const keys = Object.keys(CONNECTIONS);
387
- if (pattern != null) {
388
- for (let i = 0; i < keys.length; i++) {
389
- if (pattern.test(keys[i])) {
390
- CONNECTIONS[keys[i]].forEach((socket) => {
391
- socket.send('refreshCSS');
392
- });
393
- return;
394
- }
395
- }
396
- }
397
- // See if the pattern was a particular page ID.
398
- for (let i = 0; i < keys.length; i++) {
399
- const key = keys[i];
400
- for (let s = 0; s < CONNECTIONS[key].length; s++) {
401
- const socket = CONNECTIONS[key][s];
402
- if (socket.nssUID === original) {
403
- socket.send('refreshCSS');
404
- return;
405
- }
406
- }
407
- }
408
- };
409
-
410
- /**
411
- * The server listener for NSS, all HTTP requests are handled here.
412
- *
413
- * @param {Object} request The HTTP request object.
414
- * @param {Object} response The HTTP response object waiting for communication back.
415
- * @return {null} Used only as a short circuit.
416
- */
417
- const serverListener = function (request, response) {
418
-
419
- const requestURL = request.url.replace(OP.root, '').replace(/(\.{1,2}[\\/])/g, '');
420
- let safeURL = Path.normalize(Path.join(OP.root, requestURL.split(/[?#]/)[0]));
421
- const filename = Path.basename(safeURL);
422
-
423
- if (filename === 'websocket.ws') {
424
- /*
425
- * ERROR: This should never trigger! If it does that means the server has an issue.
426
- * We can try and send back a 100 to save the socket connection but we might be
427
- * toast, http.createServer is having issues on this machine/ network.
428
- */
429
- response.writeHead(HTTPStatus.continue, getHeaders('text/plain'));
430
- return;
431
- }
432
-
433
- // If accessing a directory that has an OP.indexPage route users to that page instead.
434
- if (FS.existsSync(safeURL) && FS.lstatSync(safeURL).isDirectory()) {
435
- const index = Path.join(safeURL, OP.indexPage);
436
- if (FS.existsSync(index)) {
437
- safeURL = index;
438
- }
439
- }
440
-
441
- // Make sure we have permissions to view this page and then show it if we can.
442
- FS.access(safeURL, FS.constants.R_OK, (exists) => {
443
-
444
- // 404 page not found.
445
- if (exists !== null) {
446
- // If this 404 error is for a missing favicon use NSS's default.
447
- if (filename === 'favicon.ico') {
448
- const buffer = FS.readFileSync(Path.normalize(`${__dirname}/sources/favicon.ico`), { encoding: 'utf8', flag: 'r' });
449
- response.writeHead(HTTPStatus.found, getHeaders('image/x-icon'));
450
- response.write(buffer);
451
- } else {
452
- response.writeHead(HTTPStatus.notFound, getHeaders('text/html'));
453
- response.write('<h1>404 Page Not Found</h1>');
454
- response.write(`<p>The requested URL ${safeURL.replace(OP.root, '')} was not found on this server.</p>`);
455
- }
456
- response.end();
457
- return;
458
- }
459
-
460
- // Pull in the inject script now because we may need it for a directory listing page.
461
- const injectScript = Path.normalize(`${__dirname}/sources/socket.html`);
462
- const inject = FS.readFileSync(injectScript, { encoding: 'utf8', flag: 'r' });
463
-
464
- // Directory stop processing and show directory listing if enabled.
465
- if (FS.lstatSync(safeURL).isDirectory()) {
466
- if (OP.dirListing) {
467
- const buffer = FS.readFileSync(Path.normalize(`${__dirname}/sources/dir-listing.html`), { encoding: 'utf8', flag: 'r' });
468
- let html = buffer.toString();
469
- const path = `/${safeURL.replace(OP.root, '')}`;
470
- const [fileList, dirList] = getDirList(safeURL);
471
- html = html.replace('${path}', path);
472
- html = html.replace('${files}', fileList);
473
- html = html.replace('${directories}', dirList);
474
- response.writeHead(HTTPStatus.ok, getHeaders('text/html'));
475
- response.write(html);
476
- response.write(inject);
477
- } else {
478
- response.writeHead(HTTPStatus.forbidden, getHeaders('text/html'));
479
- response.write('<h1>Forbidden</h1>');
480
- response.write(`<p>You do not have permission to access ${safeURL.replace(OP.root, '')} on this server.</p>`);
481
- response.write('<p>Directory listing disabled.</p>');
482
- }
483
- response.end();
484
- return;
485
- }
486
-
487
- // Actual file we can attempt to serve it.
488
- FS.readFile(safeURL, 'binary', (err, file) => {
489
-
490
- // 500 server error.
491
- if (err) {
492
- // Put the error in the browser
493
- response.writeHead(HTTPStatus.error, getHeaders('text/plain'));
494
- response.write(`${err}\n`);
495
- response.end();
496
- return;
497
- }
498
-
499
- // If the requested file has a matching MIME-Type use it or use the default;
500
- let contentType = ContentTypes[Path.extname(safeURL)];
501
- if (!contentType) {
502
- contentType = OP.contentType;
503
- }
504
-
505
- // Output the file to the browser.
506
- response.writeHead(HTTPStatus.ok, getHeaders(contentType, safeURL));
507
-
508
- // If needed inject NSS's WebSocket at the end of the page.
509
- if (RELOAD.includes(Path.extname(safeURL))) {
510
- let html = file.toString();
511
- const last = html.lastIndexOf('</body>');
512
- if (last && last > 0) {
513
- const start = html.substr(0, last);
514
- const end = html.substr(last);
515
- html = start + inject + end;
516
- response.write(html, 'utf8');
517
- } else {
518
- response.write(file, 'binary');
519
- }
520
- } else {
521
- response.write(file, 'binary');
522
- }
523
-
524
- // Close initial connection.
525
- response.end();
526
- });
527
-
528
- });
529
- };
530
-
531
- /**
532
- * The WebSocket listener for NSS, all WebSocket requests are handled here.
533
- *
534
- * @param {Object} socket The WebSocket object for this connection.
535
- * @param {Object} request The incoming initial connection.
536
- */
537
- const socketListener = function (socket, request) {
538
-
539
- // Strip the page ID and /ws tag off the url to get the actual url.
540
- let cleanURL = request.url.substr(1, request.url.indexOf('/ws?id=') - 1);
541
- if (!cleanURL) {
542
- cleanURL = OP.indexPage;
543
- }
544
-
545
- // Record the unique page ID directly on the socket object.
546
- const pageID = request.url.substr(request.url.indexOf('?id=')).replace('?id=', '');
547
- socket.nssUID = pageID;
548
-
549
- // Record new socket connections.
550
- if (CONNECTIONS[cleanURL]) {
551
- CONNECTIONS[cleanURL].push(socket); // This page has opened multiple times.
552
- } else {
553
- CONNECTIONS[cleanURL] = [socket]; // Fist time we've seen this page.
554
- }
555
-
556
- // If auto restart is supposed to be disabled tell the page now.
557
- if (OP.disableAutoRestart && OP.disableAutoRestart === true) {
558
- socket.send('disableAutoRestart');
559
- }
560
-
561
- // Handle future incoming WebSocket messages from this page.
562
- socket.on('message', (message) => {
563
- // See if the message belongs to a callback and send it there.
564
- for (let i = 0; i < OP.callbacks.length; i++) {
565
- const regex = OP.callbacks[i][0];
566
- const callback = OP.callbacks[i][1];
567
- if (regex.test(cleanURL)) {
568
- callback(message.toString(), pageID);
569
- return;
570
- }
571
- }
572
- // No one is listening for this message.
573
- console.log(`Unanswered WebSocket message from ${cleanURL}: ${message.toString()}`);
574
- });
575
-
576
- // When a connection closes remove it from CONNECTIONS.
577
- socket.on('close', () => {
578
- // Remove this page from our list of active connections.
579
- const connections = CONNECTIONS[cleanURL];
580
- for (let i = 0; i < connections.length; i++) {
581
- if (connections[i].nssUID === pageID) {
582
- connections.splice(i, 1);
583
- break;
584
- }
585
- }
586
- });
587
-
588
- };
589
-
590
- /**
591
- * Attempt to start the HTTP server and WebSocket listener.
592
- *
593
- * @param {Int|null} port Allows force overriding of port number; you should usually not use
594
- * this, it's meant to be used internally to NSS.
595
- * @param {Function} [callback] Optional function to call when the server successfully starts
596
- * (true) or gives up on trying to start (false);
597
- * @return {null} Used only as a short circuit.
598
- */
599
- const start = function (port, callback) {
600
-
601
- // Post is usually internal to NSS so check if a user placed the callback first.
602
- if (port && typeof port === 'function') {
603
- callback = port;
604
- port = null;
605
- }
606
-
607
- // Make sure we have a proper callback function or null the variable.
608
- if (callback && typeof callback !== 'function') {
609
- callback = null;
610
- }
611
-
612
- // Don't start an already running server.
613
- if (OP.running) {
614
- console.log('Server is already running.');
615
- // Notify the callback.
616
- if (callback) {
617
- callback(true);
618
- }
619
- return;
620
- }
621
-
622
- // Create the HTTP server.
623
- SERVER = Http.createServer(serverListener);
624
- // Capture connection upgrade requests so we don't break WebSocket connections.
625
- SERVER.on('upgrade', (request, socket) => {
626
- /*
627
- * Node's http server is capable of handling websocket but you have to manually
628
- * handle a lot of work like the handshakes. We use WebSocket.Server to avoid
629
- * having to do all that extra work. See the following if you want to handle
630
- * websocket without the WebSocket module:
631
- * https://medium.com/hackernoon/implementing-a-websocket-server-with-node-js-d9b78ec5ffa8
632
- *
633
- * SERVER.upgrade will clash with SOCKET.connection by running first. Currently
634
- * SERVER.upgrade is not used but in case we do in the future ignore all websocket
635
- * requests so they get passed on to the SOCKET.connection listener.
636
- */
637
- if (request.headers.upgrade === 'websocket') {
638
- // eslint-disable-next-line no-useless-return
639
- return;
640
- }
641
- });
642
- // Capture server errors and respond as needed.
643
- SERVER.on('error', (error) => {
644
- // The port we tried to use is taken, increment and try to start again.
645
- if (error.code === 'EADDRINUSE') {
646
- if (port) {
647
- // Stop trying new ports after 100 attempts.
648
- if (OP.port - port > 100) {
649
- console.log(`FATAL ERROR: Could not find an available port number in the range of ${OP.port}–${OP.port + 100}.`);
650
- // Notify the callback.
651
- if (callback) {
652
- callback(false);
653
- }
654
- return;
655
- }
656
- start(port + 1, callback);
657
- } else {
658
- start(OP.port + 1, callback);
659
- }
660
- }
661
- });
662
-
663
- // Attempt to start the server now.
664
- port = port || OP.port;
665
- SERVER.listen(port, () => {
666
- // Server started successfully without error.
667
- OP.running = true;
668
-
669
- // Start the WebSocket Server on the same port.
670
- SOCKET = new WebSocket.Server({ server: SERVER });
671
- SOCKET.on('connection', socketListener);
672
-
673
- // Warn the user if we had to change port numbers.
674
- if (port && (port !== OP.port)) {
675
- console.log(`Port ${OP.port} was in use, switched to using ${port}.\n`);
676
- }
677
-
678
- // Log the ip addresses being watched.
679
- console.log('Node Simple Server live @:');
680
- const addresses = getAddresses(port);
681
- addresses.forEach((address) => {
682
- console.log(` ${address}`);
683
- });
684
- console.log('');
685
-
686
- // Notify the callback.
687
- if (callback) {
688
- callback(true);
689
- }
690
- });
691
- };
692
-
693
- /**
694
- * Stop the HTTP server and WebSocket listener gracefully.
695
- *
696
- * @param {Function} [callback] Optional function to call when the server successfully
697
- * stops (true).
698
- */
699
- const stop = function (callback) {
700
- if (OP.running) {
701
- // If any back-end files are being watched for changes stop monitoring them.
702
- watchEnd();
703
- // Close all socket connections; these would force the server to stay up.
704
- const keys = Object.keys(CONNECTIONS);
705
- keys.forEach((key) => {
706
- CONNECTIONS[key].forEach((socket) => {
707
- socket.send('close');
708
- socket.close();
709
- });
710
- });
711
- // Now gracefully close SERVER and SOCKET.
712
- SERVER.close();
713
- SOCKET.close();
714
- // Reset NSS.
715
- SERVER = null;
716
- SOCKET = null;
717
- OP.running = false;
718
- console.log('Server has been stopped.');
719
- }
720
- // Notify the callback.
721
- if (callback) {
722
- callback(true);
723
- }
724
- };
725
-
726
- /**
727
- * Stop watching directories or files for changes; previously registered with watch().
728
- *
729
- * Warning: If you watched a directory for changes watch() will auto watch all contents
730
- * of that directory recursively. You will need a different paths argument to truly
731
- * remove all files, it may be easier to call watchEnd() and then restart the watch()
732
- * you still need.
733
- *
734
- * @param {String|Array} paths Files, directories, or glob patterns for tracking. Takes an
735
- * array of strings or just one string.
736
- */
737
- const unwatch = function (paths) {
738
- // Convert paths to array if it's not already.
739
- if (whatIs(paths) === 'string') {
740
- paths = [paths];
741
- }
742
- // Search all watchers and remove any paths that match.
743
- WATCHING.forEach((watcher) => {
744
- watcher.unwatch(paths);
745
- });
746
- };
747
-
748
- /**
749
- * Unregister a back-end function that was set with registerCallback.
750
- *
751
- * @param {String} pattern The same regular expression (regex) object or string that was
752
- * used when the callback function was first registered.
753
- * @param {Function} callback The function that was originally registered as the callback.
754
- * @return {Boolean} True is the function was unregistered, false otherwise.
755
- */
756
- const unregisterCallback = function (pattern, callback) {
757
- // Make sure the pattern is not null.
758
- let oldRegex = makeRegex(pattern);
759
- if (!oldRegex) {
760
- return false;
761
- }
762
- // Convert the regex to a string otherwise comparing will never work.
763
- oldRegex = oldRegex.toString();
764
- // Remove the pattern and callback from the registered callbacks if they exits.
765
- for (let i = 0; i < OP.callbacks.length; i++) {
766
- const regex = OP.callbacks[i][0].toString();
767
- const func = OP.callbacks[i][1];
768
- if (regex === oldRegex && func === callback) {
769
- OP.callbacks.splice(i, 1);
770
- return true;
771
- }
772
- }
773
- return false;
774
- };
775
-
776
- /**
777
- * Start watching a file, files, directory, or directories for changes and then callback to
778
- * functions that can/ will respond to these changes.
779
- *
780
- * @param {String|Array} paths Files, directories, or glob patterns for tracking. Takes an
781
- * array of strings or just one string.
782
- * @param {Object} options A special configuration object that includes {@link https://github.com/paulmillr/chokidar|chokidar} options and NSS options.
783
- * See NSS's README for more information, options.events is required!
784
- * @return {Boolean} True if it appears everything worked, false if something was missing or
785
- * an error was thrown.
786
- */
787
- const watch = function (paths, options) {
788
- // Insure we have an options object.
789
- if (!options || !options.events) {
790
- return false;
791
- }
792
- /*
793
- * For security and a better user experience set the watchers current working
794
- * directory to NSS's root if the setting is missing.
795
- */
796
- if (!options.cwd) {
797
- options.cwd = OP.root;
798
- }
799
- // Convert paths to array if it's not already.
800
- if (whatIs(paths) === 'string') {
801
- paths = [paths];
802
- }
803
- try {
804
- // Start watching the path(s).
805
- const watcher = Chokidar.watch(paths, options);
806
- WATCHING.push(watcher);
807
- // Hookup requested listeners; they are case sensitive so type them right in your code!
808
- const safe = ['all', 'add', 'addDir', 'change', 'unlink', 'unlinkDir', 'ready', 'raw', 'error'];
809
- Object.keys(options.events).forEach((key) => {
810
- if (safe.includes(key)) {
811
- watcher.on(key, options.events[key]);
812
- }
813
- });
814
- } catch (error) {
815
- console.log(error);
816
- return false;
817
- }
818
- return true;
819
- };
820
-
821
- /**
822
- * Stop watching registered file, files, directory, or directories for changes.
823
- */
824
- const watchEnd = function () {
825
- WATCHING.forEach((watcher) => {
826
- watcher.close();
827
- });
828
- WATCHING.splice(0, WATCHING.length);
829
- };
830
-
831
- /**
832
- * The fastest way to get the actual type of anything in JavaScript; {@link https://jsbench.me/ruks9jljcu/2| Benchmarks}.
833
- *
834
- * @param {*} unknown Anything you wish to check the type of.
835
- * @return {String|undefined} The type of the unknown value passed in or undefined.
836
- */
837
- const whatIs = function (unknown) {
838
- try {
839
- return ({}).toString.call(unknown).match(/\s([^\]]+)/)[1].toLowerCase();
840
- } catch (e) { return undefined; }
841
- };
842
-
843
- // Configure the new instance of NSS.
844
- initialize(options);
845
-
846
- // Public methods.
847
- return {
848
- getAddresses,
849
- getWatched,
850
- message,
851
- registerCallback,
852
- reloadPages,
853
- reloadSinglePage,
854
- reloadSingleStyles,
855
- reloadStyles,
856
- start,
857
- stop,
858
- unregisterCallback,
859
- unwatch,
860
- watch,
861
- watchEnd
862
- };
863
-
864
- }
865
-
866
- module.exports = NodeSimpleServer;