diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64b2329..91a882a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,7 @@ jobs: uses: taiki-e/install-action@git-cliff - name: Run github release script - run: npm run release:git + run: node scripts/actions/github.js env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} @@ -95,7 +95,7 @@ jobs: run: npm install - name: Run npm release script - run: npm run release:npm + run: node scripts/actions/npm.js env: GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_SHA: ${{ github.sha }} diff --git a/.npmignore b/.npmignore index a4404ee..d4f867f 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,7 @@ CHANGELOG.md cargo.toml .husky +cliff.toml # Test examples diff --git a/examples/basic_client/index.js b/examples/basic_client/index.js index 4df8075..a0de7b3 100644 --- a/examples/basic_client/index.js +++ b/examples/basic_client/index.js @@ -3,12 +3,13 @@ const arox = require("../../dist/index"); const backend = require("i18next-fs-backend"); const path = require("node:path"); const { LogLevel } = require("../../dist/utils/logger/ILogger"); +const { IntentsBitField } = require("discord.js"); const myinstance = i18next.createInstance({ supportedLngs: ["en-US", "tr"], fallbackLng: "en-US", defaultNS: "translation", - ns: ["translation", "test"], + ns: ["translation", "test", "error"], backend: { loadPath: path.join(__dirname, "locales/{{lng}}/{{ns}}.json"), }, @@ -19,7 +20,11 @@ const myinstance = i18next.createInstance({ myinstance.use(backend); const client = new arox.Client({ - intents: 37376, + intents: [ + IntentsBitField.Flags.Guilds, + IntentsBitField.Flags.GuildMessages, + IntentsBitField.Flags.MessageContent, + ], prefix: { enabled: true, prefix: "a!" }, logger: { level: LogLevel.Trace, @@ -29,12 +34,12 @@ const client = new arox.Client({ }); arox.setClient(client); -const command = new arox.CommandBuilder({ - name: "arox", - description: "Arox test command", - slash: true, - prefix: true, -}); +const command = new arox.CommandBuilder( + new arox.ApplicationCommandBuilder() + .setName("arox") + .setDescription("Arox Test Command") + .addAliases("a") +); arox.clearClient(); command diff --git a/examples/basic_client/locales/en-US/error.json b/examples/basic_client/locales/en-US/error.json new file mode 100644 index 0000000..55bf77b --- /dev/null +++ b/examples/basic_client/locales/en-US/error.json @@ -0,0 +1,4 @@ +{ + "command.notfound": "Command not found or disabled1", + "command.disabled": "Command not found or disabled2" +} diff --git a/package-lock.json b/package-lock.json index 4c69a3e..e5f9200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,14 @@ "@swc/helpers": "^0.5.18", "colorette": "^2.0.20", "fast-glob": "^3.3.3", - "i18next": "^25.8.0", - "i18next-fs-backend": "^2.6.1", - "lodash": "^4.17.21" + "i18next": "^25.8.0" }, "devDependencies": { "@swc/cli": "^0.7.10", "@types/lodash": "^4.17.23", "@types/node": "^25.0.9", "husky": "^9.1.7", + "i18next-fs-backend": "^2.6.1", "libnpmpack": "^9.0.12", "oxfmt": "^0.27.0", "oxlint": "^1.39.0", @@ -2880,6 +2879,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.1.tgz", "integrity": "sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==", + "dev": true, "license": "MIT" }, "node_modules/iconv-lite": { @@ -3137,7 +3137,8 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.snakecase": { "version": "4.1.1", diff --git a/package.json b/package.json index 6ab7580..8b7380e 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "imports": { - "#types/*": "./types/*", - "#utils": "./src/utils/index.ts", - "#structures": "./src/structures/index.ts", - "#ctx": "./src/context.ts" + "#types/*": "./types/*" }, "scripts": { "prepare": "node scripts/prepareHusky.js", @@ -32,24 +29,21 @@ "format": "oxfmt", "build:js": "swc src -d dist --strip-leading-paths --copy-files", "build:dts": "tsc --emitDeclarationOnly", - "build": "npm run build:js && node ./scripts/actions/patch.js && npm run build:dts", - "release:git": "node scripts/actions/github.js", - "release:npm": "node scripts/actions/npm.js" + "build": "npm run build:js && node ./scripts/actions/patch.js && npm run build:dts" }, "dependencies": { "@sapphire/timestamp": "^1.0.5", "@swc/helpers": "^0.5.18", "colorette": "^2.0.20", "fast-glob": "^3.3.3", - "i18next": "^25.8.0", - "i18next-fs-backend": "^2.6.1", - "lodash": "^4.17.21" + "i18next": "^25.8.0" }, "devDependencies": { "@swc/cli": "^0.7.10", "@types/lodash": "^4.17.23", "@types/node": "^25.0.9", "husky": "^9.1.7", + "i18next-fs-backend": "^2.6.1", "libnpmpack": "^9.0.12", "oxfmt": "^0.27.0", "oxlint": "^1.39.0", diff --git a/scripts/utils/build.js b/scripts/utils/build.js index aa4d5d2..e09aa4f 100644 --- a/scripts/utils/build.js +++ b/scripts/utils/build.js @@ -16,9 +16,12 @@ module.exports = async function buildPackage(tempjson) { console.error("Build failed, aborting release"); throw err; } + + delete tempjson.scripts; + delete tempjson.devDependencies; await writeFile( path.join(process.cwd(), "package.json"), - JSON.stringify(tempjson, null, 2) + JSON.stringify(tempjson) ); console.log("Packing npm..."); const tarballBuffer = await pack(process.cwd()); diff --git a/src/context.ts b/src/context.ts index 3994d7b..b91af1c 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,4 +1,4 @@ -import { Client } from "#structures"; +import { Client } from "./structures"; // Birden fazla client olursa hata çıkartabilir ama aklıma gelen tek şey bu export let currentClient: Client | null = null; diff --git a/src/events/interaction.ts b/src/events/interaction.ts index 9ca5710..4b2aa85 100644 --- a/src/events/interaction.ts +++ b/src/events/interaction.ts @@ -1,29 +1,40 @@ import { Events, MessageFlags } from "discord.js"; -import { EventBuilder, Context } from "#structures"; +import { EventBuilder, Context } from "../structures/index"; new EventBuilder(Events.InteractionCreate, false).onExecute( async function (context, interaction) { if (!interaction.isChatInputCommand()) return; const command = context.client.commands.get(interaction.commandName); - if (!command || !command.supportsSlash) { + const ctx = new Context(context.client, { interaction }); + + if (!command) { await interaction.reply({ - content: "Command not found or disabled.", + content: ctx.t("error.command.notfound", { + defaultValue: "Command not found or disabled.", + }), + flags: MessageFlags.Ephemeral, + }); + return; + } + if (!command.supportsSlash) { + await interaction.reply({ + content: ctx.t("error.command.disabled", { + defaultValue: "Command not found or disabled.", + }), flags: MessageFlags.Ephemeral, }); return; } - try { - const ctx = new Context(context.client, { interaction }); ctx.locale = interaction.locale; context.logger.debug( - `${ctx.author?.tag ?? "Unknown"} used ${command.name}(interaction)` + `${ctx.author?.tag ?? "Unknown"} used ${command.data.name}(interaction)` ); if (command._onInteraction) await command._onInteraction(ctx.toJSON()); } catch (error) { context.client.logger.error( - `Error executing command ${command.name}:`, + `Error executing command ${command.data.name}:`, error ); if (interaction.replied || interaction.deferred) { diff --git a/src/events/message.ts b/src/events/message.ts index 39c78e2..2aa9912 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -1,6 +1,6 @@ import { Events } from "discord.js"; -import { EventBuilder, Context } from "#structures"; -import { deleteMessageAfterSent } from "#utils"; +import { EventBuilder, Context } from "../structures/index"; +import { deleteMessageAfterSent } from "../utils/index"; new EventBuilder( Events.MessageCreate, @@ -22,12 +22,28 @@ new EventBuilder( const commandAlias = context.client.aliases.findKey((cmd) => cmd.has(commandName) ); + const ctx = new Context(context.client, { message, args }); let command = context.client.commands.get(commandAlias ?? commandName); - if (!command || !command.supportsPrefix) { + + if (!command) { + await message + .reply({ + content: ctx.t("error.command.notfound", { + defaultValue: "Command not found or disabled.", + }), + allowedMentions: { repliedUser: false }, + }) + .then(deleteMessageAfterSent); + return; + } + + if (!command.supportsPrefix) { await message .reply({ - content: "Command not found or disabled.", + content: ctx.t("error.command.disabled", { + defaultValue: "Command not found or disabled.", + }), allowedMentions: { repliedUser: false }, }) .then(deleteMessageAfterSent); @@ -35,14 +51,13 @@ new EventBuilder( } try { - const ctx = new Context(context.client, { message, args }); context.logger.debug( - `${ctx.author?.tag ?? "Unknown"} used ${command.name}(message)` + `${ctx.author?.tag ?? "Unknown"} used ${command.data.name}(message)` ); if (command._onMessage) await command._onMessage(ctx.toJSON()); } catch (error) { context.client.logger.error( - `Error executing command ${command.name}:`, + `Error executing command ${command.data.name}:`, error ); } diff --git a/src/events/ready.ts b/src/events/ready.ts index f1f8504..fcb6448 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,5 +1,5 @@ import { Events } from "discord.js"; -import { EventBuilder } from "#structures"; +import { EventBuilder } from "../structures/index"; new EventBuilder(Events.ClientReady).onExecute(async function (context) { if (context.client.options.autoRegisterCommands) { diff --git a/src/index.ts b/src/index.ts index 4a8c0e9..9321900 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -export * from "#structures"; +export * from "./structures"; export * from "./utils/logger/Logger"; -export * from "#ctx"; +export * from "./context"; export const version = "[VI]{{version}}[/VI]"; diff --git a/src/structures/builder/Argument.ts b/src/structures/builder/Argument.ts deleted file mode 100644 index bf1c28c..0000000 --- a/src/structures/builder/Argument.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - ApplicationCommandOptionData, - ApplicationCommandOptionType, -} from "discord.js"; - -export class Argument { - public readonly name: string; - public readonly description: string; - public readonly type: ApplicationCommandOptionType; - public readonly required: boolean; - public readonly choices?: { name: string; value: string | number }[]; - - constructor(data: { - name: string; - description: string; - type: ApplicationCommandOptionType; - required?: boolean; - choices?: { name: string; value: string | number }[]; - }) { - this.name = data.name; - this.description = data.description; - this.type = data.type; - this.required = data.required ?? false; - this.choices = data.choices; - } - - public toJSON(): ApplicationCommandOptionData { - const choicesAllowedTypes = [ - ApplicationCommandOptionType.String, - ApplicationCommandOptionType.Integer, - ApplicationCommandOptionType.Number, - ]; - if (this.choices && !choicesAllowedTypes.includes(this.type)) { - throw new Error( - `Choices are not allowed for option type ${ApplicationCommandOptionType[this.type]} (${this.type})` - ); - } - return { - name: this.name, - description: this.description, - type: this.type, - required: this.required, - choices: this.choices, - } as ApplicationCommandOptionData; - } -} diff --git a/src/structures/builder/Builder.ts b/src/structures/builder/Builder.ts new file mode 100644 index 0000000..b864d92 --- /dev/null +++ b/src/structures/builder/Builder.ts @@ -0,0 +1,56 @@ +import { + SlashCommandBuilder, + RESTPostAPIChatInputApplicationCommandsJSONBody, +} from "discord.js"; +import { normalizeArray } from "../../utils/normalizeArray"; +import { Client } from "../core"; + +export interface ApplicationJSONBody extends RESTPostAPIChatInputApplicationCommandsJSONBody { + prefix_support: boolean; + slash_support: boolean; + aliases: string[]; +} +export class ApplicationCommandBuilder extends SlashCommandBuilder { + constructor() { + super(); + } + protected prefix_support: boolean = true; + protected slash_support: boolean = true; + protected aliases: string[] = []; + + setAliases(...alias: string[]) { + Reflect.set(this, "aliases", normalizeArray(alias)); + return this; + } + + addAliases(...alias: string[]) { + const currentAliases = Reflect.get(this, "aliases") || []; + Reflect.set( + this, + "aliases", + normalizeArray([...currentAliases, ...normalizeArray(alias)]) + ); + return this; + } + + setPrefixSupport(support: boolean) { + Reflect.set(this, "prefix_support", support); + return this; + } + + setSlashSupport(support: boolean) { + Reflect.set(this, "slash_support", support); + return this; + } + override toJSON(): ApplicationJSONBody { + return super.toJSON() as ApplicationJSONBody; + } + + toClientJSON( + client: Client + ): ReturnType { + return { + ...this.toJSON(), + }; + } +} diff --git a/src/structures/builder/Command.ts b/src/structures/builder/Command.ts index cd2ca3d..7a69d56 100644 --- a/src/structures/builder/Command.ts +++ b/src/structures/builder/Command.ts @@ -1,82 +1,58 @@ -import { - ApplicationCommandOptionData, - ChatInputCommandInteraction, - Message, -} from "discord.js"; -import { Context, Client } from "#structures"; -import { Argument } from "./Argument"; -import { currentClient } from "#ctx"; +import { ChatInputCommandInteraction, Message } from "discord.js"; +import { Context, Client } from "../index"; +import { currentClient } from "../../context"; import { MaybePromise } from "#types/extra.js"; -import { Logger } from "#utils"; +import { Logger } from "../../utils/index"; +import { ApplicationCommandBuilder } from "./Builder"; type MessageContext = NonNullable["toJSON"]>>; type InteractionContext = NonNullable< ReturnType["toJSON"]> >; -export interface CommandOptions { - name: string; - description: string; - aliases?: string[]; - options?: (ApplicationCommandOptionData | Argument)[]; - slash?: boolean; - prefix?: boolean; -} - export class CommandBuilder { public readonly client: Client; public readonly logger: Logger; + #supportsSlash: boolean; + #supportsPrefix: boolean; + _onMessage?: (ctx: MessageContext) => MaybePromise; + _onInteraction?: (ctx: InteractionContext) => MaybePromise; - public readonly name: string; - public readonly description: string; - public readonly aliases: string[]; - public readonly options: ApplicationCommandOptionData[]; - private _supportsSlash: boolean; - private _supportsPrefix: boolean; - public _onMessage?: (ctx: MessageContext) => MaybePromise; - public _onInteraction?: (ctx: InteractionContext) => MaybePromise; - - public get supportsSlash() { - return this._supportsSlash && this._onInteraction; + get supportsSlash() { + return this.#supportsSlash && this._onInteraction; } - public get supportsPrefix() { - return this._supportsPrefix && this._onMessage; + get supportsPrefix() { + return this.#supportsPrefix && this._onMessage; } - constructor(options: CommandOptions) { + + constructor(public readonly data: ApplicationCommandBuilder) { const client = currentClient; if (!client) throw new Error("Client is not defined"); this.client = client; this.logger = client.logger; + let commandJSON = data.toJSON(); + this.#supportsPrefix = commandJSON.prefix_support ?? false; + this.#supportsSlash = commandJSON.slash_support ?? false; - this.name = options.name; - this.description = options.description; - this.aliases = options.aliases ?? []; - - this.options = (options.options ?? []).map((opt) => { - return opt instanceof Argument ? opt.toJSON() : opt; - }); - this._supportsPrefix = options.prefix ?? false; - this._supportsSlash = options.slash ?? false; - - if (!this._supportsPrefix && !this._supportsSlash) { + if (!this.#supportsPrefix && !this.#supportsSlash) { throw new Error( - `Command ${this.name} must support either slash or prefix commands.` + `Command ${data.name} must support either slash or prefix commands.` ); } - if (this.client.commands.has(this.name)) - throw new Error(`Command name "${this.name}" is already registered.`); + if (this.client.commands.has(data.name)) + throw new Error(`Command name "${data.name}" is already registered.`); const existingAliasOwner = this.client.aliases.findKey((aliases) => - aliases.has(this.name) + aliases.has(data.name) ); if (existingAliasOwner) { throw new Error( - `Command name "${this.name}" is already registered as an alias for command "${existingAliasOwner}".` + `Command name "${data.name}" is already registered as an alias for command "${existingAliasOwner}".` ); } - for (const alias of this.aliases) { + for (const alias of commandJSON.aliases) { if (this.client.commands.has(alias)) { throw new Error( `Alias "${alias}" is already registered as a command name.` @@ -92,11 +68,11 @@ export class CommandBuilder { } } - this.client.commands.set(this.name, this); - if (this.aliases.length > 0) { - this.client.aliases.set(this.name, new Set(this.aliases)); + this.client.commands.set(commandJSON.name, this); + if (commandJSON.aliases.length > 0) { + this.client.aliases.set(commandJSON.name, new Set(commandJSON.aliases)); } - this.logger.debug(`Loaded Command ${this.name}`); + this.logger.debug(`Loaded Command ${commandJSON.name}`); } onMessage(func: (ctx: MessageContext) => MaybePromise) { diff --git a/src/structures/builder/Context.ts b/src/structures/builder/Context.ts index 9dba783..58735a4 100644 --- a/src/structures/builder/Context.ts +++ b/src/structures/builder/Context.ts @@ -1,5 +1,6 @@ import { Message, User, ChatInputCommandInteraction, Locale } from "discord.js"; -import { Client } from "#structures"; +import { Client } from "../index"; +import { TOptions } from "i18next"; type ContextPayload = T extends ChatInputCommandInteraction @@ -7,9 +8,9 @@ type ContextPayload = : { message: T; args?: string[] }; export class Context { - public readonly args: string[]; - public readonly data: T; - public locale?: `${Locale}`; + readonly args: string[]; + readonly data: T; + locale?: `${Locale}`; constructor( public readonly client: Client, @@ -24,15 +25,15 @@ export class Context { } } - public isInteraction(): this is Context { + isInteraction(): this is Context { return this.data instanceof ChatInputCommandInteraction; } - public isMessage(): this is Context { + isMessage(): this is Context { return this.data instanceof Message; } - public get author(): User | null { + get author(): User | null { if (this.isInteraction()) { return this.data.user; } @@ -42,11 +43,12 @@ export class Context { return null; } - public t(key: string, args?: Record): string { + t(key: string, options?: TOptions & { defaultValue?: string }): string { if (!this.client.i18n) { throw new Error("i18n is not initialized"); } - let locale = + + const locale = this.locale ?? (Array.isArray(this.client.i18n.options.fallbackLng) ? this.client.i18n.options.fallbackLng[0] @@ -55,10 +57,16 @@ export class Context { const t = this.client.i18n.getFixedT(locale); - return t(key, args) as string; + const result = t(key, options); + + if (result === key && options?.defaultValue) { + return options.defaultValue; + } + + return result; } - public toJSON() { + toJSON() { const { data, args, author } = this; if (this.isInteraction()) { diff --git a/src/structures/builder/Event.ts b/src/structures/builder/Event.ts index 7dfd4c1..a5bf2be 100644 --- a/src/structures/builder/Event.ts +++ b/src/structures/builder/Event.ts @@ -1,8 +1,8 @@ import { ClientEvents } from "discord.js"; import { MaybePromise } from "#types/extra.js"; -import { currentClient } from "#ctx"; -import { Client } from "#structures"; -import { Logger } from "#utils"; +import { currentClient } from "../../context"; +import { Client } from "../index"; +import { Logger } from "../../utils/index"; type EventArgs = ClientEvents[K]; type EventHandler = ( @@ -11,15 +11,15 @@ type EventHandler = ( ) => MaybePromise; export class EventBuilder { - public readonly client: Client; - public readonly logger: Logger; - private handler?: EventHandler; - private bound = false; + readonly client: Client; + readonly logger: Logger; + #handler?: EventHandler; + #bound = false; - private readonly listener = async (...args: EventArgs) => { - if (!this.handler) return; + #listener = async (...args: EventArgs) => { + if (!this.#handler) return; try { - await this.handler(this, ...args); + await this.#handler(this, ...args); } catch (error) { this.client.logger.error( `Error executing event ${this.name} (${this.constructor.name}):`, @@ -38,27 +38,27 @@ export class EventBuilder { this.logger = currentClient.logger; if (_handler) { - this.handler = _handler; - this.register(); + this.#handler = _handler; + this.#register(); } } - private register(): void { - if (this.bound || !this.handler) return; + #register(): void { + if (this.#bound || !this.#handler) return; if (this.once) { - this.client.once(this.name as string, this.listener); + this.client.once(this.name as string, this.#listener); } else { - this.client.on(this.name as string, this.listener); + this.client.on(this.name as string, this.#listener); } - this.bound = true; + this.#bound = true; this.logger.debug(`Loaded Event ${String(this.name)}`); } public onExecute(func: EventHandler) { - this.handler = func; - this.register(); + this.#handler = func; + this.#register(); return this; } } diff --git a/src/structures/builder/index.ts b/src/structures/builder/index.ts index 81fad25..aa461c8 100644 --- a/src/structures/builder/index.ts +++ b/src/structures/builder/index.ts @@ -1,4 +1,4 @@ -export * from "./Argument"; export * from "./Command"; export * from "./Context"; export * from "./Event"; +export * from "./Builder"; diff --git a/src/structures/core/Client.ts b/src/structures/core/Client.ts index 0da8eb9..559d1d3 100644 --- a/src/structures/core/Client.ts +++ b/src/structures/core/Client.ts @@ -5,7 +5,7 @@ import { Routes, IntentsBitField, } from "discord.js"; -import { CommandBuilder } from "#structures"; +import { CommandBuilder } from "../index"; import path from "path"; import { getFiles, @@ -13,10 +13,9 @@ import { getPrefix, I18nLoggerAdapter, Logger, -} from "#utils"; +} from "../../utils"; import { FrameworkOptions } from "#types/client.js"; -import { merge } from "lodash"; -import { clearClient, setClient } from "#ctx"; +import { clearClient, setClient } from "../../context"; import { i18n } from "i18next"; import { existsSync } from "fs"; @@ -28,18 +27,18 @@ const defaultOpts: Omit = { export class Client< Ready extends boolean = boolean, > extends DiscordClient { - public readonly logger: Logger; - public commands: Collection; - public aliases: Collection>; - public readonly prefix: string | false; - public i18n: i18n | undefined; + readonly logger: Logger; + commands: Collection; + aliases: Collection>; + readonly prefix: string | false; + i18n: i18n | undefined; - declare public options: Omit & { + declare options: Omit & { intents: IntentsBitField; }; constructor(opts: FrameworkOptions) { - super(merge({}, defaultOpts, opts) as FrameworkOptions); + super({ ...defaultOpts, ...opts } as FrameworkOptions); this.logger = new Logger(opts.logger); this.commands = new Collection(); this.aliases = new Collection(); @@ -51,9 +50,9 @@ export class Client< setClient(this); try { - require("../events/ready"); - require("../events/interaction"); - if (this.prefix) require("../events/message"); + require("../../events/ready.js"); + require("../../events/interaction.js"); + if (this.prefix) require("../../events/message.js"); } finally { clearClient(); } @@ -61,7 +60,7 @@ export class Client< override async login(token?: string) { if (this.options.includePaths) { for (const p of this.options.includePaths) { - this.loadDir(path.join(getProjectRoot(), p)).catch((error) => + this.#loadDir(path.join(getProjectRoot(), p)).catch((error) => this.logger.error("Error loading events:", error) ); } @@ -72,18 +71,18 @@ export class Client< return super.login(token); } - async loadDir(dir: string) { + async #loadDir(dir: string) { if (!existsSync(dir)) { this.logger.debug(`Directory not found: ${dir}`); return; } const files = getFiles(dir); for (const file of files) { - await this.loadFile(file); + await this.#loadFile(file); } } - async loadFile(file: string) { + async #loadFile(file: string) { try { delete require.cache[require.resolve(file)]; setClient(this); @@ -109,11 +108,7 @@ export class Client< const slashCommands = this.commands .filter((cmd) => cmd.supportsSlash) - .map((cmd) => ({ - name: cmd.name, - description: cmd.description, - options: cmd.options, - })); + .map((cmd) => cmd.data.toClientJSON(this)); const rest = new REST({ version: "10" }).setToken(this.token); diff --git a/src/utils/normalizeArray.ts b/src/utils/normalizeArray.ts new file mode 100644 index 0000000..cf9a009 --- /dev/null +++ b/src/utils/normalizeArray.ts @@ -0,0 +1,21 @@ +/** + * Normalizes data that is a rest parameter or an array into an array with a depth of 1. + * + * @typeParam ItemType - The data that must satisfy {@link RestOrArray}. + * @param arr - The (possibly variadic) data to normalize + */ +export function normalizeArray( + arr: RestOrArray +): ItemType[] { + if (Array.isArray(arr[0])) return [...arr[0]]; + return arr as ItemType[]; +} + +/** + * Represents data that may be an array or came from a rest parameter. + * + * @remarks + * This type is used throughout builders to ensure both an array and variadic arguments + * may be used. It is normalized with {@link normalizeArray}. + */ +export type RestOrArray = Type[] | [Type[]];