├── .eslintignore ├── .eslintrc.json ├── .github └── dependabot.yml ├── .gitignore ├── .husky ├── .gitignore ├── _ │ └── husky.sh └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── README.md ├── package-lock.json ├── package.json ├── src ├── RENAME.env ├── commands │ ├── autocomplete.ts │ ├── command-template.ts.template │ ├── ping.ts │ ├── subautocompletetest.ts │ └── subcommandtest.ts ├── config.json ├── deployGlobalCommands.ts ├── events │ ├── interactionCreate.ts │ ├── messageCreate.ts │ └── ready.ts ├── global.d.ts ├── index.ts ├── messageCommands │ ├── deploy.ts │ ├── message-command-template.js.template │ └── undeploy.ts ├── subCommands │ ├── sub-command-template.ts.template │ ├── subautocompletetest │ │ ├── autocompletegroup │ │ │ └── guidesub.ts │ │ └── guide.ts │ └── subcommandtest │ │ ├── grouptest │ │ └── pingping.ts │ │ └── test.ts └── templates │ ├── ApplicationCommand.ts │ ├── BaseCommand.ts │ ├── Event.ts │ ├── MessageCommand.ts │ └── SubCommand.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "prettier", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 14 | ], 15 | "parserOptions": { 16 | "project": ["./tsconfig.json"] 17 | }, 18 | "globals": { 19 | "client": "writable" 20 | }, 21 | "rules": { 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/no-unsafe-argument": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # MacOS 79 | .DS_STORE 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/_/husky.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | if [ -z "$husky_skip_init" ]; then 3 | debug () { 4 | if [ "$HUSKY_DEBUG" = "1" ]; then 5 | echo "husky (debug) - $1" 6 | fi 7 | } 8 | 9 | readonly hook_name="$(basename -- "$0")" 10 | debug "starting $hook_name..." 11 | 12 | if [ "$HUSKY" = "0" ]; then 13 | debug "HUSKY env variable is set to 0, skipping hook" 14 | exit 0 15 | fi 16 | 17 | if [ -f ~/.huskyrc ]; then 18 | debug "sourcing ~/.huskyrc" 19 | . ~/.huskyrc 20 | fi 21 | 22 | readonly husky_skip_init=1 23 | export husky_skip_init 24 | sh -e "$0" "$@" 25 | exitCode="$?" 26 | 27 | if [ $exitCode != 0 ]; then 28 | echo "husky - $hook_name hook exited with code $exitCode (error)" 29 | fi 30 | 31 | if [ $exitCode = 127 ]; then 32 | echo "husky - command not found in PATH=$PATH" 33 | fi 34 | 35 | exit $exitCode 36 | fi 37 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typescript-Discord.js-v14-Template 2 | 3 | This repository provides a rapid deployment template for building Discord bots using Discord.js version 14 with Typescript. It includes ready-to-use code for handling commands, events, and interactions, so you can get your bot up and running quickly. 4 | 5 | ## 📦 Installation 6 | 7 | Follow these steps to get started: 8 | 9 | 1. **Clone the Repository:** Use the command below to clone the project. 10 | ```bash 11 | git clone https://github.com/OfficialDelta/Typescript-Discord.js-v14-Template.git YOUR_PROJECT_NAME 12 | ``` 13 | 14 | 2. **Configure Environment:** Rename `RENAME.env` to `.env`, and update the `.env` and `config.json` files with the required settings specific to your bot. 15 | 16 | 3. **Customize Commands:** Follow the provided templates to create and customize commands for your bot. Everything is set up to make this process as smooth as possible. 17 | 18 | ## 🏗️ Building 19 | 20 | Use the predefined scripts in the `package.json` file to build and manage your project. These scripts streamline various tasks, such as compiling and linting. 21 | 22 | ## ✉️ Support & Contributions 23 | 24 | Feel free to open issues or submit pull requests if you have suggestions, questions, or run into any problems. Your contributions are welcome! 25 | 26 | --- 27 | 28 | Happy coding, and enjoy building your Discord bot with the Typescript-Discord.js-v14-Template! 🚀 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "lint-staged": { 4 | "*.ts": "eslint --cache --fix", 5 | "*.{ts,css,md}": "prettier --write" 6 | }, 7 | "scripts": { 8 | "prepare": "husky install", 9 | "lint": "eslint --cache --fix src", 10 | "prettier": "prettier --write src", 11 | "compile": "tsc && npm run copy", 12 | "clean": "rimraf dist/", 13 | "copy": "npx copyfiles -u 1 src/.env src/**/*.json dist/", 14 | "build": "npm run lint && npm run prettier && npm run clean && npm run compile", 15 | "start": "npx copyfiles -u 1 src/.env src/**/*.json dist/ && cd dist && node . && cd ..", 16 | "dev": "npm run clean && tsc-watch --onSuccess \"npm run start\"", 17 | "prod": "npm run build && npm run start" 18 | }, 19 | "dependencies": { 20 | "@discordjs/rest": "^2.0.1", 21 | "discord.js": "^14.13.0", 22 | "dotenv": "^16.3.1" 23 | }, 24 | "devDependencies": { 25 | "@tsconfig/node20": "^20.1.1", 26 | "@typescript-eslint/eslint-plugin": "^6.4.1", 27 | "@typescript-eslint/parser": "^6.4.1", 28 | "eslint": "^8.47.0", 29 | "eslint-config-prettier": "^9.0.0", 30 | "husky": "^8.0.3", 31 | "lint-staged": "^14.0.1", 32 | "prettier": "^3.0.2", 33 | "typescript": "^5.1.6", 34 | "rimraf": "^5.0.1", 35 | "tsc-watch": "^6.0.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RENAME.env: -------------------------------------------------------------------------------- 1 | TOKEN="INSERT-TOKEN-HERE" 2 | CLIENT_ID="INSERT-CLIENT-ID-HERE" 3 | DEFAULT_GUILD_ID="INSERT-DEFAULT-GUILD-ID-HERE" 4 | OWNER_ID="INSERT-OWNER-ID-HERE" -------------------------------------------------------------------------------- /src/commands/autocomplete.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import ApplicationCommand from '../templates/ApplicationCommand.js' 3 | 4 | export default new ApplicationCommand({ 5 | data: new SlashCommandBuilder() 6 | .setName('guide') 7 | .addStringOption((option) => 8 | option 9 | .setName('query') 10 | .setDescription('Phrase to search for') 11 | .setAutocomplete(true) 12 | .setRequired(true) 13 | ) 14 | .addStringOption((option) => 15 | option 16 | .setName('version') 17 | .setDescription('Version to search in') 18 | .setAutocomplete(true) 19 | .setRequired(true) 20 | ) 21 | .setDescription('Search a guide!'), 22 | 23 | async autocomplete(interaction) { 24 | const focusedOption = interaction.options.getFocused(true) 25 | let choices: string[] = [] 26 | 27 | if (focusedOption.name === 'query') { 28 | choices = [ 29 | 'Popular Topics: Threads', 30 | 'Sharding: Getting started', 31 | 'Library: Voice Connections', 32 | 'Interactions: Replying to slash commands', 33 | 'Popular Topics: Embed preview' 34 | ] 35 | } 36 | 37 | if (focusedOption.name === 'version') { 38 | choices = ['v9', 'v11', 'v12', 'v13', 'v14'] 39 | } 40 | 41 | const filtered = choices.filter((choice) => 42 | choice.startsWith(focusedOption.value) 43 | ) 44 | await interaction.respond( 45 | filtered.map((choice) => ({ name: choice, value: choice })) 46 | ) 47 | }, 48 | async execute(interaction) { 49 | const query = interaction.options.getString('query') 50 | const version = interaction.options.getString('version') 51 | await interaction.reply(`You searched for ${query} in ${version}!`) 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /src/commands/command-template.ts.template: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import ApplicationCommand from '../templates/ApplicationCommand.js' 3 | 4 | export default new ApplicationCommand({ 5 | data: new SlashCommandBuilder() 6 | .setName('ping') 7 | .setDescription('Replies pong!'), 8 | async execute(interaction): Promise { 9 | await interaction.reply('Pong!') 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/commands/ping.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from 'discord.js' 2 | import ApplicationCommand from '../templates/ApplicationCommand.js' 3 | 4 | export default new ApplicationCommand({ 5 | data: new SlashCommandBuilder() 6 | .setName('ping') 7 | .setDescription('Replies pong!'), 8 | async execute(interaction): Promise { 9 | await interaction.reply('Pong!') 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/commands/subautocompletetest.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SlashCommandBuilder, 3 | SlashCommandSubcommandBuilder, 4 | SlashCommandSubcommandGroupBuilder 5 | } from 'discord.js' 6 | import ApplicationCommand from '../templates/ApplicationCommand.js' 7 | 8 | export default new ApplicationCommand({ 9 | data: new SlashCommandBuilder() 10 | .setName('subautocompletetest') 11 | .setDescription('A test for autocomplete subcommands') 12 | .addSubcommandGroup( 13 | new SlashCommandSubcommandGroupBuilder() 14 | .setName('autocompletegroup') 15 | .setDescription('A test for autocomplete subcommand groups') 16 | .addSubcommand( 17 | new SlashCommandSubcommandBuilder() 18 | .setName('guidesub') 19 | .setDescription('Search a guide!') 20 | .addStringOption((option) => 21 | option 22 | .setName('query') 23 | .setDescription('Phrase to search for') 24 | .setAutocomplete(true) 25 | .setRequired(true) 26 | ) 27 | .addStringOption((option) => 28 | option 29 | .setName('version') 30 | .setDescription('Version to search in') 31 | .setAutocomplete(true) 32 | .setRequired(true) 33 | ) 34 | ) 35 | ) 36 | .addSubcommand( 37 | new SlashCommandSubcommandBuilder() 38 | .setName('guide') 39 | .setDescription('Search a guide!') 40 | .addStringOption((option) => 41 | option 42 | .setName('query') 43 | .setDescription('Phrase to search for') 44 | .setAutocomplete(true) 45 | .setRequired(true) 46 | ) 47 | .addStringOption((option) => 48 | option 49 | .setName('version') 50 | .setDescription('Version to search in') 51 | .setAutocomplete(true) 52 | .setRequired(true) 53 | ) 54 | ), 55 | hasSubCommands: true 56 | }) 57 | -------------------------------------------------------------------------------- /src/commands/subcommandtest.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SlashCommandBuilder, 3 | SlashCommandSubcommandBuilder, 4 | SlashCommandSubcommandGroupBuilder 5 | } from 'discord.js' 6 | import ApplicationCommand from '../templates/ApplicationCommand.js' 7 | 8 | export default new ApplicationCommand({ 9 | data: new SlashCommandBuilder() 10 | .setName('subcommandtest') 11 | .setDescription('A test for subcommands') 12 | .addSubcommandGroup( 13 | new SlashCommandSubcommandGroupBuilder() 14 | .setName('grouptest') 15 | .setDescription('A test for subcommand groups') 16 | .addSubcommand( 17 | new SlashCommandSubcommandBuilder() 18 | .setName('pingping') 19 | .setDescription('Replies pongpong!') 20 | ) 21 | ) 22 | .addSubcommand( 23 | new SlashCommandSubcommandBuilder() 24 | .setName('test') 25 | .setDescription('A test subcommand') 26 | ), 27 | hasSubCommands: true 28 | }) 29 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "!" 3 | } 4 | -------------------------------------------------------------------------------- /src/deployGlobalCommands.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import { REST } from '@discordjs/rest' 3 | import { RESTPostAPIApplicationCommandsJSONBody, Routes } from 'discord.js' 4 | import { readdirSync } from 'fs' 5 | import type ApplicationCommand from './templates/ApplicationCommand.js' 6 | const { TOKEN, CLIENT_ID } = process.env 7 | 8 | export default async function deployGlobalCommands() { 9 | const commands: RESTPostAPIApplicationCommandsJSONBody[] = [] 10 | const commandFiles: string[] = readdirSync('./commands').filter( 11 | (file) => file.endsWith('.js') || file.endsWith('.ts') 12 | ) 13 | 14 | for (const file of commandFiles) { 15 | const command: ApplicationCommand = (await import(`./commands/${file}`)) 16 | .default as ApplicationCommand 17 | const commandData = command.data.toJSON() 18 | commands.push(commandData) 19 | } 20 | 21 | const rest = new REST({ version: '10' }).setToken(TOKEN as string) 22 | 23 | try { 24 | console.log('Started refreshing application (/) commands.') 25 | 26 | await rest.put(Routes.applicationCommands(CLIENT_ID as string), { 27 | body: [] 28 | }) 29 | 30 | await rest.put(Routes.applicationCommands(CLIENT_ID as string), { 31 | body: commands 32 | }) 33 | 34 | console.log('Successfully reloaded application (/) commands.') 35 | } catch (error) { 36 | console.error(error) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/events/interactionCreate.ts: -------------------------------------------------------------------------------- 1 | import { BaseInteraction, Events } from 'discord.js' 2 | import type ApplicationCommand from '../templates/ApplicationCommand.js' 3 | import Event from '../templates/Event.js' 4 | 5 | export default new Event({ 6 | name: Events.InteractionCreate, 7 | async execute(interaction: BaseInteraction): Promise { 8 | if (interaction.isChatInputCommand()) { 9 | if (!client.commands.has(interaction.commandName)) return 10 | try { 11 | const command: ApplicationCommand = (await client.commands.get( 12 | interaction.commandName 13 | )) as ApplicationCommand 14 | 15 | if (!command.execute) { 16 | console.error( 17 | `Failed to find execution handler for ${command.data.name}` 18 | ) 19 | await interaction.reply({ 20 | content: 21 | 'There was an error while executing this command!', 22 | ephemeral: true 23 | }) 24 | return 25 | } 26 | 27 | await command.execute(interaction) 28 | } catch (error) { 29 | console.error(error) 30 | await interaction.reply({ 31 | content: 'There was an error while executing this command!', 32 | ephemeral: true 33 | }) 34 | } 35 | } else if (interaction.isAutocomplete()) { 36 | if (!client.commands.has(interaction.commandName)) return 37 | 38 | try { 39 | const command: ApplicationCommand = (await client.commands.get( 40 | interaction.commandName 41 | )) as ApplicationCommand 42 | 43 | if (!command.autocomplete) { 44 | console.error( 45 | `Failed to find autocomplete handler for ${command.data.name}` 46 | ) 47 | await interaction.respond([ 48 | { 49 | name: 'Failed to autocomplete', 50 | value: 'error' 51 | } 52 | ]) 53 | return 54 | } 55 | 56 | await command.autocomplete(interaction) 57 | } catch (error) { 58 | console.error(error) 59 | } 60 | } 61 | } 62 | }) 63 | -------------------------------------------------------------------------------- /src/events/messageCreate.ts: -------------------------------------------------------------------------------- 1 | import type MessageCommand from '../templates/MessageCommand.js' 2 | import Event from '../templates/Event.js' 3 | import { Events, Message } from 'discord.js' 4 | import { default as config } from '../config.json' assert { type: 'json' } 5 | 6 | export default new Event({ 7 | name: Events.MessageCreate, 8 | async execute(message: Message): Promise { 9 | // ! Message content is a priviliged intent now! 10 | 11 | // Handles non-slash commands, only recommended for deploy commands 12 | 13 | // filters out bots and non-prefixed messages 14 | if (!message.content.startsWith(config.prefix) || message.author.bot) 15 | return 16 | 17 | // fetches the application owner for the bot 18 | if (!client.application?.owner) await client.application?.fetch() 19 | 20 | // get the arguments and the actual command name for the inputted command 21 | const args = message.content 22 | .slice(config.prefix.length) 23 | .trim() 24 | .split(/ +/) 25 | const commandName = (args.shift()).toLowerCase() 26 | 27 | const command = 28 | (client.msgCommands.get(commandName) as MessageCommand) || 29 | (client.msgCommands.find( 30 | (cmd: MessageCommand): boolean => 31 | cmd.aliases && cmd.aliases.includes(commandName) 32 | ) as MessageCommand) 33 | 34 | // dynamic command handling 35 | if (!command) return 36 | 37 | try { 38 | await command.execute(message, args) 39 | } catch (error) { 40 | console.error(error) 41 | } 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /src/events/ready.ts: -------------------------------------------------------------------------------- 1 | import { Events } from 'discord.js' 2 | import Event from '../templates/Event.js' 3 | 4 | export default new Event({ 5 | name: Events.ClientReady, 6 | once: true, 7 | execute(): void { 8 | // Runs when the bot logs in 9 | console.log(`Logged in as ${client.user?.tag as string}!`) 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | import { Client, Collection } from 'discord.js' 3 | import ApplicationCommand from '../templates/ApplicationCommand' 4 | import MessageCommand from '../templates/MessageCommand' 5 | 6 | interface DiscordClient extends Client { 7 | commands: Collection 8 | msgCommands: Collection 9 | } 10 | 11 | declare global { 12 | var client: DiscordClient 13 | 14 | type PartialBy = Omit & Partial> 15 | } 16 | 17 | export {} 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import 'dotenv/config' 3 | 4 | import { Client, GatewayIntentBits, Collection, Partials } from 'discord.js' 5 | import { readdirSync } from 'fs' 6 | import type ApplicationCommand from './templates/ApplicationCommand.js' 7 | import type Event from './templates/Event.js' 8 | import type MessageCommand from './templates/MessageCommand.js' 9 | import deployGlobalCommands from './deployGlobalCommands.js' 10 | const { TOKEN } = process.env 11 | 12 | await deployGlobalCommands() 13 | 14 | // Discord client object 15 | global.client = Object.assign( 16 | new Client({ 17 | intents: [ 18 | GatewayIntentBits.Guilds, 19 | GatewayIntentBits.GuildMessages, 20 | GatewayIntentBits.DirectMessages, 21 | GatewayIntentBits.MessageContent 22 | ], 23 | partials: [Partials.Channel] 24 | }), 25 | { 26 | commands: new Collection(), 27 | msgCommands: new Collection() 28 | } 29 | ) 30 | 31 | // Set each command in the commands folder as a command in the client.commands collection 32 | const commandFiles: string[] = readdirSync('./commands').filter( 33 | (file) => file.endsWith('.js') || file.endsWith('.ts') 34 | ) 35 | for (const file of commandFiles) { 36 | const command: ApplicationCommand = (await import(`./commands/${file}`)) 37 | .default as ApplicationCommand 38 | client.commands.set(command.data.name, command) 39 | } 40 | 41 | const msgCommandFiles: string[] = readdirSync('./messageCommands').filter( 42 | (file) => file.endsWith('.js') || file.endsWith('.ts') 43 | ) 44 | for (const file of msgCommandFiles) { 45 | const command: MessageCommand = (await import(`./messageCommands/${file}`)) 46 | .default as MessageCommand 47 | client.msgCommands.set(command.name, command) 48 | } 49 | 50 | // Event handling 51 | const eventFiles: string[] = readdirSync('./events').filter( 52 | (file) => file.endsWith('.js') || file.endsWith('.ts') 53 | ) 54 | 55 | for (const file of eventFiles) { 56 | const event: Event = (await import(`./events/${file}`)).default as Event 57 | if (event.once) { 58 | client.once(event.name, (...args) => event.execute(...args)) 59 | } else { 60 | client.on(event.name, (...args) => event.execute(...args)) 61 | } 62 | } 63 | 64 | await client.login(TOKEN) 65 | -------------------------------------------------------------------------------- /src/messageCommands/deploy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ 2 | import { readdirSync } from 'fs' 3 | import type ApplicationCommand from '../templates/ApplicationCommand.js' 4 | import MessageCommand from '../templates/MessageCommand.js' 5 | import { REST } from '@discordjs/rest' 6 | import { RESTPostAPIApplicationCommandsJSONBody, Routes } from 'discord.js' 7 | import { default as config } from '../config.json' assert { type: 'json' } 8 | 9 | const { TOKEN, CLIENT_ID, OWNER_ID } = process.env as { 10 | TOKEN: string 11 | CLIENT_ID: string 12 | OWNER_ID: string 13 | } 14 | 15 | export default new MessageCommand({ 16 | name: 'deploy', 17 | description: 'Deploys the slash commands', 18 | async execute(message, args): Promise { 19 | if (message.author.id !== OWNER_ID) return 20 | 21 | if (!args[0]) { 22 | await message.reply({ 23 | content: `Incorrect number of arguments! The correct format is \`${config.prefix}deploy \`` 24 | }) 25 | return 26 | } 27 | 28 | console.log(`Undeploying commands by ${message.author.tag}!}`) 29 | 30 | if (args[0].toLowerCase() === 'global') { 31 | // global deployment 32 | 33 | const commands: RESTPostAPIApplicationCommandsJSONBody[] = [] 34 | const commandFiles: string[] = readdirSync('./commands').filter( 35 | (file) => file.endsWith('.js') || file.endsWith('.ts') 36 | ) 37 | 38 | for (const file of commandFiles) { 39 | const command: ApplicationCommand = ( 40 | await import(`../commands/${file}`) 41 | ).default as ApplicationCommand 42 | const commandData = command.data.toJSON() 43 | commands.push(commandData) 44 | } 45 | 46 | const rest = new REST({ version: '10' }).setToken(TOKEN) 47 | 48 | try { 49 | console.log('Started refreshing application (/) commands.') 50 | 51 | await rest.put(Routes.applicationCommands(CLIENT_ID), { 52 | body: commands 53 | }) 54 | 55 | console.log('Successfully reloaded application (/) commands.') 56 | } catch (error) { 57 | console.error(error) 58 | } 59 | 60 | await message.reply({ content: 'Deploying!' }) 61 | } else if (args[0].toLowerCase() === 'guild') { 62 | // guild deployment 63 | 64 | const commands: RESTPostAPIApplicationCommandsJSONBody[] = [] 65 | const commandFiles: string[] = readdirSync('./commands').filter( 66 | (file) => file.endsWith('.js') || file.endsWith('.ts') 67 | ) 68 | 69 | for (const file of commandFiles) { 70 | const command: ApplicationCommand = ( 71 | await import(`../commands/${file}`) 72 | ).default as ApplicationCommand 73 | const commandData = command.data.toJSON() 74 | commands.push(commandData) 75 | } 76 | 77 | const rest = new REST({ version: '10' }).setToken(TOKEN) 78 | 79 | try { 80 | console.log('Started refreshing application (/) commands.') 81 | 82 | await rest.put( 83 | Routes.applicationGuildCommands( 84 | CLIENT_ID, 85 | message.guild?.id as string 86 | ), 87 | { 88 | body: commands 89 | } 90 | ) 91 | 92 | console.log('Successfully reloaded application (/) commands.') 93 | } catch (error) { 94 | console.error(error) 95 | } 96 | 97 | await message.reply({ content: 'Deploying!' }) 98 | } 99 | } 100 | }) 101 | -------------------------------------------------------------------------------- /src/messageCommands/message-command-template.js.template: -------------------------------------------------------------------------------- 1 | import MessageCommand from '../templates/MessageCommand' 2 | 3 | export default new MessageCommand({ 4 | name: 'ping', // command name 5 | description: 'Ping!', // command description 6 | async execute(message, args) { 7 | // code to run on command execution 8 | // for example: 9 | await message.channel.send('Pong.') 10 | }, 11 | }) -------------------------------------------------------------------------------- /src/messageCommands/undeploy.ts: -------------------------------------------------------------------------------- 1 | import MessageCommand from '../templates/MessageCommand.js' 2 | import { default as config } from '../config.json' assert { type: 'json' } 3 | const { OWNER_ID } = process.env 4 | 5 | export default new MessageCommand({ 6 | name: 'undeploy', 7 | description: 'Undeploys the slash commands', 8 | async execute(message, args): Promise { 9 | if (message.author.id !== OWNER_ID) return 10 | 11 | if (!args[0]) { 12 | await message.reply( 13 | `Incorrect number of arguments! The correct format is \`${config.prefix}undeploy \`` 14 | ) 15 | return 16 | } 17 | 18 | if (args[0].toLowerCase() === 'global') { 19 | // global undeployment 20 | 21 | // undeploy the commands 22 | await client.application?.commands.set([]) 23 | 24 | await message.reply({ content: 'Undeploying!' }) 25 | } else if (args[0].toLowerCase() === 'guild') { 26 | // guild deployment 27 | 28 | // undeploy the commands 29 | await message.guild?.commands.set([]) 30 | 31 | await message.reply({ content: 'Undeploying!' }) 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/subCommands/sub-command-template.ts.template: -------------------------------------------------------------------------------- 1 | import type { ChatInputCommandInteraction } from 'discord.js' 2 | import SubCommand from '../../templates/SubCommand.js' 3 | 4 | export default new SubCommand({ 5 | async execute(interaction: ChatInputCommandInteraction): Promise { 6 | await interaction.reply('Pong!') 7 | } 8 | }) -------------------------------------------------------------------------------- /src/subCommands/subautocompletetest/autocompletegroup/guidesub.ts: -------------------------------------------------------------------------------- 1 | import SubCommand from '../../../templates/SubCommand.js' 2 | 3 | export default new SubCommand({ 4 | async autocomplete(interaction) { 5 | const focusedOption = interaction.options.getFocused(true) 6 | let choices: string[] = [] 7 | 8 | if (focusedOption.name === 'query') { 9 | choices = [ 10 | 'Popular Topics: Threads', 11 | 'Sharding: Getting started', 12 | 'Library: Voice Connections', 13 | 'Interactions: Replying to slash commands', 14 | 'Popular Topics: Embed preview' 15 | ] 16 | } 17 | 18 | if (focusedOption.name === 'version') { 19 | choices = ['v9', 'v11', 'v12', 'v13', 'v14'] 20 | } 21 | 22 | const filtered = choices.filter((choice) => 23 | choice.startsWith(focusedOption.value) 24 | ) 25 | await interaction.respond( 26 | filtered.map((choice) => ({ name: choice, value: choice })) 27 | ) 28 | }, 29 | async execute(interaction) { 30 | const query = interaction.options.getString('query') 31 | const version = interaction.options.getString('version') 32 | await interaction.reply(`You searched for ${query} in ${version}!`) 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/subCommands/subautocompletetest/guide.ts: -------------------------------------------------------------------------------- 1 | import SubCommand from '../../templates/SubCommand.js' 2 | 3 | export default new SubCommand({ 4 | async autocomplete(interaction) { 5 | const focusedOption = interaction.options.getFocused(true) 6 | let choices: string[] = [] 7 | 8 | if (focusedOption.name === 'query') { 9 | choices = [ 10 | 'Popular Topics: Threads', 11 | 'Sharding: Getting started', 12 | 'Library: Voice Connections', 13 | 'Interactions: Replying to slash commands', 14 | 'Popular Topics: Embed preview' 15 | ] 16 | } 17 | 18 | if (focusedOption.name === 'version') { 19 | choices = ['v9', 'v11', 'v12', 'v13', 'v14'] 20 | } 21 | 22 | const filtered = choices.filter((choice) => 23 | choice.startsWith(focusedOption.value) 24 | ) 25 | await interaction.respond( 26 | filtered.map((choice) => ({ name: choice, value: choice })) 27 | ) 28 | }, 29 | async execute(interaction) { 30 | const query = interaction.options.getString('query') 31 | const version = interaction.options.getString('version') 32 | await interaction.reply(`You searched for ${query} in ${version}!`) 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/subCommands/subcommandtest/grouptest/pingping.ts: -------------------------------------------------------------------------------- 1 | import type { ChatInputCommandInteraction } from 'discord.js' 2 | import SubCommand from '../../../templates/SubCommand.js' 3 | 4 | export default new SubCommand({ 5 | async execute(interaction: ChatInputCommandInteraction): Promise { 6 | await interaction.reply('PongPong!') 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/subCommands/subcommandtest/test.ts: -------------------------------------------------------------------------------- 1 | import type { ChatInputCommandInteraction } from 'discord.js' 2 | import SubCommand from '../../templates/SubCommand.js' 3 | 4 | export default new SubCommand({ 5 | async execute(interaction: ChatInputCommandInteraction): Promise { 6 | await interaction.reply('The command works!') 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/templates/ApplicationCommand.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import type { 3 | AutocompleteInteraction, 4 | ChatInputCommandInteraction, 5 | ContextMenuCommandBuilder, 6 | SlashCommandBuilder, 7 | SlashCommandSubcommandsOnlyBuilder 8 | } from 'discord.js' 9 | import type SubCommand from './SubCommand.js' 10 | 11 | /** 12 | * Represents an Application Command 13 | */ 14 | export default class ApplicationCommand { 15 | data: 16 | | SlashCommandBuilder 17 | | ContextMenuCommandBuilder 18 | | SlashCommandSubcommandsOnlyBuilder 19 | hasSubCommands: boolean 20 | execute?: (interaction: ChatInputCommandInteraction) => Promise | void 21 | autocomplete?: ( 22 | interaction: AutocompleteInteraction 23 | ) => Promise | void 24 | 25 | /** 26 | * @param {{ 27 | * data: SlashCommandBuilder | ContextMenuCommandBuilder | SlashCommandSubcommandsOnlyBuilder 28 | * hasSubCommands?: boolean 29 | * execute?: (interaction: ChatInputCommandInteraction) => Promise | void 30 | * autocomplete?: (interaction: AutocompleteInteraction) => Promise | void 31 | * }} options - The options for the slash command 32 | */ 33 | constructor(options: { 34 | data: 35 | | SlashCommandBuilder 36 | | ContextMenuCommandBuilder 37 | | SlashCommandSubcommandsOnlyBuilder 38 | hasSubCommands?: boolean 39 | execute?: ( 40 | interaction: ChatInputCommandInteraction 41 | ) => Promise | void 42 | autocomplete?: ( 43 | interaction: AutocompleteInteraction 44 | ) => Promise | void 45 | }) { 46 | if (options.hasSubCommands) { 47 | this.execute = async (interaction: ChatInputCommandInteraction) => { 48 | const subCommandGroup = interaction.options.getSubcommandGroup() 49 | const commandName = interaction.options.getSubcommand() 50 | 51 | if (!commandName) { 52 | await interaction.reply({ 53 | content: "I couldn't understand that command!", 54 | ephemeral: true 55 | }) 56 | } else { 57 | try { 58 | const command = ( 59 | await import( 60 | `../subCommands/${this.data.name}/${ 61 | subCommandGroup ? `${subCommandGroup}/` : '' 62 | }${commandName}.js` 63 | ) 64 | ).default as SubCommand 65 | await command.execute(interaction) 66 | } catch (error) { 67 | console.error(error) 68 | await interaction.reply({ 69 | content: 70 | 'An error occured when attempting to execute that command!', 71 | ephemeral: true 72 | }) 73 | } 74 | } 75 | } 76 | 77 | this.autocomplete = async ( 78 | interaction: AutocompleteInteraction 79 | ) => { 80 | const subCommandGroup = interaction.options.getSubcommandGroup() 81 | const subCommandName = interaction.options.getSubcommand() 82 | 83 | if (subCommandGroup || subCommandName) { 84 | try { 85 | const subCommand = ( 86 | await import( 87 | `../subCommands/${this.data.name}/${ 88 | subCommandGroup ? `${subCommandGroup}/` : '' 89 | }${subCommandName}.js` 90 | ) 91 | ).default as SubCommand 92 | if (subCommand.autocomplete) { 93 | await subCommand.autocomplete(interaction) 94 | } 95 | } catch (error) { 96 | console.error(error) 97 | await interaction.respond([ 98 | { 99 | name: 'Failed to autocomplete', 100 | value: 'error' 101 | } 102 | ]) 103 | } 104 | } 105 | } 106 | } else if (options.execute) { 107 | this.execute = options.execute 108 | } else if (options.autocomplete) { 109 | this.autocomplete = options.autocomplete 110 | } else { 111 | throw new Error('No execute function provided') 112 | } 113 | 114 | this.data = options.data 115 | if (!options.hasSubCommands) { 116 | this.autocomplete = options.autocomplete 117 | } 118 | this.hasSubCommands = options.hasSubCommands ?? false 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/templates/BaseCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base class for all commands 3 | * @abstract 4 | */ 5 | export default abstract class BaseCommand { 6 | name: string 7 | description: string 8 | execute: (...args: any) => Promise | void 9 | 10 | /** 11 | * @param {{ 12 | * name: string, 13 | * description: string, 14 | * execute: (...args: any) => Promise | void 15 | * }} object 16 | */ 17 | constructor(object: { 18 | name: string 19 | description: string 20 | execute: (...args: any) => Promise | void 21 | }) { 22 | this.name = object.name 23 | this.description = object.description 24 | this.execute = object.execute 25 | } 26 | 27 | /** 28 | * @param {string} name - The name 29 | */ 30 | setName(name: string): void { 31 | this.name = name 32 | } 33 | 34 | /** 35 | * @param {string} description - The description 36 | */ 37 | setDescription(description: string): void { 38 | this.description = description 39 | } 40 | 41 | /** 42 | * @param {(...args: any) => Promise | void} executeFunction - The function 43 | */ 44 | setExecute(executeFunction: (...args: any) => Promise | void): void { 45 | this.execute = executeFunction 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/templates/Event.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an Event 3 | */ 4 | export default class Event { 5 | name: string 6 | once: boolean 7 | execute: (...args: any) => Promise | void 8 | 9 | /** 10 | * @param {{ 11 | * name: string, 12 | * once: boolean, 13 | * execute: (...args: any) => Promise | void 14 | * }} object 15 | */ 16 | constructor(object: { 17 | name: string 18 | once?: boolean 19 | execute: (...args: any) => Promise | void 20 | }) { 21 | this.name = object.name 22 | this.once = object.once ?? false 23 | this.execute = object.execute 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/templates/MessageCommand.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from 'discord.js' 2 | import BaseCommand from './BaseCommand.js' 3 | 4 | /* 5 | * Represents a Message Command 6 | */ 7 | export default class MessageCommand extends BaseCommand { 8 | aliases: string[] 9 | override execute: (message: Message, args: string[]) => Promise | void 10 | 11 | /** 12 | * @param {{ 13 | * name: string, 14 | * description: string, 15 | * aliases?: string[], 16 | * execute: (message: Message, args: string[]) => Promise | void 17 | * }} options - The options for the message command 18 | */ 19 | constructor(options: { 20 | name: string 21 | description: string 22 | aliases?: string[] 23 | execute: (message: Message, args: string[]) => Promise | void 24 | }) { 25 | super(options) 26 | this.execute = options.execute 27 | this.aliases = options.aliases ?? [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/templates/SubCommand.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ChatInputCommandInteraction, 3 | AutocompleteInteraction 4 | } from 'discord.js' 5 | 6 | /** 7 | * Represents a SubCommand 8 | */ 9 | export default class SubCommand { 10 | execute: (interaction: ChatInputCommandInteraction) => Promise | void 11 | autocomplete?: ( 12 | interaction: AutocompleteInteraction 13 | ) => Promise | void 14 | 15 | /** 16 | * 17 | * @param {{ 18 | * execute: (interaction: ChatInputCommandInteraction) => Promise | void 19 | * autocomplete?: (interaction: AutocompleteInteraction) => Promise | void 20 | * }} options - The options for the subcommand 21 | */ 22 | constructor(options: { 23 | execute: ( 24 | interaction: ChatInputCommandInteraction 25 | ) => Promise | void 26 | autocomplete?: ( 27 | interaction: AutocompleteInteraction 28 | ) => Promise | void 29 | }) { 30 | this.execute = options.execute 31 | if (options.autocomplete) { 32 | this.autocomplete = options.autocomplete 33 | } 34 | } 35 | 36 | /** 37 | * Set the autocomplete function of the subcommand 38 | * @param {(interaction: AutocompleteInteraction) => Promise | void} autocompleteFunction - The function 39 | */ 40 | setAutocomplete( 41 | autocompleteFunction: ( 42 | interaction: AutocompleteInteraction 43 | ) => Promise | void 44 | ): void { 45 | this.autocomplete = autocompleteFunction 46 | } 47 | 48 | /** 49 | * Set the execute function of the subcommand 50 | * @param {(interaction: ChatInputCommandInteraction) => Promise | void} executeFunction - The function 51 | */ 52 | setExecute( 53 | executeFunction: ( 54 | interaction: ChatInputCommandInteraction 55 | ) => Promise | void 56 | ): void { 57 | this.execute = executeFunction 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "outDir": "./dist", 6 | "module": "ESNext", 7 | "esModuleInterop": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------