mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 08:03:30 +01:00
feat: add Bun templates for create-discord-bot (#11348)
* feat: add Bun templates for create-discord-bot - Add TypeScript Bun template with complete src structure - Add JavaScript Bun template with complete src structure - Both templates mirror the Node.js versions with Bun-specific configuration Closes #11346 * fix: update template and bun template loading --------- Co-authored-by: almeidx <github@almeidx.dev> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
5
packages/create-discord-bot/.gitignore
vendored
5
packages/create-discord-bot/.gitignore
vendored
@@ -13,10 +13,7 @@ pids
|
||||
|
||||
# Env
|
||||
.env
|
||||
!template/Bun/.env
|
||||
!template/Deno/.env
|
||||
!template/JavaScript/.env
|
||||
!template/TypeScript/.env
|
||||
!template/**/.env
|
||||
|
||||
# Dist
|
||||
dist
|
||||
|
||||
@@ -44,27 +44,17 @@ export async function createDiscordBot({ directory, installPackages, typescript,
|
||||
}
|
||||
|
||||
console.log(`Creating ${directoryName} in ${styleText('green', root)}.`);
|
||||
|
||||
const deno = packageManager === 'deno';
|
||||
await cp(new URL(`../template/${deno ? 'Deno' : typescript ? 'TypeScript' : 'JavaScript'}`, import.meta.url), root, {
|
||||
const bun = packageManager === 'bun';
|
||||
|
||||
const lang = typescript ? 'TypeScript' : 'JavaScript';
|
||||
const templateBasePath = deno ? 'Deno' : bun ? `Bun/${lang}` : lang;
|
||||
|
||||
await cp(new URL(`../template/${templateBasePath}`, import.meta.url), root, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const bun = packageManager === 'bun';
|
||||
if (bun) {
|
||||
await cp(
|
||||
new URL(`../template/Bun/${typescript ? 'TypeScript' : 'JavaScript'}/package.json`, import.meta.url),
|
||||
`${root}/package.json`,
|
||||
);
|
||||
|
||||
if (typescript) {
|
||||
await cp(
|
||||
new URL('../template/Bun/TypeScript/tsconfig.eslint.json', import.meta.url),
|
||||
`${root}/tsconfig.eslint.json`,
|
||||
);
|
||||
await cp(new URL('../template/Bun/TypeScript/tsconfig.json', import.meta.url), `${root}/tsconfig.json`);
|
||||
}
|
||||
}
|
||||
|
||||
process.chdir(root);
|
||||
|
||||
const newVSCodeSettings = await readFile('./.vscode/settings.json', { encoding: 'utf8' });
|
||||
|
||||
@@ -19,6 +19,10 @@ export function resolvePackageManager(): PackageManager {
|
||||
return 'deno';
|
||||
}
|
||||
|
||||
if (process.versions.bun) {
|
||||
return 'bun';
|
||||
}
|
||||
|
||||
// If this is not present, return the default package manager.
|
||||
if (!npmConfigUserAgent) {
|
||||
return DEFAULT_PACKAGE_MANAGER;
|
||||
|
||||
2
packages/create-discord-bot/template/Bun/JavaScript/.env
Normal file
2
packages/create-discord-bot/template/Bun/JavaScript/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
DISCORD_TOKEN=
|
||||
APPLICATION_ID=
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc.json",
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
10
packages/create-discord-bot/template/Bun/JavaScript/.vscode/extensions.json
vendored
Normal file
10
packages/create-discord-bot/template/Bun/JavaScript/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"codezombiech.gitignore",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"christian-kohler.path-intellisense",
|
||||
"oven.bun-vscode"
|
||||
]
|
||||
}
|
||||
13
packages/create-discord-bot/template/Bun/JavaScript/.vscode/settings.json
vendored
Normal file
13
packages/create-discord-bot/template/Bun/JavaScript/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.trimAutoWhitespace": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.eol": "\n",
|
||||
"npm.packageManager": "bun"
|
||||
}
|
||||
@@ -10,7 +10,20 @@ const config = [
|
||||
...node,
|
||||
...prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
Bun: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-globals': 0,
|
||||
'n/prefer-global/buffer': [2, 'never'],
|
||||
'n/prefer-global/console': [2, 'always'],
|
||||
'n/prefer-global/process': [2, 'never'],
|
||||
'n/prefer-global/text-decoder': [2, 'always'],
|
||||
'n/prefer-global/text-encoder': [2, 'always'],
|
||||
'n/prefer-global/url-search-params': [2, 'always'],
|
||||
'n/prefer-global/url': [2, 'always'],
|
||||
'jsdoc/check-tag-names': 0,
|
||||
'jsdoc/no-undefined-types': 0,
|
||||
'jsdoc/valid-types': 0,
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "prettier --check . && eslint --ext .js --format=pretty src",
|
||||
"deploy": "bun run src/util/deploy.js",
|
||||
"format": "prettier --write . && eslint --ext .js --fix --format=pretty src",
|
||||
"start": "bun run src/index.js"
|
||||
"lint": "prettier --check . && eslint --ext .js,.mjs,.cjs --format=pretty src",
|
||||
"deploy": "bun --env-file=.env src/util/deploy.js",
|
||||
"format": "prettier --write . && eslint --ext .js,.mjs,.cjs --fix --format=pretty src",
|
||||
"start": "bun --env-file=.env src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.4.0",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* 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 schema for a command
|
||||
*/
|
||||
export const schema = z.object({
|
||||
data: z.record(z.string(), z.any()),
|
||||
execute: z.function(),
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) => schema.safeParse(structure).success;
|
||||
@@ -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,10 @@
|
||||
/** @type {import('../index.js').Command} */
|
||||
export default {
|
||||
data: {
|
||||
name: 'user',
|
||||
description: 'Provides information about the user.',
|
||||
},
|
||||
async execute(interaction) {
|
||||
await interaction.reply(`This command was run by ${interaction.user.username}.`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*
|
||||
* @template {keyof import('discord.js').ClientEvents} [EventName=keyof import('discord.js').ClientEvents]
|
||||
* @typedef {object} Event
|
||||
* @property {(...parameters: import('discord.js').ClientEvents[EventName]) => Promise<void> | void} execute The function to execute the command
|
||||
* @property {EventName} 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 schema for an event.
|
||||
*
|
||||
*/
|
||||
export const schema = z.object({
|
||||
name: z.string(),
|
||||
once: z.boolean().optional().default(false),
|
||||
execute: z.function(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Defines the predicate to check if an object is a valid Event type.
|
||||
*
|
||||
* @type {import('../util/loaders.js').StructurePredicate<Event>}
|
||||
* @returns {structure is Event}
|
||||
*/
|
||||
export const predicate = (structure) => schema.safeParse(structure).success;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Events } from 'discord.js';
|
||||
import { loadCommands } from '../util/loaders.js';
|
||||
|
||||
const commands = await loadCommands(new URL('../commands/', import.meta.url));
|
||||
|
||||
/** @type {import('../events/index.js').Event<Events.InteractionCreate>} */
|
||||
export default {
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Events } from 'discord.js';
|
||||
|
||||
/** @type {import('./index.js').Event<Events.ClientReady>} */
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
async execute(client) {
|
||||
console.log(`Ready! Logged in as ${client.user.tag}`);
|
||||
},
|
||||
};
|
||||
@@ -1 +1,22 @@
|
||||
console.log();
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { loadEvents } from './util/loaders.js';
|
||||
|
||||
// Initialize the client
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
// Load the events and commands
|
||||
const events = await loadEvents(new URL('events/', import.meta.url));
|
||||
|
||||
// Register the event handlers
|
||||
for (const event of events) {
|
||||
client[event.once ? 'once' : 'on'](event.name, async (...args) => {
|
||||
try {
|
||||
await event.execute(...args);
|
||||
} catch (error) {
|
||||
console.error(`Error executing event ${String(event.name)}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Login to the client
|
||||
void client.login(Bun.env.DISCORD_TOKEN);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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(Bun.env.DISCORD_TOKEN);
|
||||
const api = new API(rest);
|
||||
|
||||
const result = await api.applicationCommands.bulkOverwriteGlobalCommands(Bun.env.APPLICATION_ID, commandData);
|
||||
|
||||
console.log(`Successfully registered ${result.length} commands.`);
|
||||
@@ -0,0 +1,80 @@
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { basename, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Glob } from 'bun';
|
||||
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 Structure
|
||||
* @typedef {(structure: unknown) => structure is Structure} StructurePredicate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Loads all the structures in the provided directory.
|
||||
*
|
||||
* @template Structure
|
||||
* @param {import('node:fs').PathLike} dir - The directory to load the structures from
|
||||
* @param {StructurePredicate<Structure>} predicate - The predicate to check if the structure is valid
|
||||
* @param {boolean} recursive - Whether to recursively load the structures in the directory
|
||||
* @returns {Promise<Structure[]>}
|
||||
*/
|
||||
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.`);
|
||||
}
|
||||
|
||||
// Create an empty array to store the structures
|
||||
/** @type {Structure[]} */
|
||||
const structures = [];
|
||||
|
||||
// Create a glob pattern to match the .js files
|
||||
const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
|
||||
const pattern = resolve(basePath, recursive ? '**/*.js' : '*.js');
|
||||
const glob = new Glob(pattern);
|
||||
|
||||
// Loop through all the matching files in the directory
|
||||
for await (const file of glob.scan('.')) {
|
||||
// If the file is index.js, skip the file
|
||||
if (basename(file) === 'index.js') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const { default: structure } = await import(file);
|
||||
|
||||
// If the default export 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);
|
||||
}
|
||||
2
packages/create-discord-bot/template/Bun/TypeScript/.env
Normal file
2
packages/create-discord-bot/template/Bun/TypeScript/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
DISCORD_TOKEN=
|
||||
APPLICATION_ID=
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc.json",
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
10
packages/create-discord-bot/template/Bun/TypeScript/.vscode/extensions.json
vendored
Normal file
10
packages/create-discord-bot/template/Bun/TypeScript/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"codezombiech.gitignore",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"christian-kohler.path-intellisense",
|
||||
"oven.bun-vscode"
|
||||
]
|
||||
}
|
||||
13
packages/create-discord-bot/template/Bun/TypeScript/.vscode/settings.json
vendored
Normal file
13
packages/create-discord-bot/template/Bun/TypeScript/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.trimAutoWhitespace": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.eol": "\n",
|
||||
"npm.packageManager": "bun"
|
||||
}
|
||||
@@ -18,6 +18,14 @@ const config = [
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-globals': 0,
|
||||
'n/prefer-global/buffer': [2, 'never'],
|
||||
'n/prefer-global/console': [2, 'always'],
|
||||
'n/prefer-global/process': [2, 'never'],
|
||||
'n/prefer-global/text-decoder': [2, 'always'],
|
||||
'n/prefer-global/text-encoder': [2, 'always'],
|
||||
'n/prefer-global/url-search-params': [2, 'always'],
|
||||
'n/prefer-global/url': [2, 'always'],
|
||||
'import/extensions': 0,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "tsc && prettier --check . && eslint --ext .ts --format=pretty src",
|
||||
"deploy": "bun run src/util/deploy.ts",
|
||||
"deploy": "bun --env-file=.env src/util/deploy.ts",
|
||||
"format": "prettier --write . && eslint --ext .ts --fix --format=pretty src",
|
||||
"start": "bun run src/index.ts"
|
||||
"start": "bun --env-file=.env src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.4.0",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'discord.js';
|
||||
import { z } from 'zod';
|
||||
import type { StructurePredicate } from '../util/loaders.ts';
|
||||
|
||||
/**
|
||||
* 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 schema for a command
|
||||
*/
|
||||
export const schema = z.object({
|
||||
data: z.record(z.string(), z.any()),
|
||||
execute: z.function(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Defines the predicate to check if an object is a valid Command type.
|
||||
*/
|
||||
export const predicate: StructurePredicate<Command> = (structure: unknown): structure is Command =>
|
||||
schema.safeParse(structure).success;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Command } from './index.ts';
|
||||
|
||||
export default {
|
||||
data: {
|
||||
name: 'ping',
|
||||
description: 'Ping!',
|
||||
},
|
||||
async execute(interaction) {
|
||||
await interaction.reply('Pong!');
|
||||
},
|
||||
} satisfies Command;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Command } from '../index.ts';
|
||||
|
||||
export default {
|
||||
data: {
|
||||
name: 'user',
|
||||
description: 'Provides information about the user.',
|
||||
},
|
||||
async execute(interaction) {
|
||||
await interaction.reply(`This command was run by ${interaction.user.username}.`);
|
||||
},
|
||||
} satisfies Command;
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ClientEvents } from 'discord.js';
|
||||
import { z } from 'zod';
|
||||
import type { StructurePredicate } from '../util/loaders.ts';
|
||||
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*/
|
||||
export type Event<EventName extends keyof ClientEvents = keyof ClientEvents> = {
|
||||
/**
|
||||
* The function to execute when the event is emitted.
|
||||
*
|
||||
* @param parameters - The parameters of the event
|
||||
*/
|
||||
execute(...parameters: ClientEvents[EventName]): Promise<void> | void;
|
||||
/**
|
||||
* The name of the event to listen to
|
||||
*/
|
||||
name: EventName;
|
||||
/**
|
||||
* Whether or not the event should only be listened to once
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
once?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the schema for an event.
|
||||
*/
|
||||
export const schema = z.object({
|
||||
name: z.string(),
|
||||
once: z.boolean().optional().default(false),
|
||||
execute: z.function(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Defines the predicate to check if an object is a valid Event type.
|
||||
*/
|
||||
export const predicate: StructurePredicate<Event> = (structure: unknown): structure is Event =>
|
||||
schema.safeParse(structure).success;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Events } from 'discord.js';
|
||||
import { loadCommands } from '../util/loaders.ts';
|
||||
import type { Event } from './index.ts';
|
||||
|
||||
const commands = await loadCommands(new URL('../commands/', import.meta.url));
|
||||
|
||||
export default {
|
||||
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);
|
||||
}
|
||||
},
|
||||
} satisfies Event<Events.InteractionCreate>;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Events } from 'discord.js';
|
||||
import type { Event } from './index.ts';
|
||||
|
||||
export default {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
async execute(client) {
|
||||
console.log(`Ready! Logged in as ${client.user.tag}`);
|
||||
},
|
||||
} satisfies Event<Events.ClientReady>;
|
||||
@@ -1 +1,22 @@
|
||||
export {};
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { loadEvents } from './util/loaders.ts';
|
||||
|
||||
// Initialize the client
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
// Load the events and commands
|
||||
const events = await loadEvents(new URL('events/', import.meta.url));
|
||||
|
||||
// Register the event handlers
|
||||
for (const event of events) {
|
||||
client[event.once ? 'once' : 'on'](event.name, async (...args) => {
|
||||
try {
|
||||
await event.execute(...args);
|
||||
} catch (error) {
|
||||
console.error(`Error executing event ${String(event.name)}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Login to the client
|
||||
void client.login(Bun.env.DISCORD_TOKEN);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { API } from '@discordjs/core/http-only';
|
||||
import { REST } from 'discord.js';
|
||||
import { loadCommands } from './loaders.ts';
|
||||
|
||||
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(Bun.env.DISCORD_TOKEN!);
|
||||
const api = new API(rest);
|
||||
|
||||
const result = await api.applicationCommands.bulkOverwriteGlobalCommands(Bun.env.APPLICATION_ID!, commandData);
|
||||
|
||||
console.log(`Successfully registered ${result.length} commands.`);
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { PathLike } from 'node:fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { basename, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Glob } from 'bun';
|
||||
import { predicate as commandPredicate, type Command } from '../commands/index.ts';
|
||||
import { predicate as eventPredicate, type Event } from '../events/index.ts';
|
||||
|
||||
/**
|
||||
* A predicate to check if the structure is valid
|
||||
*/
|
||||
export type StructurePredicate<Structure> = (structure: unknown) => structure is Structure;
|
||||
|
||||
/**
|
||||
* 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<Structure>(
|
||||
dir: PathLike,
|
||||
predicate: StructurePredicate<Structure>,
|
||||
recursive = true,
|
||||
): Promise<Structure[]> {
|
||||
// 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.`);
|
||||
}
|
||||
|
||||
// Create an empty array to store the structures
|
||||
const structures: Structure[] = [];
|
||||
|
||||
// Create a glob pattern to match the .ts files
|
||||
const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
|
||||
const pattern = resolve(basePath, recursive ? '**/*.ts' : '*.ts');
|
||||
const glob = new Glob(pattern);
|
||||
|
||||
// Loop through all the matching files in the directory
|
||||
for await (const file of glob.scan('.')) {
|
||||
// If the file is index.ts, skip the file
|
||||
if (basename(file) === 'index.ts') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const { default: structure } = await import(file);
|
||||
|
||||
// If the default export 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);
|
||||
}
|
||||
@@ -4,5 +4,5 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src"]
|
||||
"include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src", "bin"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user