/* global defaultModules, vendor */ /* MagicMirror² * Module and File loaders. * * By Michael Teeuw https://michaelteeuw.nl * MIT Licensed. */ const Loader = (function () { /* Create helper variables */ const loadedModuleFiles = []; const loadedFiles = []; const moduleObjects = []; /* Private Methods */ /** * Loops through all modules and requests start for every module. */ const startModules = async function () { const modulePromises = []; for (const module of moduleObjects) { try { modulePromises.push(module.start()); } catch (error) { Log.error(`Error when starting node_helper for module ${module.name}:`); Log.error(error); } } const results = await Promise.allSettled(modulePromises); // Log errors that happened during async node_helper startup results.forEach((result) => { if (result.status === "rejected") { Log.error(result.reason); } }); // Notify core of loaded modules. MM.modulesStarted(moduleObjects); // Starting modules also hides any modules that have requested to be initially hidden for (const thisModule of moduleObjects) { if (thisModule.data.hiddenOnStartup) { Log.info("Initially hiding " + thisModule.name); thisModule.hide(); } } }; /** * Retrieve list of all modules. * * @returns {object[]} module data as configured in config */ const getAllModules = function () { return config.modules; }; /** * Generate array with module information including module paths. * * @returns {object[]} Module information. */ const getModuleData = function () { const modules = getAllModules(); const moduleFiles = []; modules.forEach(function (moduleData, index) { const module = moduleData.module; const elements = module.split("/"); const moduleName = elements[elements.length - 1]; let moduleFolder = config.paths.modules + "/" + module; if (defaultModules.indexOf(moduleName) !== -1) { moduleFolder = config.paths.modules + "/default/" + module; } if (moduleData.disabled === true) { return; } moduleFiles.push({ index: index, identifier: "module_" + index + "_" + module, name: moduleName, path: moduleFolder + "/", file: moduleName + ".js", position: moduleData.position, hiddenOnStartup: moduleData.hiddenOnStartup, header: moduleData.header, configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false, config: moduleData.config, classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module }); }); return moduleFiles; }; /** * Load modules via ajax request and create module objects. * * @param {object} module Information about the module we want to load. * @returns {Promise} resolved when module is loaded */ const loadModule = async function (module) { const url = module.path + module.file; /** * @returns {Promise} */ const afterLoad = async function () { const moduleObject = Module.create(module.name); if (moduleObject) { await bootstrapModule(module, moduleObject); } }; if (loadedModuleFiles.indexOf(url) !== -1) { await afterLoad(); } else { await loadFile(url); loadedModuleFiles.push(url); await afterLoad(); } }; /** * Bootstrap modules by setting the module data and loading the scripts & styles. * * @param {object} module Information about the module we want to load. * @param {Module} mObj Modules instance. */ const bootstrapModule = async function (module, mObj) { Log.info("Bootstrapping module: " + module.name); mObj.setData(module); await mObj.loadScripts(); Log.log("Scripts loaded for: " + module.name); await mObj.loadStyles(); Log.log("Styles loaded for: " + module.name); await mObj.loadTranslations(); Log.log("Translations loaded for: " + module.name); moduleObjects.push(mObj); }; /** * Load a script or stylesheet by adding it to the dom. * * @param {string} fileName Path of the file we want to load. * @returns {Promise} resolved when the file is loaded */ const loadFile = async function (fileName) { const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); let script, stylesheet; switch (extension.toLowerCase()) { case "js": return new Promise((resolve) => { Log.log("Load script: " + fileName); script = document.createElement("script"); script.type = "text/javascript"; script.src = fileName; script.onload = function () { resolve(); }; script.onerror = function () { Log.error("Error on loading script:", fileName); resolve(); }; document.getElementsByTagName("body")[0].appendChild(script); }); case "css": return new Promise((resolve) => { Log.log("Load stylesheet: " + fileName); stylesheet = document.createElement("link"); stylesheet.rel = "stylesheet"; stylesheet.type = "text/css"; stylesheet.href = fileName; stylesheet.onload = function () { resolve(); }; stylesheet.onerror = function () { Log.error("Error on loading stylesheet:", fileName); resolve(); }; document.getElementsByTagName("head")[0].appendChild(stylesheet); }); } }; /* Public Methods */ return { /** * Load all modules as defined in the config. */ loadModules: async function () { let moduleData = getModuleData(); /** * @returns {Promise} when all modules are loaded */ const loadNextModule = async function () { if (moduleData.length > 0) { const nextModule = moduleData[0]; await loadModule(nextModule); moduleData = moduleData.slice(1); await loadNextModule(); } else { // All modules loaded. Load custom.css // This is done after all the modules so we can // overwrite all the defined styles. await loadFile(config.customCss); // custom.css loaded. Start all modules. await startModules(); } }; await loadNextModule(); }, /** * Load a file (script or stylesheet). * Prevent double loading and search for files in the vendor folder. * * @param {string} fileName Path of the file we want to load. * @param {Module} module The module that calls the loadFile function. * @returns {Promise} resolved when the file is loaded */ loadFileForModule: async function (fileName, module) { if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) { Log.log("File already loaded: " + fileName); return; } if (fileName.indexOf("http://") === 0 || fileName.indexOf("https://") === 0 || fileName.indexOf("/") !== -1) { // This is an absolute or relative path. // Load it and then return. loadedFiles.push(fileName.toLowerCase()); return loadFile(fileName); } if (vendor[fileName] !== undefined) { // This file is available in the vendor folder. // Load it from this vendor folder. loadedFiles.push(fileName.toLowerCase()); return loadFile(config.paths.vendor + "/" + vendor[fileName]); } // File not loaded yet. // Load it based on the module path. loadedFiles.push(fileName.toLowerCase()); return loadFile(module.file(fileName)); } }; })();