Skip to content

sunarjs/sunar

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation


Sunar simple banner

npm version npm downloads License: MIT TypeScript

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.

πŸš€ Features

  • 🎯 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

πŸ“¦ Installation

npm install sunar discord.js
yarn add sunar discord.js
pnpm add sunar discord.js
bun add sunar discord.js

Note: Sunar is ESM-only. Make sure your project has "type": "module" in your package.json or use .mjs file extensions.

🏁 Quick Start

1. Initialize the client

// 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();

2. Create your first signal

// 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 deployment

Registering commands on every bot startup can hit Discord's rate limits and is unnecessary in production.

3. Create your first command

// 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 };

4. Handle interactions

// 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 };

πŸ“š Core Concepts

Builders

Sunar provides various builders for different Discord interactions:

  • Slash - Slash commands (/command)
  • Button - Interactive buttons
  • Modal - Popup forms
  • ContextMenu - Right-click context menus
  • SelectMenu - Dropdown selection menus
  • Signal - Custom events and Discord events
  • SlashParent - Parent commands for subcommands
  • SlashSubcommand - Subcommands and grouped commands
  • Protector - Middleware for authorization and validation

Mutators

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 };

🎯 Advanced Examples

Modal with Form Handling

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 };

Dynamic Button Handling with Regex

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 };

Subcommands and Groups

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 };

πŸ“ Recommended Project Structure

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

πŸ”§ TypeScript Support

Sunar provides full TypeScript support. For TypeScript projects:

TypeScript Example

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

πŸ“– API Reference

Core Exports

// 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';

Selective Imports

// 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';

πŸ›  Development

Building

bun run build

Testing

bun run test

Development Mode

bun run test:dev

🀝 Contributing

Contributions 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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

❀️ Support

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

πŸ”— Links

πŸ™ Credits

Special thanks to:

  • Discord.js - The incredible library that powers Sunar
  • Fumadocs - Excellent framework for creating documentation

Made with ❀️ by Usse

Sponsor this project