mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat(create-discord-bot): Add prompts, command handler and deployment script (#9570)
* feat(create-discord-bot): Add prompts, command handler and deployment script * fix: revert package template name * chore: make requested changes * chore: make requested changes * fix: remove uneeded listeners * fix: use `0` for eslint * fix: remaining requested changes * chore: requested changes * fix: remove redundant call
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/create-discord-bot/*'",
|
||||
"release": "cliff-jumper"
|
||||
},
|
||||
"bin": "./dist/create-discord-bot.mjs",
|
||||
"bin": "./dist/index.mjs",
|
||||
"directories": {
|
||||
"lib": "src"
|
||||
},
|
||||
@@ -47,12 +47,14 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.2.0",
|
||||
"commander": "^10.0.1",
|
||||
"prompts": "^2.4.2",
|
||||
"validate-npm-package-name": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@favware/cliff-jumper": "^2.0.0",
|
||||
"@microsoft/api-extractor": "^7.35.0",
|
||||
"@types/node": "16.18.32",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@types/validate-npm-package-name": "^4.0.0",
|
||||
"@vitest/coverage-c8": "^0.31.1",
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
@@ -1,70 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// eslint-disable-next-line n/shebang
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { cp, stat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { URL } from 'node:url';
|
||||
import chalk from 'chalk';
|
||||
import { program } from 'commander';
|
||||
import validateProjectName from 'validate-npm-package-name';
|
||||
import { install, resolvePackageManager } from './helpers/packageManager.js';
|
||||
import { GUIDE_URL } from './util/constants.js';
|
||||
|
||||
program
|
||||
.description('Create a basic discord.js bot.')
|
||||
.option('--typescript', 'Whether to use the TypeScript template.')
|
||||
.argument('<directory>', 'The directory where this will be created.')
|
||||
.parse();
|
||||
|
||||
const { typescript } = program.opts();
|
||||
const [directory] = program.args;
|
||||
|
||||
if (!directory) {
|
||||
console.error(chalk.red('Please specify the project directory.'));
|
||||
process.exit(1);
|
||||
interface Options {
|
||||
directory: string;
|
||||
javascript?: boolean;
|
||||
typescript?: boolean;
|
||||
}
|
||||
|
||||
const root = path.resolve(directory);
|
||||
const directoryName = path.basename(root);
|
||||
|
||||
if (existsSync(root) && readdirSync(root).length > 0) {
|
||||
console.error(chalk.red(`The directory ${chalk.yellow(`"${directoryName}"`)} is not empty.`));
|
||||
console.error(chalk.red(`Please specify an empty directory.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// We'll use the directory name as the project name. Check npm name validity.
|
||||
const validationResult = validateProjectName(directoryName);
|
||||
|
||||
if (!validationResult.validForNewPackages) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Cannot create a project named ${chalk.yellow(`"${directoryName}"`)} due to npm naming restrictions.\n\nErrors:`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const error of [...(validationResult.errors ?? []), ...(validationResult.warnings ?? [])]) {
|
||||
console.error(chalk.red(`- ${error}`));
|
||||
export async function createDiscordBot({ typescript, javascript, directory }: Options) {
|
||||
if (!directory) {
|
||||
console.error(chalk.red('Please specify the project directory.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.error(chalk.red('\nSee https://docs.npmjs.com/cli/configuring-npm/package-json for more details.'));
|
||||
process.exit(1);
|
||||
const root = path.resolve(directory);
|
||||
const directoryName = path.basename(root);
|
||||
|
||||
const directoryStats = await stat(root).catch(async (error) => {
|
||||
// Create a new directory if the specified one does not exist.
|
||||
if (error.code === 'ENOENT') {
|
||||
await mkdir(root, { recursive: true });
|
||||
return stat(root);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
// If a directory exists and it's not empty, throw an error.
|
||||
if (directoryStats.isDirectory() && (await readdir(root)).length > 0) {
|
||||
console.error(chalk.red(`The directory ${chalk.yellow(`"${directoryName}"`)} is not empty.`));
|
||||
console.error(chalk.red(`Please specify an empty directory.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// We'll use the directory name as the project name. Check npm name validity.
|
||||
const validationResult = validateProjectName(directoryName);
|
||||
|
||||
if (!validationResult.validForNewPackages) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Cannot create a project named ${chalk.yellow(
|
||||
`"${directoryName}"`,
|
||||
)} due to npm naming restrictions.\n\nErrors:`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const error of [...(validationResult.errors ?? []), ...(validationResult.warnings ?? [])]) {
|
||||
console.error(chalk.red(`- ${error}`));
|
||||
}
|
||||
|
||||
console.error(chalk.red('\nSee https://docs.npmjs.com/cli/configuring-npm/package-json for more details.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Creating ${directoryName} in ${chalk.green(root)}.`);
|
||||
await cp(new URL(`../template/${typescript ? 'TypeScript' : 'JavaScript'}`, import.meta.url), root, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
process.chdir(root);
|
||||
|
||||
const newPackageJSON = await readFile('./package.json', { encoding: 'utf8' }).then((str) =>
|
||||
str.replace('[REPLACE-NAME]', directoryName),
|
||||
);
|
||||
await writeFile('./package.json', newPackageJSON);
|
||||
|
||||
const packageManager = resolvePackageManager();
|
||||
install(packageManager);
|
||||
console.log(chalk.green('All done! Be sure to read through the discord.js guide for help on your journey.'));
|
||||
console.log(`Link: ${chalk.cyan(GUIDE_URL)}`);
|
||||
}
|
||||
|
||||
if (!existsSync(root)) {
|
||||
mkdirSync(root, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Creating ${directoryName} in ${chalk.green(root)}.`);
|
||||
cpSync(new URL(`../template/${typescript ? 'TypeScript' : 'JavaScript'}`, import.meta.url), root, { recursive: true });
|
||||
|
||||
process.chdir(root);
|
||||
|
||||
const newPackageJSON = readFileSync('./package.json', { encoding: 'utf8' }).replace('[REPLACE-NAME]', directoryName);
|
||||
writeFileSync('./package.json', newPackageJSON);
|
||||
|
||||
const packageManager = resolvePackageManager();
|
||||
install(packageManager);
|
||||
console.log(chalk.green('All done! Be sure to read through the discord.js guide for help on your journey.'));
|
||||
console.log(`Link: ${chalk.cyan(GUIDE_URL)}`);
|
||||
|
||||
39
packages/create-discord-bot/src/index.ts
Normal file
39
packages/create-discord-bot/src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { program } from 'commander';
|
||||
import prompts from 'prompts';
|
||||
import { createDiscordBot } from './create-discord-bot.js';
|
||||
|
||||
program
|
||||
.description('Create a basic discord.js bot.')
|
||||
.option('--directory', 'The directory where this will be created.')
|
||||
.option('--typescript', 'Whether to use the TypeScript template.')
|
||||
.option('--javascript', 'Whether to use the JavaScript template.')
|
||||
.parse();
|
||||
|
||||
let { typescript, javascript, directory } = program.opts();
|
||||
|
||||
if (!directory) {
|
||||
directory = (
|
||||
await prompts({
|
||||
type: 'text',
|
||||
name: 'directory',
|
||||
initial: 'my-bot',
|
||||
message: 'What is the name of the directory you want to create this project in?',
|
||||
})
|
||||
).directory;
|
||||
}
|
||||
|
||||
if (typescript === undefined && javascript === undefined) {
|
||||
const { useTypescript } = await prompts({
|
||||
type: 'toggle',
|
||||
name: 'useTypescript',
|
||||
message: 'Do you want to use TypeScript?',
|
||||
initial: true,
|
||||
active: 'Yes',
|
||||
inactive: 'No',
|
||||
});
|
||||
|
||||
typescript = useTypescript;
|
||||
javascript = !useTypescript;
|
||||
}
|
||||
|
||||
await createDiscordBot({ typescript, javascript, directory });
|
||||
@@ -1 +1,2 @@
|
||||
DISCORD_TOKEN=
|
||||
APPLICATION_ID=
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["neon/common", "neon/node", "neon/prettier"]
|
||||
"extends": ["neon/common", "neon/node", "neon/prettier"],
|
||||
"rules": {
|
||||
"jsdoc/valid-types": 0,
|
||||
"jsdoc/check-tag-names": 0,
|
||||
"jsdoc/no-undefined-types": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
"scripts": {
|
||||
"lint": "prettier --check . && eslint src --ext .js,.cjs --format=pretty",
|
||||
"format": "prettier --write . && eslint src --ext .js,.cjs --fix --format=pretty",
|
||||
"start": "node --require dotenv/config src/index.js"
|
||||
"start": "node --require dotenv/config src/index.js",
|
||||
"deploy": "node --require dotenv/config src/util/deploy.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^0.6.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.0.3"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Defines the structure of a command.
|
||||
*
|
||||
* @typedef {object} Command
|
||||
* @property {import('discord.js').RESTPostAPIApplicationCommandsJSONBody} data The data for the command
|
||||
* @property {(interaction: import('discord.js').CommandInteraction) => Promise<void> | void} execute The function to execute when the command is called
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the predicate to check if an object is a valid Command type.
|
||||
*
|
||||
* @type {import('../util/loaders.js').StructurePredicate<Command>}
|
||||
* @returns {structure is Command}
|
||||
*/
|
||||
export const predicate = (structure) =>
|
||||
Boolean(structure) &&
|
||||
typeof structure === 'object' &&
|
||||
'data' in structure &&
|
||||
'execute' in structure &&
|
||||
typeof structure.data === 'object' &&
|
||||
typeof structure.execute === 'function';
|
||||
@@ -0,0 +1,10 @@
|
||||
/** @type {import('./index.js').Command} */
|
||||
export default {
|
||||
data: {
|
||||
name: 'ping',
|
||||
description: 'Ping!',
|
||||
},
|
||||
async execute(interaction) {
|
||||
await interaction.reply('Pong!');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*
|
||||
* @template {keyof import('discord.js').ClientEvents} [T=keyof import('discord.js').ClientEvents]
|
||||
* @typedef {object} Event
|
||||
* @property {(...parameters: import('discord.js').ClientEvents[T]) => Promise<void> | void} execute The function to execute the command
|
||||
* @property {T} name The name of the event to listen to
|
||||
* @property {boolean} [once] Whether or not the event should only be listened to once
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the predicate to check if an object is a valid Event type.
|
||||
*
|
||||
* @type {import('../util/loaders').StructurePredicate<Event>}
|
||||
* @returns {structure is Event}
|
||||
*/
|
||||
export const predicate = (structure) =>
|
||||
Boolean(structure) &&
|
||||
typeof structure === 'object' &&
|
||||
'name' in structure &&
|
||||
'execute' in structure &&
|
||||
typeof structure.name === 'string' &&
|
||||
typeof structure.execute === 'function';
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Events } from 'discord.js';
|
||||
|
||||
/** @type {import('./index.js').Event<Events.ClientReady>} */
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { loadCommands, loadEvents } from './util/loaders.js';
|
||||
import { registerEvents } from './util/registerEvents.js';
|
||||
|
||||
// Initialize the client
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
const eventsPath = new URL('events/', import.meta.url);
|
||||
const eventFiles = await readdir(eventsPath).then((files) => files.filter((file) => file.endsWith('.js')));
|
||||
|
||||
for (const file of eventFiles) {
|
||||
const event = (await import(new URL(file, eventsPath).toString())).default;
|
||||
client[event.once ? 'once' : 'on'](event.name, async (...args) => event.execute(...args));
|
||||
}
|
||||
// Load the events and commands
|
||||
const events = await loadEvents(new URL('events/', import.meta.url));
|
||||
const commands = await loadCommands(new URL('commands/', import.meta.url));
|
||||
|
||||
// Register the event handlers
|
||||
registerEvents(commands, events, client);
|
||||
|
||||
// Login to the client
|
||||
void client.login();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import process from 'node:process';
|
||||
import { URL } from 'node:url';
|
||||
import { API } from '@discordjs/core/http-only';
|
||||
import { REST } from 'discord.js';
|
||||
import { loadCommands } from './loaders.js';
|
||||
|
||||
const commands = await loadCommands(new URL('commands/', import.meta.url));
|
||||
const commandData = [...commands.values()].map((command) => command.data);
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
|
||||
const api = new API(rest);
|
||||
|
||||
const result = await api.applicationCommands.bulkOverwriteGlobalCommands(process.env.APPLICATION_ID, commandData);
|
||||
|
||||
console.log(`Successfully registered ${result.length} commands.`);
|
||||
@@ -0,0 +1,83 @@
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import { predicate as commandPredicate } from '../commands/index.js';
|
||||
import { predicate as eventPredicate } from '../events/index.js';
|
||||
|
||||
/**
|
||||
* A predicate to check if the structure is valid.
|
||||
*
|
||||
* @template T
|
||||
* @typedef {(structure: unknown) => structure is T} StructurePredicate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Loads all the structures in the provided directory.
|
||||
*
|
||||
* @template T
|
||||
* @param {import('node:fs').PathLike} dir - The directory to load the structures from
|
||||
* @param {StructurePredicate<T>} predicate - The predicate to check if the structure is valid
|
||||
* @param {boolean} recursive - Whether to recursively load the structures in the directory
|
||||
* @returns {Promise<T[]>}
|
||||
*/
|
||||
export async function loadStructures(dir, predicate, recursive = true) {
|
||||
// Get the stats of the directory
|
||||
const statDir = await stat(dir);
|
||||
|
||||
// If the provided directory path is not a directory, throw an error
|
||||
if (!statDir.isDirectory()) {
|
||||
throw new Error(`The directory '${dir}' is not a directory.`);
|
||||
}
|
||||
|
||||
// Get all the files in the directory
|
||||
const files = await readdir(dir);
|
||||
|
||||
// Create an empty array to store the structures
|
||||
/** @type {T[]} */
|
||||
const structures = [];
|
||||
|
||||
// Loop through all the files in the directory
|
||||
for (const file of files) {
|
||||
// If the file is index.js or the file does not end with .js, skip the file
|
||||
if (file === 'index.js' || !file.endsWith('.js')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the stats of the file
|
||||
const statFile = await stat(new URL(`${dir}/${file}`));
|
||||
|
||||
// If the file is a directory and recursive is true, recursively load the structures in the directory
|
||||
if (statFile.isDirectory() && recursive) {
|
||||
structures.push(...(await loadStructures(`${dir}/${file}`, predicate, recursive)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const structure = (await import(`${dir}/${file}`)).default;
|
||||
|
||||
// If the structure is a valid structure, add it
|
||||
if (predicate(structure)) structures.push(structure);
|
||||
}
|
||||
|
||||
return structures;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('node:fs').PathLike} dir
|
||||
* @param {boolean} [recursive]
|
||||
* @returns {Promise<Map<string,import('../commands/index.js').Command>>}
|
||||
*/
|
||||
export async function loadCommands(dir, recursive = true) {
|
||||
return (await loadStructures(dir, commandPredicate, recursive)).reduce(
|
||||
(acc, cur) => acc.set(cur.data.name, cur),
|
||||
new Map(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('node:fs').PathLike} dir
|
||||
* @param {boolean} [recursive]
|
||||
* @returns {Promise<import('../events/index.js').Event[]>}
|
||||
*/
|
||||
export async function loadEvents(dir, recursive = true) {
|
||||
return loadStructures(dir, eventPredicate, recursive);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Events } from 'discord.js';
|
||||
|
||||
/**
|
||||
* @param {Map<string, import('../commands/index.js').Command>} commands
|
||||
* @param {import('../events/index.js').Event[]} events
|
||||
* @param {import('discord.js').Client} client
|
||||
*/
|
||||
export function registerEvents(commands, events, client) {
|
||||
// Create an event to handle command interactions
|
||||
/** @type {import('../events/index.js').Event<Events.InteractionCreate>} */
|
||||
const interactionCreateEvent = {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction) {
|
||||
if (interaction.isCommand()) {
|
||||
const command = commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
throw new Error(`Command '${interaction.commandName}' not found.`);
|
||||
}
|
||||
|
||||
await command.execute(interaction);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
for (const event of [...events, interactionCreateEvent]) {
|
||||
client[event.once ? 'once' : 'on'](event.name, async (...args) => event.execute(...args));
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
DISCORD_TOKEN=
|
||||
APPLICATION_ID=
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
"scripts": {
|
||||
"test": "tsc",
|
||||
"lint": "prettier --check . && eslint ./src --ext .ts --format=pretty",
|
||||
"deploy": "node --require dotenv/config dist/util/deploy.js",
|
||||
"format": "prettier --write . && eslint ./src --ext .ts --fix --format=pretty",
|
||||
"start": "node --require dotenv/config dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^0.6.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.0.3"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'discord.js';
|
||||
import type { StructurePredicate } from '../util/loaders.js';
|
||||
|
||||
/**
|
||||
* Defines the structure of a command
|
||||
*/
|
||||
export type Command = {
|
||||
/**
|
||||
* The data for the command
|
||||
*/
|
||||
data: RESTPostAPIApplicationCommandsJSONBody;
|
||||
/**
|
||||
* The function to execute when the command is called
|
||||
*
|
||||
* @param interaction - The interaction of the command
|
||||
*/
|
||||
execute(interaction: CommandInteraction): Promise<void> | void;
|
||||
};
|
||||
|
||||
// Defines the predicate to check if an object is a valid Command type
|
||||
export const predicate: StructurePredicate<Command> = (structure): structure is Command =>
|
||||
Boolean(structure) &&
|
||||
typeof structure === 'object' &&
|
||||
'data' in structure! &&
|
||||
'execute' in structure &&
|
||||
typeof structure.data === 'object' &&
|
||||
typeof structure.execute === 'function';
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Command } from './index.js';
|
||||
|
||||
export default {
|
||||
data: {
|
||||
name: 'ping',
|
||||
description: 'Ping!',
|
||||
},
|
||||
async execute(interaction) {
|
||||
await interaction.reply('Pong!');
|
||||
},
|
||||
} satisfies Command;
|
||||
@@ -1,7 +1,33 @@
|
||||
import type { ClientEvents } from 'discord.js';
|
||||
import type { StructurePredicate } from '../util/loaders.js';
|
||||
|
||||
export interface Event<T extends keyof ClientEvents = keyof ClientEvents> {
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*/
|
||||
export type Event<T extends keyof ClientEvents = keyof ClientEvents> = {
|
||||
/**
|
||||
* The function to execute when the event is emitted.
|
||||
*
|
||||
* @param parameters - The parameters of the event
|
||||
*/
|
||||
execute(...parameters: ClientEvents[T]): Promise<void> | void;
|
||||
/**
|
||||
* The name of the event to listen to
|
||||
*/
|
||||
name: T;
|
||||
/**
|
||||
* Whether or not the event should only be listened to once
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
once?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
// Defines the predicate to check if an object is a valid Event type.
|
||||
export const predicate: StructurePredicate<Event> = (structure): structure is Event =>
|
||||
Boolean(structure) &&
|
||||
typeof structure === 'object' &&
|
||||
'name' in structure! &&
|
||||
'execute' in structure &&
|
||||
typeof structure.name === 'string' &&
|
||||
typeof structure.execute === 'function';
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import type { Event } from './events/index.js';
|
||||
import { loadCommands, loadEvents } from './util/loaders.js';
|
||||
import { registerEvents } from './util/registerEvents.js';
|
||||
|
||||
// Initialize the client
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
const eventsPath = new URL('events/', import.meta.url);
|
||||
|
||||
const eventFiles = await readdir(eventsPath).then((files) =>
|
||||
files.filter((file) => file.endsWith('.js') && file !== 'index.js'),
|
||||
);
|
||||
// Load the events and commands
|
||||
const events = await loadEvents(new URL('events/', import.meta.url));
|
||||
const commands = await loadCommands(new URL('commands/', import.meta.url));
|
||||
|
||||
for (const file of eventFiles) {
|
||||
const event: Event = (await import(new URL(file, eventsPath).toString())).default;
|
||||
client[event.once ? 'once' : 'on'](event.name, async (...args) => event.execute(...args));
|
||||
}
|
||||
// Register the event handlers
|
||||
registerEvents(commands, events, client);
|
||||
|
||||
// Login to the client
|
||||
void client.login();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import process from 'node:process';
|
||||
import { URL } from 'node:url';
|
||||
import { API } from '@discordjs/core/http-only';
|
||||
import { REST } from 'discord.js';
|
||||
import { loadCommands } from './loaders.js';
|
||||
|
||||
const commands = await loadCommands(new URL('commands/', import.meta.url));
|
||||
const commandData = [...commands.values()].map((command) => command.data);
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN!);
|
||||
const api = new API(rest);
|
||||
|
||||
const result = await api.applicationCommands.bulkOverwriteGlobalCommands(process.env.APPLICATION_ID!, commandData);
|
||||
|
||||
console.log(`Successfully registered ${result.length} commands.`);
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { PathLike } from 'node:fs';
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import type { Command } from '../commands/index.js';
|
||||
import { predicate as commandPredicate } from '../commands/index.js';
|
||||
import type { Event } from '../events/index.js';
|
||||
import { predicate as eventPredicate } from '../events/index.js';
|
||||
|
||||
/**
|
||||
* A predicate to check if the structure is valid
|
||||
*/
|
||||
export type StructurePredicate<T> = (structure: unknown) => structure is T;
|
||||
|
||||
/**
|
||||
* Loads all the structures in the provided directory
|
||||
*
|
||||
* @param dir - The directory to load the structures from
|
||||
* @param predicate - The predicate to check if the structure is valid
|
||||
* @param recursive - Whether to recursively load the structures in the directory
|
||||
* @returns
|
||||
*/
|
||||
export async function loadStructures<T>(
|
||||
dir: PathLike,
|
||||
predicate: StructurePredicate<T>,
|
||||
recursive = true,
|
||||
): Promise<T[]> {
|
||||
// Get the stats of the directory
|
||||
const statDir = await stat(dir);
|
||||
|
||||
// If the provided directory path is not a directory, throw an error
|
||||
if (!statDir.isDirectory()) {
|
||||
throw new Error(`The directory '${dir}' is not a directory.`);
|
||||
}
|
||||
|
||||
// Get all the files in the directory
|
||||
const files = await readdir(dir);
|
||||
|
||||
// Create an empty array to store the structures
|
||||
const structures: T[] = [];
|
||||
|
||||
// Loop through all the files in the directory
|
||||
for (const file of files) {
|
||||
// If the file is index.js or the file does not end with .js, skip the file
|
||||
if (file === 'index.js' || !file.endsWith('.js')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the stats of the file
|
||||
const statFile = await stat(new URL(`${dir}/${file}`));
|
||||
|
||||
// If the file is a directory and recursive is true, recursively load the structures in the directory
|
||||
if (statFile.isDirectory() && recursive) {
|
||||
structures.push(...(await loadStructures(`${dir}/${file}`, predicate, recursive)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const structure = (await import(`${dir}/${file}`)).default;
|
||||
|
||||
// If the structure is a valid structure, add it
|
||||
if (predicate(structure)) structures.push(structure);
|
||||
}
|
||||
|
||||
return structures;
|
||||
}
|
||||
|
||||
export async function loadCommands(dir: PathLike, recursive = true): Promise<Map<string, Command>> {
|
||||
return (await loadStructures(dir, commandPredicate, recursive)).reduce(
|
||||
(acc, cur) => acc.set(cur.data.name, cur),
|
||||
new Map<string, Command>(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadEvents(dir: PathLike, recursive = true): Promise<Event[]> {
|
||||
return loadStructures(dir, eventPredicate, recursive);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Events, type Client } from 'discord.js';
|
||||
import type { Command } from '../commands/index.js';
|
||||
import type { Event } from '../events/index.js';
|
||||
|
||||
export function registerEvents(commands: Map<string, Command>, events: Event[], client: Client): void {
|
||||
// Create an event to handle command interactions
|
||||
const interactionCreateEvent: Event<Events.InteractionCreate> = {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction) {
|
||||
if (interaction.isCommand()) {
|
||||
const command = commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
throw new Error(`Command '${interaction.commandName}' not found.`);
|
||||
}
|
||||
|
||||
await command.execute(interaction);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
for (const event of [...events, interactionCreateEvent]) {
|
||||
client[event.once ? 'once' : 'on'](event.name, async (...args) => event.execute(...args));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTsupConfig } from '../../tsup.config.js';
|
||||
|
||||
export default createTsupConfig({
|
||||
entry: ['src/create-discord-bot.ts'],
|
||||
entry: ['src/index.ts'],
|
||||
dts: false,
|
||||
format: 'esm',
|
||||
minify: true,
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -6723,6 +6723,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prompts@npm:^2.4.4":
|
||||
version: 2.4.4
|
||||
resolution: "@types/prompts@npm:2.4.4"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
kleur: ^3.0.3
|
||||
checksum: fa8d9a6f63f5e7f4a5b9bd4d40527ca4b8c8c6a63bf0864bf72ea85706a9e1b292d73c8c9a3b7423fb80a5d3e7d563d0069d6644384438c1251adbd9efc03a12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prop-types@npm:*":
|
||||
version: 15.7.5
|
||||
resolution: "@types/prop-types@npm:15.7.5"
|
||||
@@ -10713,6 +10723,7 @@ __metadata:
|
||||
"@favware/cliff-jumper": ^2.0.0
|
||||
"@microsoft/api-extractor": ^7.35.0
|
||||
"@types/node": 16.18.32
|
||||
"@types/prompts": ^2.4.4
|
||||
"@types/validate-npm-package-name": ^4.0.0
|
||||
"@vitest/coverage-c8": ^0.31.1
|
||||
chalk: ^5.2.0
|
||||
@@ -10722,12 +10733,13 @@ __metadata:
|
||||
eslint-config-neon: ^0.1.47
|
||||
eslint-formatter-pretty: ^5.0.0
|
||||
prettier: ^2.8.8
|
||||
prompts: ^2.4.2
|
||||
tsup: ^6.7.0
|
||||
typescript: ^5.0.4
|
||||
validate-npm-package-name: ^5.0.0
|
||||
vitest: ^0.31.1
|
||||
bin:
|
||||
create-discord-bot: ./dist/create-discord-bot.mjs
|
||||
create-discord-bot: ./dist/index.mjs
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -21058,7 +21070,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:~2.4.2":
|
||||
"prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:^2.4.2, prompts@npm:~2.4.2":
|
||||
version: 2.4.2
|
||||
resolution: "prompts@npm:2.4.2"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user