slower 1.1.8

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/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./lib/router');
package/lib/router.js ADDED
@@ -0,0 +1,316 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const { noop, slugify, isSparseEqual, last, renderDynamicHTML } = require('./utils');
4
+
5
+ class Route {
6
+ constructor (path, type, callback) {
7
+ this.path = (path.startsWith('/') ? path : '/' + path);
8
+ this.type = SlowerRouter.http_methods.includes(slugify(type).toUpperCase()) ? slugify(type).toUpperCase() : null;
9
+ this.callback = (typeof callback == 'function' ? callback : noop);
10
+ }
11
+ }
12
+
13
+ class SlowerRouter {
14
+ static http_methods = [
15
+ 'GET', 'POST', 'PUT',
16
+ 'HEAD', 'DELETE', 'OPTIONS',
17
+ 'TRACE', 'COPY', 'LOCK',
18
+ 'MKCOL', 'MOVE', 'PURGE',
19
+ 'PROPFIND', 'PROPPATCH', 'UNLOCK',
20
+ 'REPORT', 'MKACTIVITY', 'CHECKOUT',
21
+ 'MERGE', 'M-SEARCH', 'NOTIFY',
22
+ 'SUBSCRIBE', 'UNSUBSCRIBE', 'PATCH',
23
+ 'SEARCH', 'CONNECT'
24
+ ];
25
+
26
+ static mime_table = {
27
+ 'txt' : ['text/plain', 'utf-8'],
28
+ 'html': ['text/html', 'utf-8'],
29
+ 'js' : ['text/javascript', 'utf-8'],
30
+ 'css' : ['text/css', 'utf-8'],
31
+ 'ico' : ['image/png'],
32
+ 'default': ['application/octet-stream']
33
+ }
34
+
35
+ constructor () {
36
+ this.routes = [];
37
+ this.middleware = [noop];
38
+ this.fallback = noop;
39
+ this.allowedMethods = [];
40
+ this.blockedMethodCallback = noop;
41
+ }
42
+
43
+ /**
44
+ * Defines a route for a determined request method
45
+ * @category Router
46
+ * @param {String} path The route that will be defined
47
+ * @param {String} type The method to respond to
48
+ * @param {Function} callback Callback to use when the chosen route is accessed
49
+ * @returns {Object} An Route object
50
+ * @example <caption> Defining a simple GET route:</caption>
51
+ * setRoute('/', 'GET', (req, res) => {
52
+ * console.log(res.url);
53
+ * res.end('received');
54
+ * });
55
+ * // => <Route> { path:..., type:..., callback:... }
56
+ */
57
+ setRoute = function (path = '/', type = 'GET', callback) {
58
+ let stat = new Route(path, type, callback);
59
+ this.routes.push(stat);
60
+ return stat;
61
+ }
62
+
63
+ /**
64
+ * Defines a middleware for all requests
65
+ * @category Middleware
66
+ * @param {Function} callback Callback to use when the requests are made
67
+ * @returns {Array} The used middlewares list
68
+ */
69
+ setMiddleware = function (callback) {
70
+ return this.middleware.push((typeof callback == 'function' ? callback : noop));
71
+ }
72
+
73
+ // SERVES FILES FOR SPECIFIC PATHS. WILDCARS ALLOWED. BE CAREFUL - PATH TRAVERSAL MAY BE POSSIBLE
74
+ // Use app.setStatic('/*', __dirname+'/') for serving all local files.
75
+ // or use app.setStatic('/*', __dirname+'/public/') for serving files in a specific folder (public).
76
+ // This is a one-liner for the setRoute function, when the route responds with a simple page or document
77
+ /**
78
+ * Defines a route for a determined request method
79
+ * @category Router
80
+ * @param {String} path The route that will be defined
81
+ * @param {String} file The file path that will be used to respond to the route
82
+ * @param {String} mime The file's mime type
83
+ * @param {Object} replacementData The replacement data map for the dynamic HTML rendering
84
+ * @returns {Object} An Route object already configured
85
+ * @example <caption> Defining a simple GET route:</caption>
86
+ * setStatic('/login', __dirname+'/public/static/views/login.html', 'text/html', 'utf8');
87
+ */
88
+ /**
89
+ * Dynamic rendering example:
90
+ * It's a template engine, to render HTML containing template spaces.
91
+ * The charset for replacement is <{content}>
92
+ * @since 1.2.5
93
+ *
94
+ * @param {String} html The HTML code
95
+ * @param {Object} patterns The patterns to replace in the HTML code
96
+ * @return {String} The HTML with the templates replaces
97
+ *
98
+ * @example <caption> Rendering: </caption>
99
+ * var template = 'Hello, my name is <{name}>. I\\'m <{age}> years old.';
100
+ * console.log(TemplateEngine(template, {
101
+ * name: "Krasimir",
102
+ * age: 29
103
+ * }));
104
+ */
105
+ setDynamic = function (path, file = '', mime = '', replacementData) {
106
+ let encoding = (mime === SlowerRouter.mime_table['default'] ? undefined : 'utf-8')
107
+ let stat = new Route(path, 'GET', (req, res) => {
108
+ let data, targetFile, extension, targetMime, targetEncoding;
109
+ if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
110
+ targetFile = file.replace(/\//gim, '\\');
111
+ targetFile = ((targetFile.endsWith('\\')) ? targetFile : targetFile+'\\') + req.url.replace('/', '');
112
+ } else {
113
+ targetFile = ((file == '' || !file) ? req.url : file);
114
+ }
115
+ extension = last(targetFile.split('.'));
116
+ targetMime = mime || (SlowerRouter.mime_table[extension]?.[0] || SlowerRouter.mime_table.default[0]);
117
+ targetEncoding = encoding || (SlowerRouter.mime_table[extension]?.[1] || SlowerRouter.mime_table.default[1]);
118
+ try {
119
+ data = fs.readFileSync(targetFile, targetEncoding);
120
+ } catch (err) {
121
+ data = '';
122
+ }
123
+ try {
124
+ if (replacementData) data = renderDynamicHTML(data, replacementData);
125
+ } catch (err) {}
126
+ res.writeHead(200, { 'Content-Type': targetMime });
127
+ res.write(data);
128
+ res.end();
129
+ });
130
+ this.routes.push(stat);
131
+ return stat;
132
+ }
133
+
134
+ // SERVES FILES FOR SPECIFIC PATHS. WILDCARS ALLOWED. BE CAREFUL - PATH TRAVERSAL MAY BE POSSIBLE
135
+ // Use app.setStatic('/*', __dirname+'/') for serving all local files.
136
+ // or use app.setStatic('/*', __dirname+'/public/') for serving files in a specific folder (public).
137
+ // This is a one-liner for the setRoute function, when the route responds with a simple page or document
138
+ /**
139
+ * Defines a route for a determined request method
140
+ * @category Router
141
+ * @param {String} path The route that will be defined
142
+ * @param {String} file The file path that will be used to respond to the route
143
+ * @param {String} mime The file's mime type
144
+ * @param {String} encoding The file's encoding (defaults to UTF-8)
145
+ * @returns {Object} An Route object already configured
146
+ * @example <caption> Defining a simple GET route:</caption>
147
+ * setStatic('/login', __dirname+'/public/static/views/login.html', 'text/html', 'utf8');
148
+ */
149
+ setStatic = function (path, file = '', mime = '') {
150
+ let encoding = (mime === SlowerRouter.mime_table['default'] ? undefined : 'utf-8')
151
+ let stat = new Route(path, 'GET', (req, res) => {
152
+ let data, targetFile, extension, targetMime, targetEncoding;
153
+ if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
154
+ targetFile = file.replace(/\//gim, '\\');
155
+ targetFile = ((targetFile.endsWith('\\')) ? targetFile : targetFile+'\\') + req.url.replace('/', '');
156
+ } else {
157
+ targetFile = ((file == '' || !file) ? req.url : file);
158
+ }
159
+ extension = last(targetFile.split('.'));
160
+ targetMime = mime || (SlowerRouter.mime_table[extension]?.[0] || SlowerRouter.mime_table.default[0]);
161
+ targetEncoding = encoding || (SlowerRouter.mime_table[extension]?.[1] || SlowerRouter.mime_table.default[1]);
162
+ try {
163
+ data = fs.readFileSync(targetFile, targetEncoding);
164
+ } catch (err) {
165
+ data = '';
166
+ }
167
+ res.writeHead(200, { 'Content-Type': targetMime });
168
+ res.write(data);
169
+ res.end();
170
+ });
171
+ this.routes.push(stat);
172
+ return stat;
173
+ }
174
+
175
+ /**
176
+ * Defines a default response for non-defined routes (custom treated 404 error)
177
+ * @category Router
178
+ * @param {String} callback The function to execute for unhandled routes
179
+ * @returns {undefined}
180
+ * @example <caption> Defining a simple GET route:</caption>
181
+ * setStatic('/login', __dirname+'/public/static/views/login.html', 'text/html', 'utf8');
182
+ */
183
+ setFallback = function (callback) { this.fallback = callback; }
184
+
185
+ // SERVES FILES FOR SPECIFIC PATHS. WILDCARS ALLOWED. BE CAREFUL - PATH TRAVERSAL MAY BE POSSIBLE
186
+ // Use app.setStatic('/*', __dirname+'/') for serving all local files.
187
+ // or use app.setStatic('/*', __dirname+'/public/') for serving files in a specific folder (public).
188
+ // This is a one-liner for the setRoute function, when the route responds with a simple page or document
189
+ /**
190
+ * Defines a route for a determined request method
191
+ * @category Router
192
+ * @param {String} path The route that will be defined
193
+ * @param {String} file The file path that will be used to respond to the route
194
+ * @param {String} mime The file's mime type
195
+ * @param {Object} replacementData The replacement data map for the dynamic HTML rendering
196
+ * @returns {Object} An Route object already configured
197
+ * @example <caption> Defining a simple GET route:</caption>
198
+ * setStatic('/login', __dirname+'/public/static/views/login.html', 'text/html', 'utf8');
199
+ */
200
+ /**
201
+ * Dynamic rendering example:
202
+ * It's a template engine, to render HTML containing template spaces.
203
+ * The charset for replacement is <{content}>
204
+ * @since 1.2.5
205
+ *
206
+ * @param {String} html The HTML code
207
+ * @param {Object} patterns The patterns to replace in the HTML code
208
+ * @return {String} The HTML with the templates replaces
209
+ *
210
+ * @example <caption> Rendering: </caption>
211
+ * var template = 'Hello, my name is <{name}>. I\\'m <{age}> years old.';
212
+ * console.log(TemplateEngine(template, {
213
+ * name: "Krasimir",
214
+ * age: 29
215
+ * }));
216
+ */
217
+ setFallbackFile = function (file = '', mime = '', replacementData) {
218
+ this.fallback = function fb (req,res) {
219
+ let encoding = (mime === SlowerRouter.mime_table['default'] ? undefined : 'utf-8')
220
+ let data, targetFile, extension, targetMime, targetEncoding;
221
+ if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
222
+ targetFile = file.replace(/\//gim, '\\');
223
+ targetFile = ((targetFile.endsWith('\\')) ? targetFile : targetFile+'\\') + req.url.replace('/', '');
224
+ } else {
225
+ targetFile = ((file == '' || !file) ? req.url : file);
226
+ }
227
+ extension = last(targetFile.split('.'));
228
+ targetMime = mime || (SlowerRouter.mime_table[extension]?.[0] || SlowerRouter.mime_table.default[0]);
229
+ targetEncoding = encoding || (SlowerRouter.mime_table[extension]?.[1] || SlowerRouter.mime_table.default[1]);
230
+ try {
231
+ data = fs.readFileSync(targetFile, targetEncoding);
232
+ } catch (err) {
233
+ data = '';
234
+ }
235
+ try {
236
+ if (replacementData) data = renderDynamicHTML(data, replacementData);
237
+ } catch (err) {}
238
+ res.writeHead(200, { 'Content-Type': targetMime });
239
+ res.write(data);
240
+ res.end();
241
+ }
242
+ }
243
+
244
+ // Sets the methods that the application will respond to. The rest is simply discarded with empty responses.
245
+ // To configure a deeper level of error handling, or to serve customized 'METHOD_NOT_ALLOWED' errors,
246
+ // use 'setRoute' with params ('/*', {{method_name}}, (req,res) => { and serve the error file here });
247
+ // INFO: Middlewares are still triggered when a blocked method request comes.
248
+ /**
249
+ * Defines a route for a determined request method
250
+ * @category Router
251
+ * @param {Array} methods The methods that are allowed by the application. Methods that do not conform with standards are ignored.
252
+ * @returns {Object} The AllowedMethods Array object.
253
+ * @example <caption> Allowing only GET and POST:</caption>
254
+ * app.setAllowedMethods(['GET', 'POST']);
255
+ */
256
+ setAllowedMethods = function (methods = []) {
257
+ // If not specified, all methods are allowed
258
+ if (methods.length == 0 || methods == '*') {
259
+ return this.allowedMethods = JSON.parse(JSON.stringify(SlowerRouter.http_methods));
260
+ }
261
+ for (let i = 0; i < methods.length; i++) {
262
+ if (!SlowerRouter.http_methods.includes(methods[i])) continue;
263
+ this.allowedMethods.push(methods[i]);
264
+ }
265
+ // Default callback for blocked methods respond with code 200 and an empty response
266
+ this.blockedMethodCallback = function (req,res) {
267
+ res.writeHead(405); // sends the meme error 418 'i am a teapot'
268
+ res.end();
269
+ };
270
+ return this.allowedMethods;
271
+ }
272
+
273
+ start = function (port = 8080, callback) {
274
+ let routes = this.routes;
275
+ let middle = this.middleware;
276
+ let fallback = this.fallback;
277
+ let allowedMethods = this.allowedMethods;
278
+ let blockedMethodCallback = this.blockedMethodCallback;
279
+
280
+ let server = http.createServer(function (req, res) {
281
+ try {
282
+ // Runs all middlewares
283
+ for (let i = 0; i < middle.length; i++) { middle[i](req, res); }
284
+ // Only respond to allowed methods with callbacks, else, use the default empty response.
285
+ if (
286
+ allowedMethods.includes(req.method)) {
287
+ for (let i = 0; i < routes.length; i++) {
288
+ let route = routes[i];
289
+ if (
290
+ route.type === req.method &&
291
+ (
292
+ route.path === req.url ||
293
+ isSparseEqual(route.path, req.url)
294
+ )
295
+ ) {
296
+ i = routes.length;
297
+ route.callback(req, res);
298
+ return;
299
+ }
300
+ }
301
+ fallback(req,res);
302
+ } else {
303
+ blockedMethodCallback(req, res);
304
+ }
305
+ } catch(err) {
306
+ console.log(err);
307
+ }
308
+ });
309
+ callback(server);
310
+ return server.listen(port);
311
+ }
312
+ }
313
+
314
+ const Slower = function () { return new SlowerRouter(); }
315
+
316
+ module.exports = Slower;
package/lib/utils.js ADDED
@@ -0,0 +1,74 @@
1
+ const noop = function () {};
2
+
3
+ const last = (array) => { return array[array.length-1]; }
4
+
5
+ const slugify = (string, replacement = '-', replaceSpaces = true) => {
6
+ return (string
7
+ .replace(/<(?:.|\n)*?>/gm, '')
8
+ .replace(/[!\"#$%&'\(\)\*\+,\/:;<=>\?\@\[\\\]\^`\{\|\}~]/g, '')
9
+ .replace((replaceSpaces ? /(\s|\.)/g : /(\.)/g), replacement)
10
+ .replace(/—/g, replacement)
11
+ .replace(/-{2,}/g, replacement));
12
+ }
13
+
14
+ /**
15
+ * Compares two strings in 'sparse' mode. Using the wildcards '{*}' and '{?}' to match strings.
16
+ * Use '{*}' for any number of (any) characters, and {?}' for one (any) character.
17
+ *
18
+ * @since 1.2.7
19
+ *
20
+ * @param {String} str1 The first string to compare
21
+ * @param {String} str2 The second string to compare
22
+ * @return {Boolean} If the strings are sparsely equal or not
23
+ * @example <caption> Comparing simple strings: </caption>
24
+ * isSparseEqual("hello", "hello")
25
+ * // => true
26
+ * isSparseEqual("hello", "wello")
27
+ * // => false
28
+ * @example <caption> Comparing complex strings: </caption>
29
+ * isSparseEqual("{?}ello", "hello")
30
+ * // => true
31
+ * isSparseEqual("h*", "hello")
32
+ * // => true
33
+ * isSparseEqual("h{*}e", "hello")
34
+ * // => false
35
+ * isSparseEqual("h{*}e", "helle")
36
+ * // => true
37
+ */
38
+ const isSparseEqual = (str1 = '', str2 = '') => {
39
+ const string1 = str1.replace(/{\?}/g, '.').replace(/{\*}/g, '.*');
40
+ const string2 = str2.replace(/{\?}/g, '.').replace(/{\*}/g, '.*');
41
+ const regex = new RegExp(`^${string1}$`);
42
+ return regex.test(string2);
43
+ }
44
+
45
+ /**
46
+ * It's a template engine, to render HTML containing template spaces.
47
+ * The charset for replacement is <{content}>
48
+ * @since 1.2.5
49
+ *
50
+ * @param {String} html The HTML code
51
+ * @param {Object} patterns The patterns to replace in the HTML code
52
+ * @return {String} The HTML with the templates replaces
53
+ *
54
+ * @example <caption> Rendering: </caption>
55
+ * var template = 'Hello, my name is <{name}>. I\\'m <{age}> years old.';
56
+ * console.log(TemplateEngine(template, {
57
+ * name: "Krasimir",
58
+ * age: 29
59
+ * }));
60
+ */
61
+ const renderDynamicHTML = (html, patterns) => {
62
+ let template = html;
63
+ for (let item in patterns) {
64
+ template = html.replace(
65
+ new RegExp('<{'+item+'}>', 'gim'),
66
+ patterns[item]
67
+ );
68
+ }
69
+ return template;
70
+ }
71
+
72
+ const toBool = [() => true, () => false];
73
+
74
+ module.exports = { noop, slugify, isSparseEqual, toBool, last, renderDynamicHTML };
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "slower",
3
+ "version": "1.1.8",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "directories": {
7
+ "doc": "doc",
8
+ "lib": "lib"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC"
16
+ }
package/readme.md ADDED
@@ -0,0 +1,48 @@
1
+ # Slower
2
+
3
+ Slower is a small web framework, express-like, but simpler and limited.
4
+ It allows for generic route-declaration, fallback pages, and multiple middleware functions.
5
+
6
+
7
+ Example usage:
8
+ ```
9
+ const Slower = require('.');
10
+ const port = 8080;
11
+
12
+ let app = Slower();
13
+
14
+ app.setMiddleware((req, res) => {
15
+ req.time = Date.now();
16
+ console.log(`${req.time} - ${req.method} : ${req.url}`);
17
+ });
18
+
19
+ app.setStatic('/favicon.ico', __dirname+'/public/favicon.ico');
20
+
21
+ app.setRoute('/', 'GET', (req, res) => {
22
+ res.writeHead(200, { 'Content-Type': 'text/html' });
23
+ res.write('<html><body><p>This is the / page.</p></body></html>');
24
+ res.end();
25
+ });
26
+
27
+ app.setRoute('/{*}.css', 'GET', (req, res) => {
28
+ let data = fs.readFileSync(...some.css.file...)
29
+ res.writeHead(200, { 'Content-Type': 'text/css' });
30
+ res.write(data);
31
+ res.end();
32
+ });
33
+
34
+ const generateDownloadNumber = () => { Math.round(Math.random() * 10) }
35
+ app.setDynamic('/download/{?}/', './main-download.txt', { DowloadName: generateDownloadNumber });
36
+ // Responds to download routes such as '/download/2/'
37
+
38
+ app.setFallback((req, res) => {
39
+ res.writeHead(200, { 'Content-Type': 'text/html' });
40
+ res.write('<html><body><p>This is the fallback page.</p></body></html>');
41
+ res.end();
42
+ });
43
+
44
+ app.start(port, () => {
45
+ console.log(`Running on localhost:${port}`);
46
+ console.log(app);
47
+ });
48
+ ```