@kwaeri/node-kit
Version:
A simple, yet powerful, and fully-featured tooling for Node.js.
388 lines • 18 kB
JavaScript
/**
* 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
*/
;
// 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