/** * @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} 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} The loaded Module */ export async function loadModule(_mod: { EXTENSION: any }, container: Container, typeDefinition: string, dataUpdateCallback: () => void = () => { }): Promise { 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'); }); }); }