From be78e26729202d36c8335889cd2c38c5fd4ef423 Mon Sep 17 00:00:00 2001 From: ManHam Date: Fri, 16 Jan 2026 21:02:20 +0530 Subject: [PATCH] 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 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/create-discord-bot/.gitignore | 5 +- .../src/create-discord-bot.ts | 24 ++---- .../src/helpers/packageManager.ts | 4 + .../template/Bun/JavaScript/.env | 2 + .../template/Bun/JavaScript/.prettierrc.json | 9 +++ .../Bun/JavaScript/.vscode/extensions.json | 10 +++ .../Bun/JavaScript/.vscode/settings.json | 13 +++ .../template/Bun/JavaScript/eslint.config.js | 13 +++ .../template/Bun/JavaScript/package.json | 8 +- .../Bun/JavaScript/src/commands/index.js | 25 ++++++ .../Bun/JavaScript/src/commands/ping.js | 10 +++ .../JavaScript/src/commands/utility/user.js | 10 +++ .../Bun/JavaScript/src/events/index.js | 29 +++++++ .../src/events/interactionCreate.js | 20 +++++ .../Bun/JavaScript/src/events/ready.js | 10 +++ .../template/Bun/JavaScript/src/index.js | 23 +++++- .../Bun/JavaScript/src/util/deploy.js | 13 +++ .../Bun/JavaScript/src/util/loaders.js | 80 +++++++++++++++++++ .../template/Bun/TypeScript/.env | 2 + .../template/Bun/TypeScript/.prettierrc.json | 9 +++ .../Bun/TypeScript/.vscode/extensions.json | 10 +++ .../Bun/TypeScript/.vscode/settings.json | 13 +++ .../template/Bun/TypeScript/eslint.config.js | 8 ++ .../template/Bun/TypeScript/package.json | 4 +- .../Bun/TypeScript/src/commands/index.ts | 33 ++++++++ .../Bun/TypeScript/src/commands/ping.ts | 11 +++ .../TypeScript/src/commands/utility/user.ts | 11 +++ .../Bun/TypeScript/src/events/index.ts | 40 ++++++++++ .../src/events/interactionCreate.ts | 20 +++++ .../Bun/TypeScript/src/events/ready.ts | 10 +++ .../template/Bun/TypeScript/src/index.ts | 23 +++++- .../Bun/TypeScript/src/util/deploy.ts | 13 +++ .../Bun/TypeScript/src/util/loaders.ts | 71 ++++++++++++++++ .../Bun/TypeScript/tsconfig.eslint.json | 2 +- 34 files changed, 558 insertions(+), 30 deletions(-) create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/.env create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/.prettierrc.json create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/.vscode/extensions.json create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/.vscode/settings.json create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/commands/index.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/commands/ping.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/commands/utility/user.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/events/index.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/events/interactionCreate.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/events/ready.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/util/deploy.js create mode 100644 packages/create-discord-bot/template/Bun/JavaScript/src/util/loaders.js create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/.env create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/.prettierrc.json create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/.vscode/extensions.json create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/.vscode/settings.json create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/commands/index.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/commands/ping.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/commands/utility/user.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/events/index.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/events/interactionCreate.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/events/ready.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/util/deploy.ts create mode 100644 packages/create-discord-bot/template/Bun/TypeScript/src/util/loaders.ts diff --git a/packages/create-discord-bot/.gitignore b/packages/create-discord-bot/.gitignore index 548bac7e8..226e64ac4 100644 --- a/packages/create-discord-bot/.gitignore +++ b/packages/create-discord-bot/.gitignore @@ -13,10 +13,7 @@ pids # Env .env -!template/Bun/.env -!template/Deno/.env -!template/JavaScript/.env -!template/TypeScript/.env +!template/**/.env # Dist dist diff --git a/packages/create-discord-bot/src/create-discord-bot.ts b/packages/create-discord-bot/src/create-discord-bot.ts index 16fc3e82b..f0393bce8 100644 --- a/packages/create-discord-bot/src/create-discord-bot.ts +++ b/packages/create-discord-bot/src/create-discord-bot.ts @@ -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' }); diff --git a/packages/create-discord-bot/src/helpers/packageManager.ts b/packages/create-discord-bot/src/helpers/packageManager.ts index ff5d98150..dd3bdff8c 100644 --- a/packages/create-discord-bot/src/helpers/packageManager.ts +++ b/packages/create-discord-bot/src/helpers/packageManager.ts @@ -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; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/.env b/packages/create-discord-bot/template/Bun/JavaScript/.env new file mode 100644 index 000000000..b9edc2b71 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/.env @@ -0,0 +1,2 @@ +DISCORD_TOKEN= +APPLICATION_ID= diff --git a/packages/create-discord-bot/template/Bun/JavaScript/.prettierrc.json b/packages/create-discord-bot/template/Bun/JavaScript/.prettierrc.json new file mode 100644 index 000000000..7920c42b8 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc.json", + "printWidth": 120, + "useTabs": true, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "all", + "endOfLine": "lf" +} diff --git a/packages/create-discord-bot/template/Bun/JavaScript/.vscode/extensions.json b/packages/create-discord-bot/template/Bun/JavaScript/.vscode/extensions.json new file mode 100644 index 000000000..f1e19ae96 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "codezombiech.gitignore", + "christian-kohler.npm-intellisense", + "christian-kohler.path-intellisense", + "oven.bun-vscode" + ] +} diff --git a/packages/create-discord-bot/template/Bun/JavaScript/.vscode/settings.json b/packages/create-discord-bot/template/Bun/JavaScript/.vscode/settings.json new file mode 100644 index 000000000..67a23419a --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/.vscode/settings.json @@ -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" +} diff --git a/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js b/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js index 5d92eaf15..c844447f8 100644 --- a/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js +++ b/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js @@ -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, diff --git a/packages/create-discord-bot/template/Bun/JavaScript/package.json b/packages/create-discord-bot/template/Bun/JavaScript/package.json index e6d581126..bb9454637 100644 --- a/packages/create-discord-bot/template/Bun/JavaScript/package.json +++ b/packages/create-discord-bot/template/Bun/JavaScript/package.json @@ -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", diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/commands/index.js b/packages/create-discord-bot/template/Bun/JavaScript/src/commands/index.js new file mode 100644 index 000000000..db60dd550 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/commands/index.js @@ -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} 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} + * @returns {structure is Command} + */ +export const predicate = (structure) => schema.safeParse(structure).success; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/commands/ping.js b/packages/create-discord-bot/template/Bun/JavaScript/src/commands/ping.js new file mode 100644 index 000000000..8a30f80ec --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/commands/ping.js @@ -0,0 +1,10 @@ +/** @type {import('./index.js').Command} */ +export default { + data: { + name: 'ping', + description: 'Ping!', + }, + async execute(interaction) { + await interaction.reply('Pong!'); + }, +}; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/commands/utility/user.js b/packages/create-discord-bot/template/Bun/JavaScript/src/commands/utility/user.js new file mode 100644 index 000000000..9c6fcbce0 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/commands/utility/user.js @@ -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}.`); + }, +}; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/events/index.js b/packages/create-discord-bot/template/Bun/JavaScript/src/events/index.js new file mode 100644 index 000000000..5d870899c --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/events/index.js @@ -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} 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} + * @returns {structure is Event} + */ +export const predicate = (structure) => schema.safeParse(structure).success; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/events/interactionCreate.js b/packages/create-discord-bot/template/Bun/JavaScript/src/events/interactionCreate.js new file mode 100644 index 000000000..eb2fed7c6 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/events/interactionCreate.js @@ -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} */ +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); + } + }, +}; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/events/ready.js b/packages/create-discord-bot/template/Bun/JavaScript/src/events/ready.js new file mode 100644 index 000000000..8b93cfd94 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/events/ready.js @@ -0,0 +1,10 @@ +import { Events } from 'discord.js'; + +/** @type {import('./index.js').Event} */ +export default { + name: Events.ClientReady, + once: true, + async execute(client) { + console.log(`Ready! Logged in as ${client.user.tag}`); + }, +}; diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/index.js b/packages/create-discord-bot/template/Bun/JavaScript/src/index.js index b7bd4c885..97876363b 100644 --- a/packages/create-discord-bot/template/Bun/JavaScript/src/index.js +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/index.js @@ -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); diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/util/deploy.js b/packages/create-discord-bot/template/Bun/JavaScript/src/util/deploy.js new file mode 100644 index 000000000..c049b9144 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/util/deploy.js @@ -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.`); diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/util/loaders.js b/packages/create-discord-bot/template/Bun/JavaScript/src/util/loaders.js new file mode 100644 index 000000000..16d093f33 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/JavaScript/src/util/loaders.js @@ -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} predicate - The predicate to check if the structure is valid + * @param {boolean} recursive - Whether to recursively load the structures in the directory + * @returns {Promise} + */ +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>} + */ +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} + */ +export async function loadEvents(dir, recursive = true) { + return loadStructures(dir, eventPredicate, recursive); +} diff --git a/packages/create-discord-bot/template/Bun/TypeScript/.env b/packages/create-discord-bot/template/Bun/TypeScript/.env new file mode 100644 index 000000000..b9edc2b71 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/.env @@ -0,0 +1,2 @@ +DISCORD_TOKEN= +APPLICATION_ID= diff --git a/packages/create-discord-bot/template/Bun/TypeScript/.prettierrc.json b/packages/create-discord-bot/template/Bun/TypeScript/.prettierrc.json new file mode 100644 index 000000000..7920c42b8 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc.json", + "printWidth": 120, + "useTabs": true, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "all", + "endOfLine": "lf" +} diff --git a/packages/create-discord-bot/template/Bun/TypeScript/.vscode/extensions.json b/packages/create-discord-bot/template/Bun/TypeScript/.vscode/extensions.json new file mode 100644 index 000000000..f1e19ae96 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "codezombiech.gitignore", + "christian-kohler.npm-intellisense", + "christian-kohler.path-intellisense", + "oven.bun-vscode" + ] +} diff --git a/packages/create-discord-bot/template/Bun/TypeScript/.vscode/settings.json b/packages/create-discord-bot/template/Bun/TypeScript/.vscode/settings.json new file mode 100644 index 000000000..67a23419a --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/.vscode/settings.json @@ -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" +} diff --git a/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js b/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js index 041e3ca10..392da30b6 100644 --- a/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js +++ b/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js @@ -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, }, }, diff --git a/packages/create-discord-bot/template/Bun/TypeScript/package.json b/packages/create-discord-bot/template/Bun/TypeScript/package.json index b69c32510..a77bdb29f 100644 --- a/packages/create-discord-bot/template/Bun/TypeScript/package.json +++ b/packages/create-discord-bot/template/Bun/TypeScript/package.json @@ -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", diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/commands/index.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/commands/index.ts new file mode 100644 index 000000000..ba63bf6da --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/commands/index.ts @@ -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; +}; + +/** + * 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 = (structure: unknown): structure is Command => + schema.safeParse(structure).success; diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/commands/ping.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/commands/ping.ts new file mode 100644 index 000000000..7b30e8273 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/commands/ping.ts @@ -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; diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/commands/utility/user.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/commands/utility/user.ts new file mode 100644 index 000000000..34bc5315a --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/commands/utility/user.ts @@ -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; diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/events/index.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/events/index.ts new file mode 100644 index 000000000..ccbfe5c18 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/events/index.ts @@ -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 = { + /** + * The function to execute when the event is emitted. + * + * @param parameters - The parameters of the event + */ + execute(...parameters: ClientEvents[EventName]): Promise | 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 = (structure: unknown): structure is Event => + schema.safeParse(structure).success; diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/events/interactionCreate.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/events/interactionCreate.ts new file mode 100644 index 000000000..e7cad6726 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/events/interactionCreate.ts @@ -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; diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/events/ready.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/events/ready.ts new file mode 100644 index 000000000..5fd6216f9 --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/events/ready.ts @@ -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; diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts index cb0ff5c3b..095822b12 100644 --- a/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts @@ -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); diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/util/deploy.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/util/deploy.ts new file mode 100644 index 000000000..d7f26da4b --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/util/deploy.ts @@ -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.`); diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/util/loaders.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/util/loaders.ts new file mode 100644 index 000000000..19b3c025a --- /dev/null +++ b/packages/create-discord-bot/template/Bun/TypeScript/src/util/loaders.ts @@ -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: 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( + dir: PathLike, + predicate: StructurePredicate, + recursive = true, +): Promise { + // 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> { + return (await loadStructures(dir, commandPredicate, recursive)).reduce( + (acc, cur) => acc.set(cur.data.name, cur), + new Map(), + ); +} + +export async function loadEvents(dir: PathLike, recursive = true): Promise { + return loadStructures(dir, eventPredicate, recursive); +} diff --git a/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.eslint.json b/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.eslint.json index 0aa3b9660..ae192e2de 100644 --- a/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.eslint.json +++ b/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.eslint.json @@ -4,5 +4,5 @@ "compilerOptions": { "allowJs": true }, - "include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src"] + "include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src", "bin"] }