Skip to content

Commit

Permalink
feat(create-discord-bot): Add prompts, command handler and deployment…
Browse files Browse the repository at this point in the history
… 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
suneettipirneni committed Jul 16, 2023
1 parent e5effb6 commit 84f1b18
Show file tree
Hide file tree
Showing 25 changed files with 510 additions and 71 deletions.
4 changes: 3 additions & 1 deletion packages/create-discord-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/create-discord-bot/*'",
"release": "cliff-jumper"
},
"bin": "./dist/create-discord-bot.mjs",
"bin": "./dist/index.mjs",
"directories": {
"lib": "src"
},
Expand Down Expand Up @@ -47,12 +47,14 @@
"dependencies": {
"chalk": "^5.2.0",
"commander": "^10.0.1",
"prompts": "^2.4.2",
"validate-npm-package-name": "^5.0.0"
},
"devDependencies": {
"@favware/cliff-jumper": "^2.0.0",
"@microsoft/api-extractor": "^7.35.0",
"@types/node": "16.18.32",
"@types/prompts": "^2.4.4",
"@types/validate-npm-package-name": "^4.0.0",
"@vitest/coverage-c8": "^0.31.1",
"cross-env": "^7.0.3",
Expand Down
103 changes: 57 additions & 46 deletions packages/create-discord-bot/src/create-discord-bot.ts
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)}`);
}
39 changes: 39 additions & 0 deletions packages/create-discord-bot/src/index.ts
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 });
1 change: 1 addition & 0 deletions packages/create-discord-bot/template/JavaScript/.env
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DISCORD_TOKEN=
APPLICATION_ID=
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
}
}
4 changes: 3 additions & 1 deletion packages/create-discord-bot/template/JavaScript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"scripts": {
"lint": "prettier --check . && eslint src --ext .js,.cjs --format=pretty",
"format": "prettier --write . && eslint src --ext .js,.cjs --fix --format=pretty",
"start": "node --require dotenv/config src/index.js"
"start": "node --require dotenv/config src/index.js",
"deploy": "node --require dotenv/config src/util/deploy.js"
},
"dependencies": {
"@discordjs/core": "^0.6.0",
"discord.js": "^14.11.0",
"dotenv": "^16.0.3"
},
Expand Down
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';
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!');
},
};
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';
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Events } from 'discord.js';

/** @type {import('./index.js').Event<Events.ClientReady>} */
export default {
name: Events.ClientReady,
once: true,
Expand Down
17 changes: 10 additions & 7 deletions packages/create-discord-bot/template/JavaScript/src/index.js
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 packages/create-discord-bot/template/JavaScript/src/util/deploy.js
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.`);
Loading

0 comments on commit 84f1b18

Please sign in to comment.