Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/commandkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ CommandKit is a library that makes it easy to handle commands and events in your
- Automatic command registration, edits, and deletion 🤖
- Supports multiple development servers 🤝
- Supports multiple users as bot developers 👥
- Object oriented 💻
- User friendly CLI 🖥️

## Documentation

Expand Down
49 changes: 27 additions & 22 deletions packages/commandkit/bin/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function bootstrapProductionBuild(config) {
entry: [src, '!dist', '!.commandkit', `!${outDir}`],
});

if (antiCrash) await injectAntiCrash(outDir, main);
await injectShims(outDir, main, antiCrash);

status.succeed(
Colors.green(`Build completed in ${(performance.now() - start).toFixed(2)}ms!`),
Expand All @@ -55,27 +55,32 @@ export async function bootstrapProductionBuild(config) {
}
}

function injectAntiCrash(outDir, main) {
async function injectShims(outDir, main, antiCrash) {
const path = join(process.cwd(), outDir, main);
const snippet = [
'\n\n// --- CommandKit Anti-Crash Monitor ---',
';(()=>{',
" 'use strict';",
" // 'uncaughtException' event is supposed to be used to perform synchronous cleanup before shutting down the process",
' // instead of using it as a means to resume operation.',
' // But it exists here due to compatibility reasons with discord bot ecosystem.',
" const p = (t) => `\\x1b[33m${t}\\x1b[0m`, b = '[CommandKit Anti-Crash Monitor]', l = console.log, e1 = 'uncaughtException', e2 = 'unhandledRejection';",
' if (!process.eventNames().includes(e1)) // skip if it is already handled',
' process.on(e1, (e, o) => {',
' l(p(`${b} Uncaught Exception`)); l(p(b), p(e.stack || e));',
' })',
' if (!process.eventNames().includes(e2)) // skip if it is already handled',
' process.on(e2, (r) => {',
' l(p(`${b} Unhandled promise rejection`)); l(p(`${b} ${r.stack || r}`));',
' });',
'})();',
'// --- CommandKit Anti-Crash Monitor ---\n',
].join('\n');

return appendFile(path, snippet);
const antiCrashScript = antiCrash
? [
'\n\n// --- CommandKit Anti-Crash Monitor ---',
';(()=>{',
" 'use strict';",
" // 'uncaughtException' event is supposed to be used to perform synchronous cleanup before shutting down the process",
' // instead of using it as a means to resume operation.',
' // But it exists here due to compatibility reasons with discord bot ecosystem.',
" const p = (t) => `\\x1b[33m${t}\\x1b[0m`, b = '[CommandKit Anti-Crash Monitor]', l = console.log, e1 = 'uncaughtException', e2 = 'unhandledRejection';",
' if (!process.eventNames().includes(e1)) // skip if it is already handled',
' process.on(e1, (e) => {',
' l(p(`${b} Uncaught Exception`)); l(p(b), p(e.stack || e));',
' })',
' if (!process.eventNames().includes(e2)) // skip if it is already handled',
' process.on(e2, (r) => {',
' l(p(`${b} Unhandled promise rejection`)); l(p(`${b} ${r.stack || r}`));',
' });',
'})();',
'// --- CommandKit Anti-Crash Monitor ---\n',
].join('\n')
: '';

const finalScript = [antiCrashScript].join('\n');

return appendFile(path, finalScript);
}
11 changes: 10 additions & 1 deletion packages/commandkit/bin/parse-env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,23 @@ const VALUE_PREFIXES = {
DATE: 'DATE::',
};

function catcher(fn) {
try {
fn();
return true;
} catch {
return false;
}
}

export function parseEnv(src) {
for (const key in src) {
const value = src[key];

if (typeof value !== 'string') continue;

if (value.startsWith(VALUE_PREFIXES.JSON)) {
src[key] = JSON.parse(value.replace(VALUE_PREFIXES.JSON, ''));
catcher(() => (src[key] = JSON.parse(value.replace(VALUE_PREFIXES.JSON, ''))));
continue;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/commandkit/src/components/ButtonKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import {
ComponentType,
} from 'discord.js';

/**
* The handler to run when a button is clicked. This handler is called with the interaction as the first argument.
* If the first argument is null, it means that the interaction collector has been destroyed.
*/
export type CommandKitButtonBuilderInteractionCollectorDispatch = (
interaction: ButtonInteraction,
interaction: ButtonInteraction | null,
) => Awaitable<void>;

export type CommandKitButtonBuilderInteractionCollectorDispatchContextData = {
Expand Down Expand Up @@ -133,6 +137,7 @@ export class ButtonKit extends ButtonBuilder {
}

#destroyCollector() {
this.#onClickHandler?.(null);
this.#collector?.stop('end');
this.#collector?.removeAllListeners();
this.#collector = null;
Expand Down
27 changes: 18 additions & 9 deletions packages/commandkit/src/handlers/command-handler/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import loadCommandsWithRest from './functions/loadCommandsWithRest';
import registerCommands from './functions/registerCommands';
import builtInValidations from './validations';
import colors from '../../utils/colors';

import rdfc from 'rfdc';

const clone = rdfc();
import { clone } from '../../utils/clone';

/**
* A handler for client application commands.
Expand Down Expand Up @@ -80,7 +77,7 @@ export class CommandHandler {
for (const commandFilePath of commandFilePaths) {
const modulePath = toFileURL(commandFilePath);

let importedObj = await import(`${modulePath}?t=${Date.now()}`);
const importedObj = await import(`${modulePath}?t=${Date.now()}`);
let commandObj: CommandFileObject = clone(importedObj); // Make commandObj extensible

// If it's CommonJS, invalidate the import cache
Expand Down Expand Up @@ -161,15 +158,25 @@ export class CommandHandler {

handleCommands() {
this.#data.client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand() && !interaction.isContextMenuCommand()) return;
if (
!interaction.isChatInputCommand() &&
!interaction.isContextMenuCommand() &&
!interaction.isAutocomplete()
)
return;

const isAutocomplete = interaction.isAutocomplete();

const targetCommand = this.#data.commands.find(
(cmd) => cmd.data.name === interaction.commandName,
);

if (!targetCommand) return;

const { data, options, run, ...rest } = targetCommand;
const { data, options, run, autocompleteRun, ...rest } = targetCommand;

// skip if autocomplete handler is not defined
if (isAutocomplete && !autocompleteRun) return;

const commandObj = {
data: targetCommand.data,
Expand Down Expand Up @@ -217,11 +224,13 @@ export class CommandHandler {

if (!canRun) return;

targetCommand.run({
const context = {
interaction,
client: this.#data.client,
handler: this.#data.commandkitInstance,
});
};

await targetCommand[isAutocomplete ? 'autocompleteRun' : 'run']!(context);
});
}

Expand Down
18 changes: 13 additions & 5 deletions packages/commandkit/src/handlers/command-handler/typings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { ChatInputCommandInteraction, Client, ContextMenuCommandInteraction } from 'discord.js';
import { CommandKit } from '../../CommandKit';
import { CommandFileObject } from '../../typings';
import { ValidationHandler } from '../validation-handler/ValidationHandler';
import type {
AutocompleteInteraction,
ChatInputCommandInteraction,
Client,
ContextMenuCommandInteraction,
} from 'discord.js';
import type { CommandKit } from '../../CommandKit';
import type { CommandFileObject } from '../../typings';
import type { ValidationHandler } from '../validation-handler/ValidationHandler';

/**
* Command handler options.
Expand Down Expand Up @@ -86,7 +91,10 @@ export interface BuiltInValidationParams {
/**
* The interaction of the target command.
*/
interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction;
interaction:
| ChatInputCommandInteraction
| ContextMenuCommandInteraction
| AutocompleteInteraction;

/**
* The command handler's data.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BuiltInValidationParams } from '../typings';
import type { BuiltInValidationParams } from '../typings';

export default function ({ interaction, targetCommand, handlerData }: BuiltInValidationParams) {
if (interaction.isAutocomplete()) return;
if (targetCommand.options?.devOnly) {
if (interaction.inGuild() && !handlerData.devGuildIds.includes(interaction.guildId)) {
interaction.reply({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BuiltInValidationParams } from '../typings';
import { EmbedBuilder } from 'discord.js';

export default function ({ interaction, targetCommand }: BuiltInValidationParams) {
if (interaction.isAutocomplete()) return;
const userPermissions = interaction.memberPermissions;
let userPermissionsRequired = targetCommand.options?.userPermissions;
let missingUserPermissions: string[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import type { CommandHandler } from '../command-handler/CommandHandler';
import { getFilePaths, getFolderPaths } from '../../utils/get-paths';
import { toFileURL } from '../../utils/resolve-file-url';
import colors from '../../utils/colors';
import rdfc from 'rfdc';

const clone = rdfc();
import { clone } from '../../utils/clone';

/**
* A handler for client events.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import type { ValidationHandlerData, ValidationHandlerOptions } from './typings'
import { toFileURL } from '../../utils/resolve-file-url';
import { getFilePaths } from '../../utils/get-paths';
import colors from '../../utils/colors';
import rdfc from 'rfdc';

const clone = rdfc();
import { clone } from '../../utils/clone';

/**
* A handler for command validations.
Expand Down
30 changes: 21 additions & 9 deletions packages/commandkit/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
type RESTPostAPIApplicationCommandsJSONBody,
type MessageContextMenuCommandInteraction,
type UserContextMenuCommandInteraction,
type ContextMenuCommandInteraction,
type ChatInputCommandInteraction,
type PermissionsString,
type Client,
import type {
RESTPostAPIApplicationCommandsJSONBody,
MessageContextMenuCommandInteraction,
UserContextMenuCommandInteraction,
ContextMenuCommandInteraction,
ChatInputCommandInteraction,
PermissionsString,
Client,
AutocompleteInteraction,
} from 'discord.js';
import type { CommandKit } from '../CommandKit';

Expand All @@ -20,7 +21,8 @@ export interface CommandProps {
| ChatInputCommandInteraction
| ContextMenuCommandInteraction
| UserContextMenuCommandInteraction
| MessageContextMenuCommandInteraction;
| MessageContextMenuCommandInteraction
| AutocompleteInteraction;

/**
* The Discord.js client object that CommandKit is handling.
Expand All @@ -33,6 +35,16 @@ export interface CommandProps {
handler: CommandKit;
}

/**
* Props for autocomplete command run functions.
*/
export interface AutocompleteCommandProps extends CommandProps {
/**
* The current autocomplete command interaction object.
*/
interaction: AutocompleteInteraction;
}

/**
* Props for slash (chat input) command run functions.
*/
Expand Down
27 changes: 25 additions & 2 deletions packages/commandkit/src/typings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This types file is for development
// For exported types use ./types/index.ts

import type { Client, Interaction } from 'discord.js';
import type { CacheType, Client, Interaction } from 'discord.js';
import type { CommandData, CommandKit, CommandOptions, ReloadType } from './index';
import type { CommandHandler, EventHandler, ValidationHandler } from './handlers';

Expand Down Expand Up @@ -64,13 +64,36 @@ export interface CommandKitData extends CommandKitOptions {
validationHandler?: ValidationHandler;
}

/**
* Represents a command context.
*/
export interface CommandContext<T extends Interaction, Cached extends CacheType> {
/**
* The interaction that triggered this command.
*/
interaction: Interaction<CacheType>;
/**
* The client that instantiated this command.
*/
client: Client;
/**
* The command data.
*/
handler: CommandKit;
}

/**
* Represents a command file.
*/
export interface CommandFileObject {
data: CommandData;
options?: CommandOptions;
run: ({}: { interaction: Interaction; client: Client; handler: CommandKit }) => void;
run: <Cached extends CacheType = CacheType>(
ctx: CommandContext<Interaction, Cached>,
) => Awaited<void>;
autocompleteRun?: <Cached extends CacheType = CacheType>(
ctx: CommandContext<Interaction, Cached>,
) => Awaited<void>;
filePath: string;
category: string | null;
[key: string]: any;
Expand Down
3 changes: 3 additions & 0 deletions packages/commandkit/src/utils/clone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import rfdc from 'rfdc';

export const clone = rfdc();
Loading