├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bigtitle.txt ├── package.json └── src ├── index.js ├── listeners ├── discord │ ├── GuildCountListener.js │ ├── LoggingListener.js │ └── MessageListener.js └── redis │ └── RedisListener.js ├── loaders ├── ListenerLoader.js └── index.js ├── structures ├── Listener.js ├── Loader.js ├── base │ ├── Switchblade.js │ └── index.js └── index.js └── utils ├── FileUtils.js └── index.js /.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN= 2 | PREFIX=s! 3 | SHARD_ID=0 # DO NOT PUT THIS HIGHER THAN 0 WHEN DEALING WITH ONLY 1 SHARD; THANK YOU PEDRO -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@master 12 | - name: Standard 13 | uses: goto-bus-stop/standard-action@v1 14 | with: 15 | annotate: true 16 | env: 17 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 18 | build: 19 | name: Docker 20 | runs-on: ubuntu-latest 21 | needs: lint 22 | if: github.event_name == 'push' 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@master 26 | - name: Build and publish to registry 27 | uses: docker/build-push-action@v1 28 | with: 29 | username: ${{ secrets.DOCKER_USERNAME }} 30 | password: ${{ secrets.DOCKER_PASSWORD }} 31 | repository: switchbladebot/switchblade-legacy 32 | tag_with_ref: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # package-lock.json 64 | package-lock.json 65 | 66 | .vscode 67 | 68 | .idea/ 69 | 70 | src/locales/* 71 | !src/locales/en-US/ 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | COPY . . 9 | 10 | CMD [ "node", "src/index.js" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, SwitchbladeBot 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by the SwitchbladeBot. 14 | 4. Neither the name of the SwitchbladeBot nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY SWITCHBLADEBOT "AS IS" AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL SWITCHBLADEBOT BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠⚠ OUTDATED PROJECT 2 | 3 |
4 |
5 |
6 | 7 | ## Why are you rewriting the bot? 8 | Switchblade is now over two years old. It's codebase has grown a lot over the years and served us well, but it's time to change. After a lot of discussion, the team has decided to rewrite the bot into [Eris](https://github.com/abalabahaha/eris), a more robust library for Discord bots. This decision was made due to the number of servers we are currently serving quickly getting closer to Discord's obligatory sharding point (2500 servers) and the inability of our current codebase to work well with shards. 9 | 10 | ## When is the rewrite going to be complete? 11 | It will probably take some time for us to go over the whole thing and get everything working, but hold tight: the future is bright. As we rewrite each and every feature, we will also refactor our whole infrastructure, containerizing everything and splitting our code into multiple independent "services". This will allow for greater flexibility and less downtime when we create and deploy new features, resulting in a better experience. That said, we do not currently have a deadline, as we want to make sure everything works well and the changes go as smoothly as possible for the end user. 12 | 13 | ## What happens to the main repository when the rewrite is complete? 14 | The new codebase will be moved from this repository to the main one, replacing the current `master` branch but keeping it's history avaliable. 15 | -------------------------------------------------------------------------------- /bigtitle.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ _ _ _ _ 2 | / ____| (_| | | | | | | | | | 3 | | (_____ ___| |_ ___| |__ | |__ | | __ _ __| | ___ 4 | \___ \ \ /\ / | | __/ __| '_ \| '_ \| |/ _` |/ _` |/ _ \ 5 | ____) \ V V /| | || (__| | | | |_) | | (_| | (_| | __/ 6 | |_____/ \_/\_/ |_|\__\___|_| |_|_.__/|_|\__,_|\__,_|\___| 7 | 8 | {UNICODE}34m{UNICODE}1mNEXT{UNICODE}0m 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "switchblade-next", 3 | "version": "1.0.0", 4 | "description": "The next version of Switchblade", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "start-dev": "nodemon --exec node -r dotenv/config src" 9 | }, 10 | "engines": { 11 | "node": "12.16.1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+ssh://git@github.com/SwitchbladeBot/switchblade-next.git" 16 | }, 17 | "keywords": [ 18 | "discord" 19 | ], 20 | "author": "Switchblade Team", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/SwitchbladeBot/switchblade-next/issues" 24 | }, 25 | "homepage": "https://github.com/SwitchbladeBot/switchblade-next#readme", 26 | "dependencies": { 27 | "@sentry/node": "^5.15.4", 28 | "chalk": "^3.0.0", 29 | "dd-trace": "^0.20.3", 30 | "eris": "^0.11.2", 31 | "node-fetch": "^2.6.0", 32 | "tedis": "^0.1.12", 33 | "winston": "^3.2.1" 34 | }, 35 | "devDependencies": { 36 | "dotenv": "^8.2.0", 37 | "nodemon": "^2.0.2", 38 | "standard": "^14.3.3" 39 | }, 40 | "optionalDependencies": {} 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/node') 2 | Sentry.init({ dsn: process.env.SENTRY_DSN }) 3 | 4 | require('dd-trace').init({ 5 | service: 'switchblade-next', 6 | logInjection: true 7 | }) 8 | 9 | const { Switchblade } = require('./structures/base') 10 | 11 | const CLUSTER_ID = process.env.INDEX_CLUSTER_ID_FROM_ONE ? parseInt(process.env.CLUSTER_ID) - 1 : parseInt(process.env.CLUSTER_ID) 12 | const firstShardID = CLUSTER_ID * parseInt(process.env.SHARDS_PER_CLUSTER) 13 | const lastShardID = ((CLUSTER_ID + 1) * parseInt(process.env.SHARDS_PER_CLUSTER)) - 1 14 | const maxShards = parseInt(process.env.SHARDS_PER_CLUSTER) * parseInt(process.env.MAX_CLUSTERS) 15 | 16 | const client = new Switchblade(process.env.DISCORD_TOKEN, { firstShardID, lastShardID, maxShards }, { 17 | prefix: process.env.PREFIX || 'n!' 18 | }) 19 | 20 | client.logger.info(`Starting. First Shard: ${firstShardID}; Last Shard: ${lastShardID}`, { label: `Cluster ${process.env.CLUSTER_ID}` }) 21 | -------------------------------------------------------------------------------- /src/listeners/discord/GuildCountListener.js: -------------------------------------------------------------------------------- 1 | const { Listener } = require('../../structures') 2 | const fetch = require('node-fetch') 3 | 4 | const BASE_URL = process.env.BOT_LIST_POSTER_URL || 'http://bot-list-poster' 5 | 6 | module.exports = class GuildCountListener extends Listener { 7 | constructor (client) { 8 | super({ 9 | listenerClient: 'discord', 10 | events: [ 11 | 'guildCreate', 12 | 'guildDelete' 13 | ] 14 | }, client) 15 | } 16 | 17 | onGuildCreate (guild) { this.updateGuildCount(guild) } 18 | onGuildDelete (guild) { this.updateGuildCount(guild) } 19 | 20 | updateGuildCount (guild) { 21 | this.client.logger.info('Sending updated guild count to statistics manager', { label: `Shard ${guild.shard.id}` }) 22 | fetch(`${BASE_URL}/shards`, { 23 | method: 'post', 24 | body: JSON.stringify([ 25 | { 26 | id: guild.shard.id, 27 | guildCount: this.client.guilds.filter(g => g.shard.id === guild.shard.id).length 28 | } 29 | ]), 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | Authorization: `Bearer ${process.env.BOT_LIST_POSTER_SECRET}` 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/listeners/discord/LoggingListener.js: -------------------------------------------------------------------------------- 1 | const { Listener } = require('../../structures') 2 | 3 | module.exports = class LoggingListener extends Listener { 4 | constructor (client) { 5 | super({ 6 | listenerClient: 'discord', 7 | events: [ 8 | 'error', 9 | 'disconnect', 10 | 'debug', 11 | 'shardDisconnect', 12 | 'connect', 13 | 'rawWS' 14 | ] 15 | }, client) 16 | } 17 | 18 | onError (error, shardId) { 19 | this.client.logger.error(`Shard ${shardId} encountered an error. ${error.stack}`, { label: `Shard ${shardId}`, shardId, stack: error.stack }) 20 | } 21 | 22 | onDebug (message, shardId) { 23 | this.client.logger.debug(message, { label: `Shard ${shardId || '?'}` }) 24 | } 25 | 26 | onRawWS (packet, shardId) { 27 | this.client.logger.silly(packet, { label: `Shard ${shardId || '?'}`, packet }) 28 | } 29 | 30 | onDisconnect () { 31 | this.client.logger.error('All shards disconnected', { label: 'Connection' }) 32 | } 33 | 34 | onShardDisconnect (error, shardId) { 35 | if (error) { 36 | this.client.logger.error(`Disconnected. ${error.stack || error.message}`, { label: `Shard ${shardId}`, shardId }) 37 | } else { 38 | this.client.logger.warn('Disconnected.', { label: `Shard ${shardId}`, shardId }) 39 | } 40 | } 41 | 42 | onConnect (shardId) { 43 | this.client.logger.info('Connected to the gateway', { label: `Shard ${shardId}`, shardId }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/listeners/discord/MessageListener.js: -------------------------------------------------------------------------------- 1 | const { Listener } = require('../../structures') 2 | 3 | module.exports = class MessageListener extends Listener { 4 | constructor (client) { 5 | super({ 6 | listenerClient: 'discord', 7 | events: [ 8 | 'messageCreate' 9 | ] 10 | }, client) 11 | } 12 | 13 | async onMessageCreate (message) { 14 | if (message.author.bot) return 15 | this.client.logger.info(message.content, { label: 'Message' }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/listeners/redis/RedisListener.js: -------------------------------------------------------------------------------- 1 | const { Listener } = require('../../structures') 2 | 3 | module.exports = class RedisListener extends Listener { 4 | constructor (client) { 5 | super({ 6 | listenerClient: 'redis', 7 | events: [ 8 | 'connect', 9 | 'timeout', 10 | 'close', 11 | 'error' 12 | ] 13 | }, client) 14 | } 15 | 16 | onConnect () { 17 | this.client.logger.info('Connected', { label: 'Redis' }) 18 | } 19 | 20 | onTimeout () { 21 | this.client.logger.info('Timeout', { label: 'Redis' }) 22 | } 23 | 24 | onError (error) { 25 | this.client.logger.error(error, { label: 'Redis' }) 26 | } 27 | 28 | onClose (hadError) { 29 | if (hadError) { 30 | this.client.logger.error('Connection closed with errors', { label: 'Redis' }) 31 | } else { 32 | this.client.logger.warn('Connection closed without errors', { label: 'Redis' }) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/loaders/ListenerLoader.js: -------------------------------------------------------------------------------- 1 | const { Loader, Listener } = require('../structures') 2 | 3 | module.exports = class ListenerLoader extends Loader { 4 | constructor (client) { 5 | super({ critical: true, name: 'Listeners' }, client) 6 | this.listeners = [] 7 | } 8 | 9 | load () { 10 | return this.loadFiles('src/listeners', true) 11 | } 12 | 13 | loadFile (NewListener) { 14 | const listener = new NewListener(this.client) 15 | if (!(listener instanceof Listener)) throw new Error(`Failed to load ${NewListener.name}: not a Listener`) 16 | 17 | // Load all events from functions listed on listener.events 18 | const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1) 19 | listener.events.forEach(event => { 20 | if (listener.listenerClient === 'discord') { 21 | this.client.on(event, (...e) => listener['on' + capitalize(event)](...e)) 22 | } else { 23 | this.client[listener.listenerClient].on(event, (...e) => listener['on' + capitalize(event)](...e)) 24 | } 25 | }) 26 | 27 | return true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/loaders/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ListenerLoader: require('./ListenerLoader.js') 3 | } 4 | -------------------------------------------------------------------------------- /src/structures/Listener.js: -------------------------------------------------------------------------------- 1 | const { createOptionHandler } = require('../utils') 2 | 3 | module.exports = class Listener { 4 | constructor (options, client) { 5 | options = createOptionHandler('Listener', options) 6 | 7 | this.events = options.optional('events', []) 8 | this.listenerClient = options.optional('listenerClient', 'discord') 9 | 10 | this.client = client 11 | } 12 | 13 | listen () { 14 | return null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/structures/Loader.js: -------------------------------------------------------------------------------- 1 | const { createOptionHandler, FileUtils } = require('../utils') 2 | 3 | module.exports = class Loader { 4 | /** 5 | * @param {Object} opts 6 | * @param {boolean} [opts.critical] 7 | * @param {Switchblade} client 8 | */ 9 | constructor (options, client) { 10 | options = createOptionHandler('Loader', options) 11 | 12 | this.critical = options.optional('critical', false) 13 | 14 | this.name = options.optional('name', this.constructor.name) 15 | 16 | this.client = client 17 | } 18 | 19 | async preLoad () { 20 | try { 21 | const success = await this.load() 22 | if (!success) throw new Error('Unhandled error') 23 | return success 24 | } catch (e) { 25 | this.client.logger.error(`Failed to load ${this.name}`, { label: 'Loader' }) 26 | return false 27 | } 28 | } 29 | 30 | async loadFiles (path, recursive = false) { 31 | if (!path || typeof path !== 'string') throw new TypeError(`The 'path' argument on '${this.constructor.name}.loadFiles()' must be a string. Received ${typeof path} instead.`) 32 | let success = 0 33 | let fails = 0 34 | const errorFunction = e => { 35 | this.client.logger.error(e.stack || e, { label: 'Loader' }) 36 | fails++ 37 | } 38 | const successFunction = (file, fileName) => { 39 | try { 40 | if (this.loadFile(file)) { 41 | this.client.logger.debug(`Loaded ${fileName}`, { label: this.name }) 42 | success++ 43 | } else { 44 | this.client.logger.debug(`Failed to load ${fileName}`, { label: this.name }) 45 | throw new Error(`'${this.constructor.name}.loadFile()' returned an unhandled error.`) 46 | } 47 | } catch (e) { 48 | errorFunction(e) 49 | } 50 | } 51 | await FileUtils.requireDirectory(path, successFunction, errorFunction, recursive).then(() => { 52 | if (fails) this.client.logger.warn(`${success} types of ${this.name} loaded, ${fails} failed.`, { label: 'Loader' }) 53 | else this.client.logger.info(`All ${success} types of ${this.name} loaded without errors.`, { label: 'Loader' }) 54 | }) 55 | return true 56 | } 57 | 58 | async load () { 59 | return true 60 | } 61 | 62 | loadFile (file) { 63 | throw new Error(`The ${this.name} loader has not implemented the loadFile() function`) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/structures/base/Switchblade.js: -------------------------------------------------------------------------------- 1 | const { CommandClient } = require('eris') 2 | const Loaders = require('../../loaders') 3 | const { readFileSync } = require('fs') 4 | const winston = require('winston') 5 | const { Tedis } = require('tedis') 6 | 7 | module.exports = class Switchblade extends CommandClient { 8 | constructor (token, options, commandOptions) { 9 | super(token, options, commandOptions) 10 | this.token = token 11 | this.initializeWinston() 12 | this.start() 13 | } 14 | 15 | start () { 16 | if (process.env.NODE_ENV !== 'production') console.log(readFileSync('bigtitle.txt', 'utf8').toString().replace(/{UNICODE}/g, '\u001b[')) 17 | if (!this.token) { 18 | this.logger.error('Discord token not specified. Exiting.') 19 | process.exit(1) 20 | } 21 | this.logger.info('Starting Switchblade...', { label: 'Switchblade' }) 22 | 23 | this.initializeRedis() 24 | this.initializeLoaders() 25 | 26 | this.connect() 27 | } 28 | 29 | initializeWinston () { 30 | this.logger = winston.createLogger() 31 | 32 | if (process.env.NODE_ENV === 'production') { 33 | this.logger.add(new winston.transports.Console({ level: process.env.LOGGING_LEVEL || 'silly' })) 34 | } else { 35 | this.logger.add(new winston.transports.Console({ 36 | format: winston.format.combine( 37 | winston.format.colorize(), 38 | winston.format.timestamp(), 39 | winston.format.printf( 40 | info => `${info.timestamp} ${info.level}${info.label ? ` [${info.label || ''}]` : ''}: ${info.message}` 41 | ) 42 | ), 43 | level: process.env.LOGGING_LEVEL || 'silly' 44 | })) 45 | } 46 | } 47 | 48 | initializeRedis () { 49 | // TODO: Find a way to capture initialization events 50 | // Currently, the RedisListener is only loaded after Tedis tries to connect, which 51 | // leads to it not capturing the first connection events. 52 | this.logger.info('Initializing Redis', { label: 'Redis' }) 53 | this.redis = new Tedis({ 54 | host: process.env.REDIS_SERVICE_HOST || process.env.REDIS_HOST || 'redis', 55 | port: process.env.REDIS_SERVICE_PORT || process.env.REDIS_PORT || 6379, 56 | password: process.env.REDIS_PASSWORD 57 | }) 58 | } 59 | 60 | async initializeLoaders () { 61 | for (const file in Loaders) { 62 | const loader = new Loaders[file](this) 63 | let success = true 64 | try { 65 | success = await loader.preLoad() 66 | } catch (error) { 67 | this.logger.error(error.stack, { label: 'Loaders', stack: error.stack }) 68 | } finally { 69 | if (!success && loader.critical) process.exit(1) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/structures/base/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Switchblade: require('./Switchblade.js') 3 | } 4 | -------------------------------------------------------------------------------- /src/structures/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Loader 3 | Loader: require('./Loader.js'), 4 | Listener: require('./Listener.js') 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/FileUtils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { promisify } = require('util') 4 | 5 | module.exports = class FileUtils { 6 | static async requireDirectory (dirPath, success, error, recursive = true) { 7 | const files = await FileUtils.readdir(dirPath) 8 | const filesObject = {} 9 | return Promise.all(files.map(async fileName => { 10 | const fullPath = path.resolve(dirPath, fileName) 11 | if (fileName.match(/\.(js|json)$/)) { 12 | try { 13 | const required = require(fullPath) 14 | if (success) await success(required, fileName) 15 | filesObject[fileName] = required 16 | return required 17 | } catch (e) { 18 | error(e, fileName) 19 | } 20 | } else if (recursive) { 21 | const isDirectory = await FileUtils.stat(fullPath).then(f => f.isDirectory()) 22 | if (isDirectory) { 23 | return FileUtils.requireDirectory(fullPath, success, error) 24 | } 25 | } 26 | })).then(() => filesObject).catch(console.error) 27 | } 28 | } 29 | 30 | module.exports.readdir = promisify(fs.readdir) 31 | module.exports.readFile = promisify(fs.readFile) 32 | module.exports.stat = promisify(fs.stat) 33 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * @param {string} structure 4 | * @param {Object} structureOptions 5 | * @param {Object} [options] 6 | * @param {boolean} [options.optionalOptions] 7 | * @returns {Object} 8 | */ 9 | createOptionHandler (structureName, structureOptions, options = {}) { 10 | if (!options.optionalOptions && typeof options === 'undefined') { 11 | throw new Error(`The options of structure "${structureName}" is required.`) 12 | } 13 | 14 | return ({ 15 | structureOptions, 16 | optional (name, defaultValue = null) { 17 | const value = structureOptions[name] 18 | 19 | return typeof value === 'undefined' 20 | ? defaultValue 21 | : value 22 | }, 23 | 24 | required (name) { 25 | const value = structureOptions[name] 26 | 27 | if (typeof value === 'undefined') { 28 | throw new Error(`The option "${name}" of structure "${structureName}" is required.`) 29 | } 30 | 31 | return value 32 | } 33 | }) 34 | } 35 | } 36 | 37 | module.exports.FileUtils = require('./FileUtils.js') 38 | --------------------------------------------------------------------------------