A simple and easy to use Discord.js framework that provides a structured approach to building Discord bots with ESM-only support, built-in command handling, and modular architecture.
Note: This project is actively maintained and continuously improved. Check the changelog for latest updates.
- π― Type-Safe: Full TypeScript support with comprehensive type definitions
- β‘ Modern: Built for Discord.js v14 with ESM support
- π§ Modular: Clean separation of concerns with builders, handlers, and stores
- π‘οΈ Protected: Built-in cooldown and permission protection system
- π¨ Flexible: Support for slash commands, buttons, modals, context menus, and more
- π¦ Tree-Shakable: Optimized bundle size with selective imports
- π Auto-Loading: Automatic command registration and file loading
- πͺ Signals: Custom event system for enhanced bot functionality
npm install sunar discord.jsyarn add sunar discord.jspnpm add sunar discord.jsbun add sunar discord.jsNote: Sunar is ESM-only. Make sure your project has
"type": "module"in yourpackage.jsonor use.mjsfile extensions.
// src/index.js
import { Client, GatewayIntentBits, load } from 'sunar';
const start = async () => {
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages
]
});
// Load all commands, signals, and components
await load('src/{commands,signals,components}/**/*.js');
await client.login(); // uses process.env.DISCORD_TOKEN by default
};
start();// src/signals/ready.js
import { Signal, execute } from 'sunar';
import { registerCommands } from 'sunar/registry';
const ready = new Signal('ready', { once: true });
execute(ready, async (client) => {
// β οΈ WARNING: Command registration in ready event is for demonstration only!
// For production, create a separate script to register commands.
// This approach registers commands every time the bot starts.
await registerCommands(client.application);
console.log(`${client.user.tag} is ready! π`);
});
export { ready };
β οΈ Command Registration Best Practices:
- Development: Use
registerCommands()for quick testing and development- Production: Create a separate deployment script to register commands only when needed
- Guild-specific: Use
registerGuildCommands()for testing in specific servers- Global: Use
registerGlobalCommands()for production deploymentRegistering commands on every bot startup can hit Discord's rate limits and is unnecessary in production.
// src/commands/ping.js
import { Slash, execute } from 'sunar';
const ping = new Slash({
name: 'ping',
description: 'Show client ws ping'
});
execute(ping, (interaction) => {
interaction.reply({
content: `Client WS Ping: ${interaction.client.ws.ping}ms π`
});
});
export { ping };// src/signals/interactionCreate.js
import { Signal, execute } from 'sunar';
import { handleInteraction } from 'sunar/handlers';
const interactionCreate = new Signal('interactionCreate');
execute(interactionCreate, async (interaction) => {
await handleInteraction(interaction);
});
export { interactionCreate };Sunar provides various builders for different Discord interactions:
Slash- Slash commands (/command)Button- Interactive buttonsModal- Popup formsContextMenu- Right-click context menusSelectMenu- Dropdown selection menusSignal- Custom events and Discord eventsSlashParent- Parent commands for subcommandsSlashSubcommand- Subcommands and grouped commandsProtector- Middleware for authorization and validation
Enhance your builders with additional functionality:
import { Slash, execute, config, protect, Protector } from 'sunar';
// Create a protector
const adminOnly = new Protector({
commands: ['slash']
});
execute(adminOnly, (interaction, next) => {
if (!interaction.memberPermissions?.has('Administrator')) {
interaction.reply({ content: 'Admin only!', ephemeral: true });
return; // Block execution
}
return next(); // Allow execution
});
const slash = new Slash({
name: 'admin',
description: 'Admin only command'
});
// Add configuration
config(slash, {
guildIds: ['123456789'], // Guild-specific command
cooldown: 5000 // 5 second cooldown (in milliseconds)
});
// Add protection
protect(slash, [adminOnly]);
execute(slash, (interaction) => {
interaction.reply('Admin command executed!');
});
export { slash, adminOnly };import { Slash, Modal, execute } from 'sunar';
import {
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder
} from 'discord.js';
// Slash command to trigger modal
const feedback = new Slash({
name: 'feedback',
description: 'Submit feedback'
});
execute(feedback, (interaction) => {
const modal = new ModalBuilder()
.setCustomId('feedback-modal')
.setTitle('Submit Feedback')
.addComponents(
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('message')
.setLabel('Your feedback')
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
)
);
interaction.showModal(modal);
});
// Modal handler
const feedbackModal = new Modal({
id: 'feedback-modal'
});
execute(feedbackModal, (interaction) => {
const message = interaction.fields.getTextInputValue('message');
// Process feedback...
interaction.reply({
content: 'Thank you for your feedback! π',
ephemeral: true
});
});
export { feedback, feedbackModal };import { Button, execute } from 'sunar';
// Handle buttons with dynamic IDs like "delete-123", "delete-456"
const deleteButton = new Button({
id: /^delete-\d+$/
});
execute(deleteButton, (interaction) => {
const id = interaction.customId.split('-')[1];
// Delete logic here...
interaction.reply({
content: `Item ${id} deleted successfully!`,
ephemeral: true
});
});
export { deleteButton };import { SlashParent, SlashSubcommand, execute } from 'sunar';
import { ApplicationCommandOptionType } from 'discord.js';
// Parent command
const music = new SlashParent({
name: 'music',
description: 'Music commands',
groups: [{
name: "playlist",
description: "Playlist commands"
}]
});
// Subcommand
const play = new SlashSubcommand('music', {
name: 'play',
description: 'Play a song',
options: [{
name: 'query',
description: 'Song to play',
type: ApplicationCommandOptionType.String,
required: true
}]
});
// Grouped subcommand
const addToPlaylist = new SlashSubcommand('music', 'playlist', {
name: 'add',
description: 'Add song to playlist'
});
execute(play, (interaction) => {
const query = interaction.options.getString('query', true);
interaction.reply(`Now playing: ${query} π΅`);
});
execute(addToPlaylist, (interaction) => {
interaction.reply('Song added to playlist! π');
});
export { music, play, addToPlaylist };src/
βββ commands/ # Slash commands
β βββ utility/
β β βββ ping.js
β β βββ avatar.js
β βββ music/
βββ parent.js
β βββ play.js
β βββ playlist.js
βββ components/ # Interactive components
β βββ buttons/
β β βββ confirm.js
β β βββ delete.js
β βββ modals/
β β βββ feedback.js
β βββ selects/
β βββ role-select.js
βββ protectors/ # Middleware
β βββ admin-only.js
β βββ cooldown.js
βββ signals/ # Event handlers
β βββ ready.js
β βββ interactionCreate.js
β βββ messageCreate.js
βββ index.js # Bot entry point
Sunar provides full TypeScript support. For TypeScript projects:
import { Client, GatewayIntentBits, Slash, execute } from 'sunar';
const client = new Client({
intents: [GatewayIntentBits.Guilds]
});
const ping = new Slash({
name: 'ping',
description: 'Replies with Pong!'
});
execute(ping, (interaction) => {
interaction.reply(`Pong! ${interaction.client.ws.ping}ms`);
});
await client.login(); // uses process.env.DISCORD_TOKEN by default// Main client and builders
import {
Client,
Slash,
Button,
Modal,
ContextMenu,
SelectMenu,
Signal,
SlashParent,
SlashSubcommand,
Protector
} from 'sunar';
// Mutators and utilities
import { execute, config, protect, load } from 'sunar';// Registry
import { registerCommands } from 'sunar/registry';
// Handlers
import { handleInteraction, handleSlash } from 'sunar/handlers';
// Utilities
import { isSlashBuilder, isButtonBuilder } from 'sunar/utils';
// Stores
import { context, slashes, buttons } from 'sunar/stores';bun run buildbun run testbun run test:devContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
If you find Sunar helpful, please consider:
- β Starring the repository on GitHub
- π Reporting bugs or issues
- π‘ Suggesting new features
- π Contributing to documentation
- π¬ Sharing your projects built with Sunar
- π Documentation: https://sunar.js.org
- π¦ NPM Package: https://www.npmjs.com/package/sunar
- π GitHub Repository: https://github.com/sunarjs/sunar
- π Issues & Bug Reports: https://github.com/sunarjs/sunar/issues
- π¬ Discord.js Documentation: https://discord.js.org
Special thanks to:
- Discord.js - The incredible library that powers Sunar
- Fumadocs - Excellent framework for creating documentation
Made with β€οΈ by Usse
