UNPKG

@kwaeri/router

Version:

The @kwaeri/router component of the @kwaer/node-kit application platform.

229 lines 9.64 kB
/** * SPDX-PackageName: kwaeri/router * SPDX-PackageVersion: 0.3.5 * SPDX-FileCopyrightText: © 2014 - 2022 Richard Winters <kirvedx@gmail.com> and contributors * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception OR MIT */ 'use strict'; // INCLUDES import * as events from 'events'; import * as fs from 'fs'; import debug from 'debug'; // DEFINES const DEBUG = debug('nodekit:router'); export class Router { /** * @var { events.EventEmitter } promoter */ promoter; /** * @var { Controller } */ controller; /** * @var { any } */ errorController; /** * @var { Session } */ session; /** * @var { string } */ baseAssetPath; /** * @var { string } */ derivedAssetPath; /** * Class constructor */ constructor(configuration) { DEBUG(`Loading the promoter`); // Create an instance of an event emitter and store it: this.promoter = new events.EventEmitter(); // Get the session object to derive from: const derivedSession = configuration.session.provider; // Store the base controller: this.controller = configuration.controller; // Store the error controller: this.errorController = configuration.errorController || null; // Set the base and derived asset path(s): const { base, root, asset_path, asset_provider } = configuration[configuration.appType]; this.baseAssetPath = base + root + asset_path; this.derivedAssetPath = this.baseAssetPath + '/' + asset_provider; // Remove references we do not want to pass forward with the configuration: delete configuration.session.provider; delete configuration.controller; // Store the promoter in the configuration to be passed down the dependency chain: configuration.promoter = this.promoter; // Instantiate the session provider: this.session = new derivedSession(configuration); DEBUG(`Starting the session service`); // Start the session service: this.session.start(); // Set the base controller's configuration: this.controller.configuration = configuration; } /** * Routes the client request * * @param { http.IncomingMessage } request * @param { http.ServerResponse } response * * @returns { void } */ async route(request, response) { // Save a reference of this instance (route didn't bind to router in // server so "this" is Server [post rewrite]): const router = this.router, // this { session, controller } = router, path = request.path, referrer = request.headers.referrer; // For why was it named referer? (missing second r) // Assets needs to be modifiable let assets = router.derivedAssetPath; // Check the type of content the request is for, and set the proper headers. // // In the below regexp, we ensure that a period comes before the media identifier at the // END of the string so that we do not get accidental matches from a path-naming coincidence // // We also suggest that there could be another period at the end of the string followed by // either gz, bz2, or map (css.map). This covers most media types let mediaType = path.match(/\.(css|less|js|eot|ttf|woff?2|ico|bmp|jpe?g|gif|png|svg|json|xml|text|zip|tar|mp3|mp4|mov|ogg|webm)\.?(gz|bz2|map)?$/); // JS files are sometimes fetched as media (e.g. from a script element, defined in the src // attribute). When this happens we will ALWAYS have a referer (that is a string value of // referer, this means that somebody is directly requesting a .JS file (Or perhaps their // browser is in privacy mode?). // // Depending on the case we handle it correctly, directly called JS file requests are sent // to the controller, while the router handles feeding any media JS files needed by the // application directly to the browser. if (mediaType !== null) { // Personally, I don't think we need to throw an exception just because a css file or // zip file wasn't found - that's annoying, especially in node.js environments. let doThrow = false; // Since we forced that check for the period, the proper media type is DEFINITELY at // array position [1], as array position [0] would be the original string matched // that the additional matches are drawn from: // // We'll potentially work with tar.gz|bz2 files - we've added a match group for them: if (mediaType[1] === 'tar') mediaType = mediaType[2].toString(); else mediaType = mediaType[1].toString(); // Now we check the value of mediaType and set the proper content-type for the response: switch (mediaType) { // This case covers .css.map as well, 'map' is in mediaType[2] until mediaType is // redefined above case 'css': case 'less': mediaType = 'text/css'; break; case 'js': mediaType = 'application/javascript'; break; case 'eot': case 'ttf': case 'woff': case 'woff2': mediaType = 'application/x-font-' + mediaType; break; case 'ico': case 'bmp': case 'gif': case 'jpg': case 'jpeg': case 'png': case 'svg': if (mediaType === 'ico') { mediaType = 'image/x-icon'; // If this is a favicon we'll alter the source path a little bit to keep it // separate of any third-party assets var favicon = path.search("favicon"); if (favicon > -1) assets = router.baseAssetPath; } else if (mediaType === 'jpg') mediaType = 'image/jpeg'; else mediaType = 'image/' + mediaType; break; case 'xml': case 'json': mediaType = 'application/' + mediaType; break; case 'text': mediaType = 'text/plain'; break; case 'zip': mediaType = 'application/octet-stream'; break; case 'gz': mediaType = 'application/x-gzip'; break; case 'bz2': mediaType = 'application/x-bzip2'; break; case 'mp3': mediaType = 'audio/mpeg'; break; case 'mp4': mediaType = 'video/mp4'; break; case 'ogg': mediaType = 'video/ogg'; break; case 'webm': mediaType = 'video/webm'; break; } // Send a success header: response.statusCode = 200; response.setHeader('Content-Type', mediaType); DEBUG(`Set 'Content-Type' to '${mediaType}'`); // If we find a reference to extensions in our path, modify the assets path: if (path.indexOf("extensions") > -1) { // Modify the asset path assets = router.baseAssetPath; DEBUG(`Set 'assets' [path] to '${router.baseAssetPath}'`); } // Read in the asset or resource fs.readFile(// __dirname is always the location of the file it is used assets + path, // in without a trailing slash 'binary', // <-- Yep, always binary (error, data) => { // If there is an error: if (error) { // Log it: console.error(`[ROUTER] Error reading asset or resource: ${assets}${path}.`); // If we are flagged to throw an exception: if (doThrow) throw error; // Otherwise redirect the user to our nifty 404 controller: controller.handle404(request, response); } else { // If there are no errors, respond with the asset/resource response.setHeader('Content-Length', data.length); DEBUG(`Set 'Content-Length' to '${data.length}'`); response.write(data, 'binary'); response.end(); } }); } else { // Attempt to load the requested controller //session.find( // request, // response, // ( req: any, res: any ) => { // DEBUG( `Call 'approach' for '${path}'` ); // controller.approach( req, res ); // } //); DEBUG(`Call 'approach' for '${path}'`); controller.approach(...await session.find(request, response)); } } ; } //# sourceMappingURL=router.mjs.map