UNPKG

alchemymvc

Version:
716 lines (583 loc) 16.8 kB
let fs = alchemy.use('fs'), libpath = alchemy.use('path'), LOADED = Symbol('LOADED'), _duplicateCheck = {}; /** * Require the given path. * The path follows an app structure. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} dir_path * @param {Object} options * * @return {boolean} */ Alchemy.setMethod(function useAppPath(dir_path, options) { if (!fs.existsSync(dir_path)) { return false; } if (!options) { options = {}; } if (options.plugin && options.weight == null) { options.weight = 15; } let context = options?.plugin || this; let server_directories = new Map(), helper_directories = new Map(), files = new Map(); let config_directory, asset_directory, view_directory, public_path, root_path; // Loop over all the entries in the app directory eachDirectoryEntryStats(dir_path, (file_path, name, stat) => { if (stat.isDirectory()) { let corrected_name, is_helper = false; if (name == 'helper' || name == 'element') { corrected_name = name; is_helper = true; } else if (name.startsWith('helper_')) { corrected_name = name.slice(7); is_helper = true; } else if (name == 'assets') { asset_directory = file_path; return; } else if (name == 'view') { view_directory = file_path; return; } else if (name == 'public') { public_path = file_path; return; } else if (name == 'root') { root_path = file_path; return; } else if (name == 'config') { config_directory = file_path; return; } else if (name == 'source') { return; } if (is_helper) { helper_directories.set(corrected_name, file_path); } else { server_directories.set(name, file_path); } } files.set(name, file_path); }); // Load the bootstrap file first if (files.get('bootstrap.js')) { if (!options.plugin) { context.useOnce(files.get('bootstrap.js')); } files.delete('bootstrap.js'); } if (config_directory) { context.usePath(config_directory, options); } if (options.helpers !== false) { if (helper_directories.get('error')) { this.useHelperPath(helper_directories.get('error')); helper_directories.delete('error'); } if (helper_directories.get('datasource')) { this.useHelperPath(helper_directories.get('datasource')); helper_directories.delete('datasource'); } if (helper_directories.get('field')) { this.useHelperPath(helper_directories.get('field')); helper_directories.delete('field'); } if (helper_directories.get('helper')) { this.useHelperPath(helper_directories.get('helper')); helper_directories.delete('helper'); } } // Load the server-side code next for (let [name, file_path] of server_directories) { context.usePath(file_path); } if (options.helpers !== false) { if (helper_directories.get('element')) { this.useHelperPath(helper_directories.get('element')); helper_directories.delete('element'); } if (helper_directories.get('model')) { this.useHelperPath(helper_directories.get('model')); helper_directories.delete('model'); } if (helper_directories.get('document')) { this.useHelperPath(helper_directories.get('document')); helper_directories.delete('document'); } if (helper_directories.get('controller')) { this.useHelperPath(helper_directories.get('controller')); helper_directories.delete('controller'); } if (helper_directories.get('component')) { this.useHelperPath(helper_directories.get('component')); helper_directories.delete('component'); } if (helper_directories.get('validator')) { this.useHelperPath(helper_directories.get('validator')); helper_directories.delete('validator'); } for (let [name, file_path] of helper_directories) { this.useHelperPath(file_path); } } if (view_directory && options.views !== false) { alchemy.addViewDirectory(view_directory, options.weight); } if (asset_directory) { // Add the main asset directory alchemy.addAssetDirectory(asset_directory, options.weight); if (options.scripts !== false) { alchemy.addScriptDirectory(libpath.resolve(asset_directory, 'scripts'), options.weight); } if (options.stylesheets !== false) { alchemy.addStylesheetDirectory(libpath.resolve(asset_directory, 'stylesheets'), options.weight); // Also add the public folder, so less files in there can also be compiled alchemy.addStylesheetDirectory(libpath.resolve(dir_path, 'public'), options.weight); } if (options.fonts !== false) { alchemy.addFontDirectory(libpath.resolve(asset_directory, 'fonts'), options.weight); } if (options.images !== false) { alchemy.addImageDirectory(libpath.resolve(asset_directory, 'images'), options.weight); } } if (public_path && options.public !== false) { alchemy.addPublicDirectory(public_path, options.weight); } if (root_path && options.root !== false) { alchemy.addRootDirectory(root_path, options.weight); } }); /** * Require the given helper path * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} dir_path * @param {Object} options * * @return {boolean} */ Alchemy.setMethod(function useHelperPath(dir_path, options) { let sub_directories = []; eachDirectoryEntryStats(dir_path, (file_path, name, stat) => { if (stat.isDirectory()) { sub_directories.push(file_path); return; } try { alchemy.hawkejs.load(file_path, { arguments : 'hawkejs' }); } catch (err) { alchemy.printLog('warning', ['Unable to add helper file ' + name + '\n' + String(err), err], {err: err, level: -2}); alchemy.printLog('warning', ['File was at', file_path], {err: err, level: -2}); } }); for (let dir of sub_directories) { this.useHelperPath(dir, options); } }); /** * Require all the files in the given path. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} dir_path * @param {Object} options * * @return {boolean} */ Alchemy.setMethod(function usePath(dir_path, options) { let recursive = options?.recursive !== false, sub_directories = []; let context = options?.plugin || this; eachDirectoryEntryStats(dir_path, (file_path, name, stat) => { if (stat.isDirectory()) { if (recursive) { sub_directories.push(file_path); } return; } context.useOnce(file_path); }); if (recursive && sub_directories.length) { for (let dir of sub_directories) { context.usePath(dir, options); } } }); /** * Iterate over a directory's content, but only * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} path * @param {Function} callback */ function eachDirectoryEntryStats(path, callback) { let extension, file_path, files = fs.readdirSync(path), stat; for (let file of files) { // Ignore all the hidden files if (file[0] == '.') { continue; } switch (file) { case 'node_modules': case 'test': case 'empty': case 'migrations': continue; } // Ignore manual files if (file.includes('.manual.')) { continue; } extension = libpath.extname(file).toLowerCase(); switch (extension) { case '.md': case '.json': continue; } file_path = libpath.resolve(path, file); stat = fs.lstatSync(file_path); callback(file_path, file, stat); } } /** * Default _usePath options * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.5.0 * @version 0.5.0 * * @type {Object} */ Alchemy.setProperty('default_use_path_options', { ignore : false, recursive : -1, _level : -1, // Load the bootstrap.js file first bootstrap : true, // Load the app_ file afterwards app : true }); /** * Add an asset directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addAssetDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('asset.directories').push(dirPath, weight); }); /** * Add a scripts directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addScriptDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('script.directories').push(dirPath, weight); }); /** * Add a stylesheets directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addStylesheetDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('stylesheet.directories').push(dirPath, weight); }); /** * Add a font directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.3 * @version 1.1.3 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addFontDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('font.directories').push(dirPath, weight); }); /** * Add an image directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addImageDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('images.directories').push(dirPath, weight); }); /** * Add a public directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addPublicDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('public.directories').push(dirPath, weight); }); /** * Add a root directory * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addRootDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.shared('root.directories').push(dirPath, weight); }); /** * Tell hawkejs to use this path to look for views. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.0.1 * @version 0.4.0 * * @param {string} dirPath The path to load * @param {number} weight The weighted importance [10] */ Alchemy.setMethod(function addViewDirectory(dirPath, weight) { if (typeof weight !== 'number') { weight = 10; } alchemy.hawkejs.addViewDirectory(dirPath, weight); }); /** * Prepare a plugin for use. * This immediately executes the plugin's bootstrap.js file, * but the loading of the app tree happens later. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.0.1 * @version 1.4.0 * * @param {string} name The name of the plugin (which is its path) * @param {Object} options Options to pass to the plugin * * @return {Alchemy.Plugin} */ Alchemy.setMethod(function usePlugin(name, options) { let full_name; // Strip of the "alchemy-" from the name, should it be given if (name.startsWith('alchemy-')) { full_name = name; name = name.slice(8); } else { // The "full" name starts with "alchemy-" full_name = 'alchemy-' + name; } // Make sure the plugin hasn't been loaded already if (alchemy.plugins[name] != null) { if (options) { log.warn('Tried to load plugin "' + name + '" with options twice!'); } return true; } if (options == null) { options = {}; } // Create the possible paths to this plugin let possible_paths = []; if (options.path_to_plugin) { possible_paths.push(options.path_to_plugin); } else { // Look for the "alchemy-" path first possible_paths.push(alchemy.pathResolve(PATH_ROOT, 'node_modules', full_name)); possible_paths.push(alchemy.pathResolve(PATH_ROOT, 'node_modules', name)); // It's also allowed to be inside the app/plugins folder possible_paths.push(alchemy.pathResolve(PATH_APP, 'plugins', full_name)); possible_paths.push(alchemy.pathResolve(PATH_APP, 'plugins', name)); } let path_to_plugin, is_dir = false; for (let path of possible_paths) { try { let plugin_stat = fs.lstatSync(path); is_dir = plugin_stat.isDirectory() || plugin_stat.isSymbolicLink(); path_to_plugin = path; break; } catch (err) { // Ignore } } if (!is_dir) { log.error('Could not find ' + JSON.stringify(name) + ' plugin directory'); return false; } let instance = new Classes.Alchemy.Plugin(name, path_to_plugin, options); // Set the given options alchemy.plugins[name] = instance; instance.doPreload(); return instance; }); /** * If a plugin hasn't been loaded yet, but it is required, die * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.0.1 * @version 1.4.0 * * @param {string|Array} names * @param {boolean} attempt_require */ Alchemy.setMethod(function requirePlugin(names, attempt_require) { var message, missing = '', name, temp, i; if (attempt_require == null) { attempt_require = true; } if (!Array.isArray(names)) { names = [names]; } for (i = 0; i < names.length; i++) { name = names[i]; if (typeof alchemy.plugins[name] === 'undefined') { if (attempt_require) { temp = alchemy.usePlugin(name); if (temp) { let plugin_stage = STAGES.getStage('load_app.plugins'); if (!plugin_stage || plugin_stage.started) { // If the plugin stage has already started, // manually start this plugin now alchemy.startPlugins(name); } continue; } } if (missing) { missing += ', '; } missing += name; } } if (missing) { message = 'These required plugin(s) are missing: ' + missing; die(message, {level: 2}); } }); /** * Load in a file only once * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.0.1 * @version 1.1.0 */ Alchemy.setMethod(function useOnce(dirPath, options) { if (typeof options == 'undefined') { options = {}; } dirPath = alchemy.pathResolve.apply(null, arguments); if (typeof _duplicateCheck[dirPath] === 'undefined') { // Mainly used for tidying up the unit tests if (process.env.NO_ALCHEMY_LOAD_WARNING == 1) { options.throwError = false; options.silent = true; } if (options.client == null) { options.client = false; } try { Blast.require(dirPath, options); _duplicateCheck[dirPath] = true; //log.verbose('Used file once: ' + dirPath, {level: 1}); } catch (err) { // Add the path to the file that failed to load, // this can be used when it's a syntax error // (It's hard to find the cause otherwise) err.file_path = dirPath; _duplicateCheck[dirPath] = false; if (options.throwError !== false) { if (options.throwError !== true) { let yellow = __Janeway.esc('103;91'); alchemy.printLog('error', [yellow + '========================='], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' Failed to load file: '], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' »', dirPath.split(libpath.sep).last()], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' In directory: '], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' »', dirPath.split(libpath.sep).slice(0, -1).join(libpath.sep)], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' With error: '], {err: err, level: -2}); alchemy.printLog('error', [yellow + ' »', err], {err: err, level: -2}); alchemy.printLog('error', [yellow + '========================='], {err: err, level: -2}); } throw err; } if (!options.silent) { // @todo: "Failed to use file once..." message doesn't get displayed log.error('Failed to use file once: ' + dirPath, {level: 5, err: err, extra: true}); } } } else { //log.verbose('File not loaded, already used once: ' + dirPath, {level: 1}); } });