Anatomy of a plugin
The manifest holds six fields: name, version, author, description, main and enabled. The file named by main must be a .js or .mjs file inside the plugin folder. A .ts entry point is rejected, because the bot runs compiled JavaScript. If you prefer TypeScript, compile it to .js before shipping.
The entry file default-exports one object. Everything except name is optional, so a plugin may add only commands, only events, or neither. A plugin that adds one command is a manifest, an index file and a single command definition, and the loader picks it up from the plugins folder on start.
plugins/
my_plugin/
manifest.json
index.js
// manifest.json
{
"name": "my_plugin",
"version": "1.0.0",
"author": "You",
"description": "What it does",
"main": "index.js",
"enabled": true
}
// index.js
import { SlashCommandBuilder } from 'discord.js';
let ctx;
export default {
name: 'my_plugin',
version: '1.0.0',
commands: [
{
data: new SlashCommandBuilder().setName('ping').setDescription('Reply with pong'),
category: 'plugin',
execute: async (interaction) => {
await interaction.reply({ embeds: [ctx.embed.success('Pong', 'Still here.')] });
}
}
],
events: [],
onLoad: (context) => { ctx = context; },
onUnload: () => {}
}; What a plugin gets handed
onLoad and onUnload receive a plugin context rather than the bare client. It carries the discord.js client, the embed helper the rest of the bot uses, a namespaced logger, and the translation function t(key, guildId) so a plugin can speak all nine interface languages without shipping its own i18n.
It also carries a private data directory plus readData and writeData helpers, so a plugin that needs to remember a counter or a list does not have to bring a database or reach into the bot storage. Do not import anything from src/ or dist/ - those paths differ between development and a deployed build, and the context object exists so you never need to.
- client - the discord.js client
- embed - the same embed helper the core uses
- logger - info, warn, error and debug, namespaced to the plugin
- t(key, guildId, vars) - translated strings
- dataDir, readData, writeData - the plugin private storage
Loading and unloading
Plugins load at startup, in alphabetical folder order, before the bot logs in. A bad manifest is skipped with a warning and the other plugins still load. On shutdown or unload, onUnload runs and the plugin listeners and commands are removed; a reload re-imports the file with a fresh cache key, so it picks up your edits without leaving duplicate listeners behind.
A plugin can also ship disabled: set enabled to false in the manifest and the loader will skip it until you say otherwise. The bundled sample_plugin is shipped that way on purpose. Adding or removing a command needs a restart before Discord sees the change.