structures/loader.js

const { info } = require("./Logger");
const Command = require("./Command");
const { Collection } = require("discord.js");
const path = require("path");
/** Handles the loading of commands */
class CommandLoader {
  /** @param {ModzClient} [client] - Client to use  */
  constructor(client) {
    /**
     * The client the loader is serving
     * @name CommandLoader#Client
     * @type {ModzClient}
     * @readonly
     */
    Object.defineProperty(this, "client", { value: client });

    /**
     * The loaded commands
     * @type {Collection<string, Command>}
     */
    this.commands = new Collection();

    /**
     * The loaded aliases
     * @type {Collection<string, Command>}
     */
    this.aliases = new Collection();

    /**
     * The path to the commands
     * @type {?string}
     */
    this.commandsPath = null;

    /**
     * The path to the commands
     * @type {string[]}
     */
    this.defaultCommands = [];
  }
  /**
   * Loads a single command
   * @param {Command|Function} command - Either a Command instance, or a constructor for one
   * @return {CommandLoader}
   * @see {@link CommandLoader#loadCommands}
   */
  loadCommand(command) {
    return this.loadCommands([command]);
  }
  /**
   * Loads multiple commands
   * @param {Command[]|Function[]} commands - An array of Command instances, or constructors
   * @return {CommandLoader}
   */
  loadCommands(commands) {
    if (!Array.isArray(commands)) throw new TypeError("Commands must be an Array.");
    for (let command of commands) {
      if (typeof command === "function") command = new command(this.client); // eslint-disable-line new-cap

      // Verify that it's an actual command
      if (!(command instanceof Command)) {
        this.client.emit("warn", `Attempting to register an invalid command object: ${command}; skipping.`);
        continue;
      }

      // Make sure there aren't any conflicts
      if (this.commands.some(cmd => cmd.name === command.name || cmd.aliases.includes(command.name))) {
        throw new Error(`A command with the name/alias "${command.name}" is already registered.`);
      }
      for (const alias of command.aliases) {
        if (this.commands.some(cmd => cmd.name === alias || cmd.aliases.includes(alias))) {
          throw new Error(`A command with the name/alias "${alias}" is already registered.`);
        }
      }

      // Add the command
      this.commands.set(command.name, command);
      info(`[DISCORD] Loading command ${command.name}...`);
      command.aliases.forEach(alias => { // eslint-disable-line max-nested-callbacks
        this.aliases.set(alias, command.name);
      });
      /**
       * Emitted when a command is loaded
       * @event ModzClient#commandLoad
       * @param {Command} command - Command that was loaded
       */
      this.client.emit("commandLoad", command);
    }
    return this;
  }
  /**
   * Loads all commands in a directory. The files must export a Command class constructor or instance.
   * @param {string|RequireAllOptions} options - The path to the directory, or a require-all options object
   * @return {CommandLoader}
   */
  loadCommandsIn(options) {
    const obj = require("require-all")(options);
    const commands = [];
    for (const group of Object.values(obj)) {
      for (let command of Object.values(group)) {
        if (typeof command.default === "function") command = command.default;
        commands.push(command);
      }
    }
    if (typeof options === "string" && !this.commandsPath) this.commandsPath = options;
    return this.loadCommands(commands);
  }
  /**
   * Loads the default commands
   * @param {Object} [options] - The object to specify which commands not to register
   * @param {boolean} [options.eval_=true] - Whether or not to load the built-in `eval` command
   * @param {boolean} [options.exec=true] - Whether or not to load the built-in `exec` command
   * @param {boolean} [options.help=true] - Whether or not to load the built-in `help` command
   * @param {boolean} [options.ping=true] - Whether or not to load the built-in `ping` command
   * @param {boolean} [options.stats=true] - Whether or not to load the built-in `stats` command
   * @return {CommandLoader}
   */
  loadDefaultCommands({ eval_ = true, exec = true, help = true, ping = true, stats = true, reload = true } = {}) {
    if (eval_) this.loadCommand(require("../commands/util/eval")); this.defaultCommands.push("eval");
    if (exec) this.loadCommand(require("../commands/util/exec")); this.defaultCommands.push("exec");
    if (help) this.loadCommand(require("../commands/util/help")); this.defaultCommands.push("help");
    if (ping) this.loadCommand(require("../commands/util/ping")); this.defaultCommands.push("ping");
    if (stats) this.loadCommand(require("../commands/util/stats")); this.defaultCommands.push("stats");
    if (reload) this.loadCommand(require("../commands/util/reload")); this.defaultCommands.push("reload");

    return this;
  }
  /**
   * Resolves a command file path from a command's group ID and memberName
   * @param {string} group - The command's group
   * @param {string} name - Name of the command
   * @return {string} Fully-resolved path to the corresponding command file
   */
  resolveCommandPath(group, name) {
    return path.join(this.commandsPath, group, `${name}.js`);
  }
  reloadCommand(command, oldCommand) {
    if (typeof command === "function") command = new command(this.client); // eslint-disable-line new-cap
    if (command.name !== oldCommand.name) throw new Error("Command name cannot change.");
    if (command.group !== oldCommand.group) throw new Error("Command group cannot change.");
    this.commands.set(command.name, command);
    /**
     * Emitted when a command is reregistered
     * @event ModzClient#commandReload
     * @param {Command} newCommand - New command
     * @param {Command} oldCommand - Old command
     */
    this.client.emit("commandReload", command, oldCommand);
    this.client.emit("debug", `Reloaded command ${command.name}.`);
  }
  /**
   * Finds all commands that match the search string
   * @param {string} [searchString] - The string to search for
   * @param {boolean} [exact=false] - Whether the search should be exact
   * @return {Command[]} All commands that are found
   */
  findCommands(searchString = null, exact = false) {
    if (!searchString) return this.commands;

    // Find all matches
    const lcSearch = searchString.toLowerCase();
    const matchedCommands = this.commands.filterArray(
      exact ? commandFilterExact(lcSearch) : commandFilterInexact(lcSearch)
    );
    if (exact) return matchedCommands;

    // See if there's an exact match
    for (const command of matchedCommands) {
      if (command.name === lcSearch || (command.aliases && command.aliases.some(ali => ali === lcSearch))) {
        return [command];
      }
    }

    return matchedCommands;
  }
}

function commandFilterExact(search) {
  return cmd => cmd.name === search
  || (cmd.aliases && cmd.aliases.some(ali => ali === search))
  || `${cmd.name}` === search;
}

function commandFilterInexact(search) {
  return cmd => cmd.name.includes(search)
  || `${cmd.name}` === search
  || (cmd.aliases && cmd.aliases.some(ali => ali.includes(search)));
}

module.exports = CommandLoader;