529 lines
15 KiB
TypeScript
529 lines
15 KiB
TypeScript
|
/**
|
||
|
* @author Martin Karkowski
|
||
|
* @email m.karkowski@zema.de
|
||
|
* @create date 2018-10-30 16:17:06
|
||
|
* @modify date 2020-09-09 15:58:44
|
||
|
* @desc [description]
|
||
|
*/
|
||
|
|
||
|
|
||
|
/** Clear the Screen */
|
||
|
declare const process: any;
|
||
|
|
||
|
import { exists, mkdir, readFile, writeFile } from "fs";
|
||
|
import { Container } from "inversify";
|
||
|
import { type } from "os";
|
||
|
/** Import Stuff for File-Handeling */
|
||
|
import { join, resolve } from "path";
|
||
|
import { promisify } from "util";
|
||
|
import { createPath, listFiles } from "../../../lib/helpers/fileMethods";
|
||
|
import { objectToMap, rgetattr } from "../../../lib/helpers/objectMethods";
|
||
|
import { IJsonSchema } from "../../../types/IJSONSchema";
|
||
|
import { DEFAULT_CONFIG_FILE, DEFAULT_SEARCHPATH } from "../../mod-Assembly-Builder/constants/constants";
|
||
|
import { Builder } from "../../mod-Assembly-Builder/src/Container-Builder.FileLoader";
|
||
|
import { SystemLogger } from "../../mod-Logger/src/Unique.Logger";
|
||
|
import { generateJSONSchemas } from "./converter.helper";
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
export const DEFAULT_STARTUP_CONFIG_FILE = "startup-configuration.json";
|
||
|
export const DEFAULT_PATH_TO_STARTUP_CONFIG_FILE =
|
||
|
"./config/startup-configuration.json";
|
||
|
|
||
|
|
||
|
const _logger = SystemLogger.logger.getLogger("module-loader");
|
||
|
|
||
|
export const FOLDER_SEPERATOR = type()
|
||
|
.toLowerCase()
|
||
|
.includes("windows")
|
||
|
? "\\"
|
||
|
: "/";
|
||
|
|
||
|
const _readFile = promisify(readFile);
|
||
|
const _writeFile = promisify(writeFile);
|
||
|
const _exists = promisify(exists);
|
||
|
const _mkdir = promisify(mkdir);
|
||
|
|
||
|
/**
|
||
|
* Function which will load multiple Modules
|
||
|
*
|
||
|
* @export
|
||
|
* @param {Container} container The Inversify-Container, which is used to build the Module
|
||
|
* @param {Array<{ path: string; typeDefinition?: string }>} config An Array containing the Element, which should be loaded
|
||
|
* @returns {Promise<des.MODULEBASE>} The loaded Module
|
||
|
*/
|
||
|
export async function asyncLoadModules(
|
||
|
container: Container,
|
||
|
config: Array<{ path: string; typeDefinition?: string }>,
|
||
|
dataUpdateCallback: () => void
|
||
|
): Promise<{ [index: string]: des.IMODULE }> {
|
||
|
/** Define the Return Type */
|
||
|
const _ret: { [index: string]: des.IMODULE } = {};
|
||
|
|
||
|
/** Scan for Modules and Add them */
|
||
|
for (const _element of config) {
|
||
|
/** Dynamically Load the Modules. */
|
||
|
const _file = _element.path;
|
||
|
|
||
|
try {
|
||
|
/** Import the Module */
|
||
|
const _mod = await import(resolve(_file));
|
||
|
_ret[_file] = await loadModule(_mod, container, _element.typeDefinition, dataUpdateCallback)
|
||
|
} catch (e) {
|
||
|
_logger.error(e, "could not import the Module:" + _file);
|
||
|
}
|
||
|
|
||
|
_logger.debug("loaded: " + _file);
|
||
|
}
|
||
|
|
||
|
_logger.info("All Modules have been loaded");
|
||
|
return _ret;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Function which will load an Extension
|
||
|
*
|
||
|
* @example // Load the Desired Module
|
||
|
* import * as AUTO_IBN_MODULE from '../modules/mod-Basic-Modules/src/auto-ibn-Module';
|
||
|
*
|
||
|
* const _module = loadModule(AUTO_IBN_MODULE, inversifyContainer, './schema/Auto-Ibn-Module.json');
|
||
|
*
|
||
|
* @export
|
||
|
* @param {{EXTENSION: any}} _mod The Module Object. (Normaly this defaults to the imported Module)
|
||
|
* @param {Container} container The Inversify-Container, which is used to build the Module
|
||
|
* @param {string} typeDefinition The Path, where Type-Definitions will be found
|
||
|
* @param {() => void} [dataUpdateCallback=() => {}] Callback for the Module on Updates.
|
||
|
* @returns {Promise<des.MODULEBASE>} The loaded Module
|
||
|
*/
|
||
|
export async function loadModule(_mod: { EXTENSION: any }, container: Container, typeDefinition: string, dataUpdateCallback: () => void = () => { }): Promise<des.MODULEBASE> {
|
||
|
if (_mod.EXTENSION) {
|
||
|
/** Load the Module */
|
||
|
const _extension: des.MODULEBASE = new _mod.EXTENSION(dataUpdateCallback);
|
||
|
|
||
|
/** Init the Module */
|
||
|
_extension.init(container);
|
||
|
|
||
|
/** Perform the onload-method */
|
||
|
_extension.onLoad();
|
||
|
|
||
|
/** If requried Load the Schema-JSON (only if it exsits) */
|
||
|
if (typeDefinition) {
|
||
|
if (await _exists(typeDefinition)) {
|
||
|
const _schema = JSON.parse(
|
||
|
await _readFile(typeDefinition, { encoding: "utf-8" })
|
||
|
);
|
||
|
|
||
|
for (const _funcName in _extension.functions) {
|
||
|
/** Adapt the Schema => */
|
||
|
const _func = _extension.functions[_funcName];
|
||
|
|
||
|
/** Extract the Schema for Inputs */
|
||
|
for (const t of ["input", "output"]) {
|
||
|
const _type = t as "input" | "output";
|
||
|
if (_func[_type].path) {
|
||
|
const _extractedSchema: IJsonSchema = rgetattr(
|
||
|
_schema,
|
||
|
_func[_type].path as string
|
||
|
);
|
||
|
|
||
|
if (_extractedSchema) {
|
||
|
const _schemaAsMap = objectToMap(_extractedSchema)
|
||
|
const _refs = Array.from(_schemaAsMap.entries()).filter(([key, value]) => key.endsWith('$ref')).map(([key, value]) => value as string);
|
||
|
|
||
|
if (_refs.length > 0) {
|
||
|
// Extract the Required Definitions
|
||
|
const _definitions = {};
|
||
|
_logger.warn('Requiring References for ' + _funcName, _refs);
|
||
|
console.log(_refs);
|
||
|
|
||
|
_func[_type].schema = _extractedSchema
|
||
|
} else {
|
||
|
// Assign the Extracted Schema to the Definition
|
||
|
_func[_type].schema = _extractedSchema
|
||
|
}
|
||
|
} else {
|
||
|
_logger.warn('Could not extract Schema for ' + _extension.name + '.' + _funcName + '. Using nulltype', _func[_type].path,);
|
||
|
_func[_type].schema = {
|
||
|
type: 'null'
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return _extension;
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Function to determine all Modules in the given Directory.
|
||
|
* After finding those, they will be loaded. Before this function
|
||
|
* can be called, make shure all assemblies are added to the
|
||
|
* Inversify-Container, otherwise this method will throw errors.
|
||
|
*
|
||
|
* @export
|
||
|
* @param {Container} : Promise<{ [index: string]: des.IMODULE }>
|
||
|
* @param {string} [path=DEFAULT_SEARCHPATH] A default Search Path is provided (./dist/modules)
|
||
|
* @returns {Promise<{ [index: string]: des.IMODULE }>} An object, using the Filename as Key an providing the loaded Module
|
||
|
*/
|
||
|
export async function asyncLoadAllModules(
|
||
|
container: Container,
|
||
|
dataUpdateCallback: () => void,
|
||
|
path: string = DEFAULT_SEARCHPATH
|
||
|
): Promise<{ [index: string]: des.IMODULE }> {
|
||
|
const _files = await listFiles(path, "-Module.js");
|
||
|
const _modules = new Array<{ path: string }>();
|
||
|
|
||
|
/** Adapt the Modules-Array to match the Required Shape */
|
||
|
for (const _file of _files) {
|
||
|
_modules.push({ path: _file });
|
||
|
}
|
||
|
|
||
|
return await asyncLoadModules(container, _modules, dataUpdateCallback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper-Function which will load all Modules. But instead of running async, it
|
||
|
* perfroms in a Callback-Manner
|
||
|
*
|
||
|
* @export
|
||
|
* @param {Array<{ path: string }>} config
|
||
|
* @param {Container} container
|
||
|
* @param {(error: any, data: { [index: string]: des.IMODULE }) => void} callback
|
||
|
*/
|
||
|
export function loadModules(
|
||
|
config: Array<{ path: string }>,
|
||
|
container: Container,
|
||
|
dataUpdateCallback: () => void,
|
||
|
callback: (error: any, data: { [index: string]: des.IMODULE }) => void
|
||
|
): void {
|
||
|
/** Perform the Function with the given callbacks */
|
||
|
asyncLoadModules(container, config, dataUpdateCallback).then(
|
||
|
(result) => {
|
||
|
callback(undefined, result);
|
||
|
},
|
||
|
(error) => {
|
||
|
callback(error, {});
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Helper Function, which will create a Default Module Config.
|
||
|
*
|
||
|
* @export
|
||
|
* @param {string} [_pathToFile=""]
|
||
|
* @param {boolean} [writeFile=true]
|
||
|
* @returns The Created Module-Description Files
|
||
|
*/
|
||
|
export async function writeDefaultModuleConfig(
|
||
|
_pathToFile: string = "",
|
||
|
writeFile = true
|
||
|
) {
|
||
|
const _extensions = await asyncLoadAllModules(
|
||
|
Builder.instance.container,
|
||
|
() => { },
|
||
|
DEFAULT_SEARCHPATH
|
||
|
);
|
||
|
|
||
|
/** Create the File Name */
|
||
|
const _dirName = join(process.cwd(), "config");
|
||
|
let _fileName = join(await createPath(_dirName), DEFAULT_CONFIG_FILE);
|
||
|
|
||
|
/** If a Path is given use this Path instead*/
|
||
|
if (_pathToFile) {
|
||
|
_fileName = _pathToFile;
|
||
|
}
|
||
|
|
||
|
/** Create a Config Element */
|
||
|
let _config = {};
|
||
|
|
||
|
/** Check if a Config exists => If So Extend the given config */
|
||
|
if (await _exists(_fileName)) {
|
||
|
_config = JSON.parse(await _readFile(_fileName, { encoding: "utf-8" }));
|
||
|
}
|
||
|
|
||
|
_config["modules"] = [];
|
||
|
|
||
|
/** Extract all files: */
|
||
|
const _files = await listFiles("./", "-Module-Types.ts");
|
||
|
|
||
|
/** Define the Schema-Dir */
|
||
|
const _schemaDir = join(resolve(process.cwd()), "schemas");
|
||
|
|
||
|
if (!(await _exists(_schemaDir))) {
|
||
|
await _mkdir(_schemaDir);
|
||
|
}
|
||
|
|
||
|
await generateJSONSchemas(
|
||
|
Object.getOwnPropertyNames(_extensions),
|
||
|
_files,
|
||
|
_schemaDir
|
||
|
);
|
||
|
|
||
|
/** Store the Config */
|
||
|
for (const _mod in _extensions) {
|
||
|
const _name = _mod.split(FOLDER_SEPERATOR)[
|
||
|
_mod.split(FOLDER_SEPERATOR).length - 1
|
||
|
];
|
||
|
let _typeName = _name.slice(0, _name.length - ".js".length) + "-Types.ts";
|
||
|
let _typeFile: string | undefined = undefined;
|
||
|
|
||
|
/** Search for the Type-Defintion File */
|
||
|
for (const _foundTypeFile of _files) {
|
||
|
if (_foundTypeFile.endsWith(_typeName)) {
|
||
|
_typeFile = _foundTypeFile;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Adapt the Name */
|
||
|
_typeName = _typeName.split("-Module-Types.ts")[0];
|
||
|
|
||
|
/** Only if a type File is provided => Get the JSON-Schema */
|
||
|
if (!_typeFile) {
|
||
|
_logger.warn("No Module-Types found for " + _typeName);
|
||
|
|
||
|
for (const _func in _extensions[_mod].functions) {
|
||
|
const _funcDescriptor = _extensions[_mod].functions[_func];
|
||
|
|
||
|
const _schema = { definitions: {} };
|
||
|
|
||
|
if (
|
||
|
_funcDescriptor.input.schema !== undefined ||
|
||
|
_funcDescriptor.output.schema !== undefined
|
||
|
) {
|
||
|
Object.assign(_schema.definitions, _funcDescriptor.input.schema);
|
||
|
Object.assign(_schema.definitions, _funcDescriptor.output.schema);
|
||
|
|
||
|
await _writeFile(
|
||
|
join(_schemaDir, _typeName + ".json"),
|
||
|
JSON.stringify(_schema, undefined, 4)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_config["modules"].push({
|
||
|
name: _extensions[_mod].name,
|
||
|
description: _extensions[_mod].description,
|
||
|
path: _mod,
|
||
|
typeDefinition: join(_schemaDir, _typeName + ".json")
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (writeFile) {
|
||
|
/** Write the new Configuration */
|
||
|
await _writeFile(_fileName, JSON.stringify(_config, undefined, 4));
|
||
|
_logger.warn("Writing new Assembly-Config in :" + _fileName);
|
||
|
}
|
||
|
|
||
|
/** Return the created Filename */
|
||
|
return _config["modules"];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function which will extract all Auto-Startup-Functions of a Module.
|
||
|
*
|
||
|
* @export
|
||
|
* @param {Container} container The Container containing all Assemblies
|
||
|
* @param {string} [_pathToFile=''] The Path to a File. Default = ./config/Startup-Config.json
|
||
|
* @returns
|
||
|
*/
|
||
|
export async function writeDefaultStartupConfig(
|
||
|
container: Container,
|
||
|
_pathToFile = DEFAULT_PATH_TO_STARTUP_CONFIG_FILE,
|
||
|
_assemblyFile = DEFAULT_CONFIG_FILE,
|
||
|
writeFile = true
|
||
|
) {
|
||
|
/** Load all Modules */
|
||
|
const _extensions = await asyncLoadModules(
|
||
|
container,
|
||
|
JSON.parse((await _readFile(_assemblyFile)).toString("utf-8")).modules,
|
||
|
() => { }
|
||
|
);
|
||
|
const _autostart = new Array<{ funcname: string; params: any }>();
|
||
|
|
||
|
/** Extract the autostart option on each Element */
|
||
|
for (const _name of Object.getOwnPropertyNames(_extensions)) {
|
||
|
const _extension = _extensions[_name];
|
||
|
_autostart.push(..._extension.autoStart);
|
||
|
}
|
||
|
|
||
|
/** Create the File Name */
|
||
|
const _dirName = join(resolve(process.cwd()), "config");
|
||
|
let _fileName = join(await createPath(_dirName), DEFAULT_STARTUP_CONFIG_FILE);
|
||
|
|
||
|
/** If a Path is given use this Path instead*/
|
||
|
if (_pathToFile) {
|
||
|
_fileName = _pathToFile;
|
||
|
}
|
||
|
|
||
|
if (writeFile) {
|
||
|
/** Write the Results */
|
||
|
await _writeFile(_fileName, JSON.stringify(_autostart, undefined, 4));
|
||
|
_logger.warn("Writing new Autostart-Config in :" + _fileName);
|
||
|
}
|
||
|
|
||
|
return _autostart;
|
||
|
}
|
||
|
|
||
|
async function writeUserConfig() {
|
||
|
process.stdout.write("\x1B[2J\x1B[0f");
|
||
|
|
||
|
// Import Inquirer and the Fuzzy-Path Module
|
||
|
const inquirer = require("inquirer");
|
||
|
inquirer.registerPrompt("path", require("inquirer-fuzzy-path"));
|
||
|
|
||
|
const nameInput = {
|
||
|
type: "input",
|
||
|
name: "fileName",
|
||
|
message: "Enter the Configuration File Name",
|
||
|
default: DEFAULT_CONFIG_FILE,
|
||
|
validate: function (value: string) {
|
||
|
if (value.endsWith(".json")) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return "Please add the Enter a Valid Filename (including .json)";
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let result = await inquirer.prompt([nameInput]);
|
||
|
|
||
|
/** Create the File Name */
|
||
|
const _dirName = join(process.cwd(), "config");
|
||
|
let _fileName = join(await createPath(_dirName), result.fileName);
|
||
|
|
||
|
/** List all Files which are used to describe an assembly */
|
||
|
const _mods = await writeDefaultModuleConfig(_fileName, false);
|
||
|
process.stdout.write("\x1B[2J\x1B[0f");
|
||
|
|
||
|
const files = {
|
||
|
type: "checkbox",
|
||
|
name: "elements",
|
||
|
message: "Select/Deselect the Assemblies",
|
||
|
choices: _mods.map((mod) => {
|
||
|
return {
|
||
|
name: mod.name,
|
||
|
checked: true
|
||
|
};
|
||
|
})
|
||
|
};
|
||
|
|
||
|
result = await inquirer.prompt([files]);
|
||
|
|
||
|
/** Create a Config Element */
|
||
|
let _config = {};
|
||
|
|
||
|
/** Check if a Config exists => If So Extend the given config */
|
||
|
if (await _exists(_fileName)) {
|
||
|
_config = JSON.parse(await _readFile(_fileName, { encoding: "utf-8" }));
|
||
|
}
|
||
|
|
||
|
_config["modules"] = _mods.filter((element) => {
|
||
|
if (result.elements.includes(element.name)) {
|
||
|
return element;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
process.stdout.write("\x1B[2J\x1B[0f");
|
||
|
|
||
|
/** Write the new Configuration */
|
||
|
await _writeFile(_fileName, JSON.stringify(_config, undefined, 4));
|
||
|
_logger.warn("Writing new Assembly-Config in :" + _fileName);
|
||
|
|
||
|
process.stdout.write("\x1B[2J\x1B[0f");
|
||
|
|
||
|
result = await inquirer.prompt([
|
||
|
{
|
||
|
type: "confirm",
|
||
|
name: "writeStartup",
|
||
|
message: "Do you want to create a Startup-File?",
|
||
|
default: false
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
if (result.writeStartup) {
|
||
|
result = await inquirer.prompt([
|
||
|
{
|
||
|
type: "input",
|
||
|
name: "fileName",
|
||
|
message: "Enter the File Name to store the auto-startup",
|
||
|
default: DEFAULT_STARTUP_CONFIG_FILE,
|
||
|
validate: function (value: string) {
|
||
|
if (value.endsWith(".json")) {
|
||
|
return true;
|
||
|
}
|
||
|
return "Please add the Enter a Valid Filename (including .json)";
|
||
|
}
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
const _configFile = _fileName;
|
||
|
_fileName = join(await createPath(_dirName), result.fileName);
|
||
|
|
||
|
const _startup = await writeDefaultStartupConfig(
|
||
|
Builder.instance.container,
|
||
|
_fileName,
|
||
|
_configFile,
|
||
|
false
|
||
|
);
|
||
|
|
||
|
process.stdout.write("\x1B[2J\x1B[0f");
|
||
|
|
||
|
result = await inquirer.prompt([
|
||
|
{
|
||
|
type: "checkbox",
|
||
|
name: "elements",
|
||
|
message: "Select/Deselect the Assemblies",
|
||
|
choices: _startup.map((action) => {
|
||
|
return {
|
||
|
name: JSON.stringify(action),
|
||
|
short: action.funcname,
|
||
|
value: action.funcname,
|
||
|
checked: true
|
||
|
};
|
||
|
})
|
||
|
}
|
||
|
]);
|
||
|
|
||
|
/** Write the Results */
|
||
|
await _writeFile(
|
||
|
_fileName,
|
||
|
JSON.stringify(
|
||
|
_startup.filter((element) =>
|
||
|
result.elements.includes(element.funcname)
|
||
|
),
|
||
|
undefined,
|
||
|
4
|
||
|
)
|
||
|
);
|
||
|
_logger.warn("Writing new Autostart-Config in :" + _fileName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
if (typeof require != "undefined" && require.main == module) {
|
||
|
process.stdout.write("\x1B[2J\x1B[0f");
|
||
|
|
||
|
SystemLogger.logger.level = 'error';
|
||
|
SystemLogger.logger.lock('dontChange');
|
||
|
|
||
|
|
||
|
Builder.load();
|
||
|
Builder.on("loaded", () => {
|
||
|
writeUserConfig().catch(e => {
|
||
|
_logger.fatal(e, 'Failed creating config');
|
||
|
});
|
||
|
});
|
||
|
}
|