-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(create-discord-bot): Add prompts, command handler and deployment…
… script (#9570) * feat(create-discord-bot): Add prompts, command handler and deployment script * fix: revert package template name * chore: make requested changes * chore: make requested changes * fix: remove uneeded listeners * fix: use `0` for eslint * fix: remaining requested changes * chore: requested changes * fix: remove redundant call
- Loading branch information
1 parent
e5effb6
commit 84f1b18
Showing
25 changed files
with
510 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,81 @@ | ||
#!/usr/bin/env node | ||
|
||
// eslint-disable-next-line n/shebang | ||
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; | ||
import { cp, stat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; | ||
import path from 'node:path'; | ||
import process from 'node:process'; | ||
import { URL } from 'node:url'; | ||
import chalk from 'chalk'; | ||
import { program } from 'commander'; | ||
import validateProjectName from 'validate-npm-package-name'; | ||
import { install, resolvePackageManager } from './helpers/packageManager.js'; | ||
import { GUIDE_URL } from './util/constants.js'; | ||
|
||
program | ||
.description('Create a basic discord.js bot.') | ||
.option('--typescript', 'Whether to use the TypeScript template.') | ||
.argument('<directory>', 'The directory where this will be created.') | ||
.parse(); | ||
|
||
const { typescript } = program.opts(); | ||
const [directory] = program.args; | ||
|
||
if (!directory) { | ||
console.error(chalk.red('Please specify the project directory.')); | ||
process.exit(1); | ||
interface Options { | ||
directory: string; | ||
javascript?: boolean; | ||
typescript?: boolean; | ||
} | ||
|
||
const root = path.resolve(directory); | ||
const directoryName = path.basename(root); | ||
export async function createDiscordBot({ typescript, javascript, directory }: Options) { | ||
if (!directory) { | ||
console.error(chalk.red('Please specify the project directory.')); | ||
process.exit(1); | ||
} | ||
|
||
if (existsSync(root) && readdirSync(root).length > 0) { | ||
console.error(chalk.red(`The directory ${chalk.yellow(`"${directoryName}"`)} is not empty.`)); | ||
console.error(chalk.red(`Please specify an empty directory.`)); | ||
process.exit(1); | ||
} | ||
const root = path.resolve(directory); | ||
const directoryName = path.basename(root); | ||
|
||
// We'll use the directory name as the project name. Check npm name validity. | ||
const validationResult = validateProjectName(directoryName); | ||
const directoryStats = await stat(root).catch(async (error) => { | ||
// Create a new directory if the specified one does not exist. | ||
if (error.code === 'ENOENT') { | ||
await mkdir(root, { recursive: true }); | ||
return stat(root); | ||
} | ||
|
||
if (!validationResult.validForNewPackages) { | ||
console.error( | ||
chalk.red( | ||
`Cannot create a project named ${chalk.yellow(`"${directoryName}"`)} due to npm naming restrictions.\n\nErrors:`, | ||
), | ||
); | ||
throw error; | ||
}); | ||
|
||
for (const error of [...(validationResult.errors ?? []), ...(validationResult.warnings ?? [])]) { | ||
console.error(chalk.red(`- ${error}`)); | ||
// If a directory exists and it's not empty, throw an error. | ||
if (directoryStats.isDirectory() && (await readdir(root)).length > 0) { | ||
console.error(chalk.red(`The directory ${chalk.yellow(`"${directoryName}"`)} is not empty.`)); | ||
console.error(chalk.red(`Please specify an empty directory.`)); | ||
process.exit(1); | ||
} | ||
|
||
console.error(chalk.red('\nSee https://docs.npmjs.com/cli/configuring-npm/package-json for more details.')); | ||
process.exit(1); | ||
} | ||
// We'll use the directory name as the project name. Check npm name validity. | ||
const validationResult = validateProjectName(directoryName); | ||
|
||
if (!existsSync(root)) { | ||
mkdirSync(root, { recursive: true }); | ||
} | ||
if (!validationResult.validForNewPackages) { | ||
console.error( | ||
chalk.red( | ||
`Cannot create a project named ${chalk.yellow( | ||
`"${directoryName}"`, | ||
)} due to npm naming restrictions.\n\nErrors:`, | ||
), | ||
); | ||
|
||
for (const error of [...(validationResult.errors ?? []), ...(validationResult.warnings ?? [])]) { | ||
console.error(chalk.red(`- ${error}`)); | ||
} | ||
|
||
console.error(chalk.red('\nSee https://docs.npmjs.com/cli/configuring-npm/package-json for more details.')); | ||
process.exit(1); | ||
} | ||
|
||
console.log(`Creating ${directoryName} in ${chalk.green(root)}.`); | ||
cpSync(new URL(`../template/${typescript ? 'TypeScript' : 'JavaScript'}`, import.meta.url), root, { recursive: true }); | ||
console.log(`Creating ${directoryName} in ${chalk.green(root)}.`); | ||
await cp(new URL(`../template/${typescript ? 'TypeScript' : 'JavaScript'}`, import.meta.url), root, { | ||
recursive: true, | ||
}); | ||
|
||
process.chdir(root); | ||
process.chdir(root); | ||
|
||
const newPackageJSON = readFileSync('./package.json', { encoding: 'utf8' }).replace('[REPLACE-NAME]', directoryName); | ||
writeFileSync('./package.json', newPackageJSON); | ||
const newPackageJSON = await readFile('./package.json', { encoding: 'utf8' }).then((str) => | ||
str.replace('[REPLACE-NAME]', directoryName), | ||
); | ||
await writeFile('./package.json', newPackageJSON); | ||
|
||
const packageManager = resolvePackageManager(); | ||
install(packageManager); | ||
console.log(chalk.green('All done! Be sure to read through the discord.js guide for help on your journey.')); | ||
console.log(`Link: ${chalk.cyan(GUIDE_URL)}`); | ||
const packageManager = resolvePackageManager(); | ||
install(packageManager); | ||
console.log(chalk.green('All done! Be sure to read through the discord.js guide for help on your journey.')); | ||
console.log(`Link: ${chalk.cyan(GUIDE_URL)}`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { program } from 'commander'; | ||
import prompts from 'prompts'; | ||
import { createDiscordBot } from './create-discord-bot.js'; | ||
|
||
program | ||
.description('Create a basic discord.js bot.') | ||
.option('--directory', 'The directory where this will be created.') | ||
.option('--typescript', 'Whether to use the TypeScript template.') | ||
.option('--javascript', 'Whether to use the JavaScript template.') | ||
.parse(); | ||
|
||
let { typescript, javascript, directory } = program.opts(); | ||
|
||
if (!directory) { | ||
directory = ( | ||
await prompts({ | ||
type: 'text', | ||
name: 'directory', | ||
initial: 'my-bot', | ||
message: 'What is the name of the directory you want to create this project in?', | ||
}) | ||
).directory; | ||
} | ||
|
||
if (typescript === undefined && javascript === undefined) { | ||
const { useTypescript } = await prompts({ | ||
type: 'toggle', | ||
name: 'useTypescript', | ||
message: 'Do you want to use TypeScript?', | ||
initial: true, | ||
active: 'Yes', | ||
inactive: 'No', | ||
}); | ||
|
||
typescript = useTypescript; | ||
javascript = !useTypescript; | ||
} | ||
|
||
await createDiscordBot({ typescript, javascript, directory }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
DISCORD_TOKEN= | ||
APPLICATION_ID= |
7 changes: 6 additions & 1 deletion
7
packages/create-discord-bot/template/JavaScript/.eslintrc.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
{ | ||
"root": true, | ||
"extends": ["neon/common", "neon/node", "neon/prettier"] | ||
"extends": ["neon/common", "neon/node", "neon/prettier"], | ||
"rules": { | ||
"jsdoc/valid-types": 0, | ||
"jsdoc/check-tag-names": 0, | ||
"jsdoc/no-undefined-types": 0 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
packages/create-discord-bot/template/JavaScript/src/commands/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* Defines the structure of a command. | ||
* | ||
* @typedef {object} Command | ||
* @property {import('discord.js').RESTPostAPIApplicationCommandsJSONBody} data The data for the command | ||
* @property {(interaction: import('discord.js').CommandInteraction) => Promise<void> | void} execute The function to execute when the command is called | ||
*/ | ||
|
||
/** | ||
* Defines the predicate to check if an object is a valid Command type. | ||
* | ||
* @type {import('../util/loaders.js').StructurePredicate<Command>} | ||
* @returns {structure is Command} | ||
*/ | ||
export const predicate = (structure) => | ||
Boolean(structure) && | ||
typeof structure === 'object' && | ||
'data' in structure && | ||
'execute' in structure && | ||
typeof structure.data === 'object' && | ||
typeof structure.execute === 'function'; |
10 changes: 10 additions & 0 deletions
10
packages/create-discord-bot/template/JavaScript/src/commands/ping.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** @type {import('./index.js').Command} */ | ||
export default { | ||
data: { | ||
name: 'ping', | ||
description: 'Ping!', | ||
}, | ||
async execute(interaction) { | ||
await interaction.reply('Pong!'); | ||
}, | ||
}; |
23 changes: 23 additions & 0 deletions
23
packages/create-discord-bot/template/JavaScript/src/events/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** | ||
* Defines the structure of an event. | ||
* | ||
* @template {keyof import('discord.js').ClientEvents} [T=keyof import('discord.js').ClientEvents] | ||
* @typedef {object} Event | ||
* @property {(...parameters: import('discord.js').ClientEvents[T]) => Promise<void> | void} execute The function to execute the command | ||
* @property {T} name The name of the event to listen to | ||
* @property {boolean} [once] Whether or not the event should only be listened to once | ||
*/ | ||
|
||
/** | ||
* Defines the predicate to check if an object is a valid Event type. | ||
* | ||
* @type {import('../util/loaders').StructurePredicate<Event>} | ||
* @returns {structure is Event} | ||
*/ | ||
export const predicate = (structure) => | ||
Boolean(structure) && | ||
typeof structure === 'object' && | ||
'name' in structure && | ||
'execute' in structure && | ||
typeof structure.name === 'string' && | ||
typeof structure.execute === 'function'; |
1 change: 1 addition & 0 deletions
1
packages/create-discord-bot/template/JavaScript/src/events/ready.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 10 additions & 7 deletions
17
packages/create-discord-bot/template/JavaScript/src/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,17 @@ | ||
import { readdir } from 'node:fs/promises'; | ||
import { URL } from 'node:url'; | ||
import { Client, GatewayIntentBits } from 'discord.js'; | ||
import { loadCommands, loadEvents } from './util/loaders.js'; | ||
import { registerEvents } from './util/registerEvents.js'; | ||
|
||
// Initialize the client | ||
const client = new Client({ intents: [GatewayIntentBits.Guilds] }); | ||
const eventsPath = new URL('events/', import.meta.url); | ||
const eventFiles = await readdir(eventsPath).then((files) => files.filter((file) => file.endsWith('.js'))); | ||
|
||
for (const file of eventFiles) { | ||
const event = (await import(new URL(file, eventsPath).toString())).default; | ||
client[event.once ? 'once' : 'on'](event.name, async (...args) => event.execute(...args)); | ||
} | ||
// Load the events and commands | ||
const events = await loadEvents(new URL('events/', import.meta.url)); | ||
const commands = await loadCommands(new URL('commands/', import.meta.url)); | ||
|
||
// Register the event handlers | ||
registerEvents(commands, events, client); | ||
|
||
// Login to the client | ||
void client.login(); |
15 changes: 15 additions & 0 deletions
15
packages/create-discord-bot/template/JavaScript/src/util/deploy.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import process from 'node:process'; | ||
import { URL } from 'node:url'; | ||
import { API } from '@discordjs/core/http-only'; | ||
import { REST } from 'discord.js'; | ||
import { loadCommands } from './loaders.js'; | ||
|
||
const commands = await loadCommands(new URL('commands/', import.meta.url)); | ||
const commandData = [...commands.values()].map((command) => command.data); | ||
|
||
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN); | ||
const api = new API(rest); | ||
|
||
const result = await api.applicationCommands.bulkOverwriteGlobalCommands(process.env.APPLICATION_ID, commandData); | ||
|
||
console.log(`Successfully registered ${result.length} commands.`); |
Oops, something went wrong.