UNPKG

@kwaeri/node-kit

Version:

A simple, yet powerful, and fully-featured tooling for Node.js.

388 lines 18 kB
/** * SPDX-PackageName: kwaeri/node-kit * SPDX-PackageVersion: 0.4.2 * 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 fs from 'fs'; import { kdt } from '@kwaeri/developer-tools'; import debug from 'debug'; /* Framework components */ // We should allow these objects to be passed in via config, so that // end users can customize the session and router objects more appropriately // by extending them.. .in fact we'll get it to that point ourselves import { Server } from '@kwaeri/server'; import { Session, SessionSync } from '@kwaeri/session'; import { Router } from '@kwaeri/router'; import { Controller } from '@kwaeri/controller'; import { Model } from '@kwaeri/model'; import { Driver } from '@kwaeri/driver'; import { Renderer } from '@kwaeri/renderer'; import { Utility } from "@kwaeri/utility"; import { crypt_md5 } from "@kwaeri/md5-js"; // DEFINES const _ = new kdt(), md5js = new crypt_md5(), DEBUG = debug('nodekit'); // PARSE COMMAND LINE ARGUMENTS: const _arguments = (argList => { let args = {}, i, option, thisOption, currentOption; for (i = 0; i < argList.length; i++) { thisOption = argList[i].trim(); option = thisOption.replace(/^\-+/, ''); if (option === thisOption) { // Argument value: if (currentOption) args[currentOption] = option; currentOption = null; } else { currentOption = option; args[currentOption] = true; } } return args; })(process.argv); /** * TODO: Evaluate this. * * See https://morioh.com/p/fe2b92cc38f3 for a thorough explanation; * * What were doing is using symbols from JavaScript and unique symbols from * Typescript, in order to leverage symbols as we would enums, whilst gaining * the benefit of symbols (consider the contexts) * * This is the manual way to establish what we're trying to do: *const DEVELOPMENT_MODE: unique symbol = Symbol( "DEVELOPMENT MODE" ); *const PRODUCTION_MODE: unique symbol = Symbol( "PRODUCTION MODE" ); * *const Modes = { * DEVELOPMENT_MODE, * PRODUCTION_MODE0 *} as const; * * We had to const the enums, so that the enum values become the type of the * symbol, rather than the symbol values overarching type (symbol) * * And it requires a helper: * *type ValuesWithKeys<T, K extends keyof T> = T[K]; *type Values<T> = ValuesWithKeys<T, keyof T> * * We can then use the symbols themseleves as enums in a function that accepts * an argument of type "Values<typeof 'enum'>" * * * And the syntactical sugar way of doing it allows us to forego setting enums * as const, nor using the helper types - by leveraging symbol keys and values * in the enum declaration: * * *const ModeEnum = { * [DEVELOPMENT_MODE]: DEVELOPMENT_MODE, * [PRODUCTION_MODE]: PRODUCTION_MODE *}; * * *function getModeTypeWithModeKeys( mode: keyof typeof ModeEnum ) { * switch( mode ) { * case DEVELOPMENT_MODE: * break; * * case PRODUCTION_MODE: * break; * } *}; */ const Modes = { default: "default", development: "development", production: "production" }; // Let's establish the app mode via node's environment: const NK_ENV = (!process.env.NODE_ENV || process.env.NODE_ENV === ("" || null)) ? Modes.default : Modes[process.env.NODE_ENV]; export class NodeKitError extends Error { message; name; constructor(message = "", name = "") { super(message); https: //stackoverflow.com/a/48342359/2041005 Error.captureStackTrace(this, NodeKitError); this.message = message; this.name = name; // Take a copy of the original formatter: const originalFormatter = Error.prepareStackTrace; // Now provide a custom formatter of our own design: //Error.prepareStackTrace = ( error: Error, structuredStackTrace ) => { //} } } /** * This is the nodekit entry point. For cuteness, we've * decided to term it 'js', such that instantiation of * the platform will derive `nodekit.js()` */ export class nodekit { /** * @var configuration */ configuration; /** * @var admin */ admin = false; /** * @var _version */ _version = "0.3.0"; /** * Class constructor * * @param { NodeKit.App.Options } configuration * * @returns { nodekit } */ constructor(configuration) { if (configuration) { /* Set up dev or production environment, ensure we support older project architectures which had a configuration in the root directory with both dev/prod environment configs in the same config.js file. Currently, however, we use a app.<environment>.json style configuration implementation that is found in the conf directory at the project root and which is loaded prior to node-kit being invoked: */ this.configuration = configuration[NK_ENV] || configuration; this.configuration.debug = (process.env.hasOwnProperty('DEBUG')) ? true : false; // Set the environment to pass along the chain this.configuration.environment = NK_ENV; // We need to set the base path in modern platform configs // as they use a json file, and so __dirname wasn't able to // be used, instead we use '.' and fs.realpathSync in order // order to return the full path of the directory the run // command was issued to node for discerning the project root: this.configuration.app.base = (this.configuration.app.base === "." || this.configuration.app.base === "") ? fs.realpathSync('.') : this.configuration.app.base; DEBUG(`Checking for provided arguments`); // Prepare a default admin setting of `false`: this.admin = false; // See if any arguments were passed to the process invocation: if (_arguments && !_.empty(_arguments)) { // See if we are running the public or administrative application: if (_arguments['admin']) { DEBUG(`Administrative environment requested`); this.admin = true; this.configuration.appType = 'admin'; // Remove the non-administrative app paths from memory: // delete this.configuration.app; } else { DEBUG(`Non-administrative environment requested`); this.configuration.appType = 'app'; // Remove the administrative app paths from memory: //delete this.configuration.admin; } } else { DEBUG(`Non-administrative environment requested`); this.configuration.appType = 'app'; // Remove the administrative app paths from memory: //delete this.configuration.admin; } } else throw (new Error('[nodekit]: Error: NOAPPCFG: No configuration object was passed to the nodekit constructor.')); } /** * Listen starts a server in simple (non-platform mode) * * @param { number } port The port to listen on * @param { string } host The IP or HOSTNAME of the host to listen from * * @returns { void } */ listen(port, host) { // Forward to init(): this.init(undefined, port, host); } /** * Initializes the application platform * * @param { configuration } options * * @return { void } */ async init(options, port, host) { // Establish the app type right away: const appType = this.configuration.appType; DEBUG(`Preparing ${appType} configuration`); // Set the defaults for those who wish to use NodaKwaeri to its // fullest, and invoke this method supplying only the necessary paths let defaults = { session: 'nodekit', router: 'nodekit', controller: 'nodekit', driver: 'nodekit', connector: undefined, model: 'nodekit', renderer: 'nodekit', utilities: { hash_md5: 'nodekit', html: 'nodekit' }, ssr: false, xrm: false, layout: '' }; options = _.extend(options || {}, defaults); DEBUG(`Checking configuration for valid paths`); // Make sure the paths were provided, or __dirname in this // file will not point to the appropriate directories for the // framework if (this.configuration && (this.configuration.app || this.configuration.admin)) { const { app, admin } = this.configuration; if ((app && !app.controller_path) || (admin && !admin.controller_path)) throw (new Error('[nodekit]: Error: INVCTRLRPATH: Invalid controller path or controller path not set.')); if ((app && !app.model_path) || (admin && !admin.model_path)) throw (new Error('[nodekit]: Error: INVMODPATH: Invalid model path or model path not set.')); if ((app && !app.view_path) || (admin && !admin.view_path)) throw (new Error('[nodekit]: Error: INVVWPATH: Invalid view path or view path not set.')); if ((app && !app.asset_path) || (admin && !admin.asset_path)) throw (new Error('[nodekit]: Error: INVASSTPATH: Invalid asset path or asset path not set.')); if ((app && !app.asset_provider) || (admin && !admin.asset_provider)) throw (new Error('[nodekit]: Error: INVASSTPRVDR: Invalid asset provider path or asset provider path not set.')); if ((app && !app.extension_path) || (admin && !admin.extension_path)) throw (new Error('[nodekit]: Error: INVEXTPATH: Invalid extension path or extension path not set.')); } // If a custom session provider was specified, use it: if (_.type(this.configuration.session) === 'object' && !_.empty(this.configuration.session)) { DEBUG(`Loading '${_.get(this.configuration.session, 'type')}' session store from '${_.get(this.configuration.session, 'provider')}'`); // For whatever reason assigning this.configuration.session to options.session creates // a circular reference when using @kwaeri/developer-tools: //options.session = _.set( options, 'session', this.configuration.session ); // So we assign it directly:instead: options.session = this.configuration.session; // If the provider is nodekit -> ensure to use async sessions (synchronous sessions utilized for fallback // in-memory sessions and/or by synchronous providers only!): if (_.get(options.session, 'provider') === "nodekit") options.session = _.set(options.session, 'provider', Session); else // Otherwise if async is set to true use async sessions: if (_.get(options.session, 'async', false) === true) options.session = _.set(options.session, 'provider', Session); else // Otherwise use synchronous sessions: options.session = _.set(options.session, 'provider', SessionSync); } else { // If a custom session provider wasn't specified, deploy the synchronous nodekit // session provider with its built-in in-memory session store using default values: DEBUG(`Loading 'in-memory' session store from 'nodekit'`); options.session = _.set({}, 'provider', SessionSync); options.session = _.set(options.session, 'type', 'in-memory'); } // If a custom router provider wasn't specified, deploy the built in router provider with // default values if (options.router === 'nodekit') { DEBUG(`Loading nodekit:Router`); options.router = Router; } // If a custom database driver wasn't specified, deploy the built in database driver with // default values if (options.driver === 'nodekit') { DEBUG(`Loading nodekit:Driver`); options.driver = Driver; // Connector should be provided manually by the user via the app's // options //options.connector = MySQLDatabaseConnector; } // If a custom model provider wasn't specified, deploy the built in model provider with // default values if (options.model === 'nodekit') { DEBUG(`Loading nodekit:Model`); options.model = new Model({ driver: options.driver, connector: options.connector, database: this.configuration.database }); } // Prepare some variables for use even if some will not be leveraged: const { base, root, layout_path, theme, controller_path, view_path, extension_path } = this.configuration[appType]; // Prepare the purported extension Path const derivedExtensionPath = base + root + extension_path; // If the app is not a REST service, we'll need rendering for the app; We // denote this by flagging for XRM support this early on (don't fear - we // do not have an XRM without the XRM template - it's simply flagging the // needed components for leveraging themes and supporting the rendering - // or output buffering - of views): options.ssr = this.configuration.ssr || false; if (options.xrm || options.ssr) { DEBUG(`Enabled (X)RM/Rendering support`); // If a custom renderer wasn't provided, add renderer_tools to the configuration if (options.renderer === 'nodekit') { DEBUG(`Loading nodekit:Renderer`); options.renderer = Renderer; } // If custom rendering tools were not provided in the configuration, add them: if (options.utilities.hash_md5 === 'nodekit') { DEBUG(`Loading nodekit:md5js`); options.utilities.hash_md5 = md5js; } if (options.utilities.html === 'nodekit') { DEBUG(`Loading nodekit:Utility`); options.utilities.html = Utility; } /** * TODO: Evaluate if no longer needed?: * if( !options.hasOwnProperty( 'utilities' ) ) { * // Prepare a utilities object: * //options.utilities = {}; * * DEBUG( `Loading hashing utilities` ); * * options.utilities.hash_md5 = hash_md5; * * DEBUG( `Loading nodekit:Utility` ); * * options.utilities.html = Utility; * } */ // Now instantiate the renderer passing in the configuration, if this application is an xrm // we'll prepare the path to the theme directory. //options.layout = this.configuration[appType].base + this.configuration[appType].root + this.configuration[appType].layout_path + '/' + this.configuration[appType].theme; options.layout = base + root + layout_path + '/' + theme; DEBUG(`(X)RM layout path: ${options.layout}`); // Construct it's derived view paths //let derivedViewPath = this.configuration[appType].base + this.configuration[appType].root + this.configuration[appType].view_path; //let derivedExtensionPath = this.configuration[appType].base + this.configuration[appType].root + this.configuration[appType].extension_path; const derivedViewPath = base + root + view_path; options.renderer = new options.renderer(options.utilities, derivedViewPath, options.xrm, options.ssr, options.layout, derivedExtensionPath); } // If a custom controller wasn't provided, deploy the built in controller initialized with the supplied controller path if (options.controller === 'nodekit') { DEBUG(`Loading nodekit:Controller`); options.controller = new Controller({ //controllerPath: this.configuration[appType].base + this.configuration[appType].root + this.configuration[appType].controller_path, //extensionPath: this.configuration[appType].base + this.configuration[appType].root + this.configuration[appType].extension_path || false, controllerPath: base + root + controller_path, extensionPath: base + root + extension_path || false, model: options.model, renderer: options.renderer || false }); const layoutsInitialized = await options.controller.initLayouts(); DEBUG(`Loaded layouts [%s]`, layoutsInitialized); // We can also delete the model_provider variable now as we won't need it delete options.model; // If there is an active renderer set, remove it from memory: if (options.renderer) delete options.renderer; } DEBUG(`Compiling the configuration`); // Let's update the configuration to handle dependencies further along the chain let configuration = _.extend(this.configuration, options); DEBUG(`Loading nodekit:Server`); // Initialize our server with the updated configuration let server = new Server(configuration); DEBUG(`Initializing the application`); server.start(); } } //# sourceMappingURL=node-kit.mjs.map