├── .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 |
--------------------------------------------------------------------------------