@hyperneutrino/djs-lite 1.2.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +118 -7
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ApplicationCommandOptionType,
2
3
  ApplicationCommandType,
3
4
  AutocompleteInteraction,
4
5
  ButtonInteraction,
@@ -12,6 +13,8 @@ import {
12
13
  RoleSelectMenuInteraction,
13
14
  StringSelectMenuInteraction,
14
15
  UserSelectMenuInteraction,
16
+ type ApplicationCommandSubCommandData,
17
+ type ApplicationCommandSubGroupData,
15
18
  type Awaitable,
16
19
  type BaseApplicationCommandData,
17
20
  type ChatInputApplicationCommandData,
@@ -22,6 +25,7 @@ import {
22
25
  type UserApplicationCommandData,
23
26
  type UserContextMenuCommandInteraction,
24
27
  } from "discord.js";
28
+ import type { Dirent } from "node:fs";
25
29
  import fs from "node:fs/promises";
26
30
  import path from "node:path";
27
31
 
@@ -53,9 +57,35 @@ abstract class Command<T extends BaseApplicationCommandData, U extends CommandIn
53
57
  }
54
58
  }
55
59
 
56
- export class SlashCommand extends Command<ChatInputApplicationCommandData & { type: ApplicationCommandType.ChatInput }, ChatInputCommandInteraction, true> {}
57
- export class UserCommand extends Command<UserApplicationCommandData, UserContextMenuCommandInteraction> {}
58
- export class MessageCommand extends Command<MessageApplicationCommandData, MessageContextMenuCommandInteraction> {}
60
+ export class SlashCommand extends Command<Omit<ChatInputApplicationCommandData, "type">, ChatInputCommandInteraction, true> {}
61
+ export class UserCommand extends Command<Omit<UserApplicationCommandData, "type">, UserContextMenuCommandInteraction> {}
62
+ export class MessageCommand extends Command<Omit<MessageApplicationCommandData, "type">, MessageContextMenuCommandInteraction> {}
63
+
64
+ export class SlashCommandWithSubcommands {
65
+ data: Omit<ChatInputApplicationCommandData, "options" | "type">;
66
+
67
+ constructor(data: typeof this.data) {
68
+ this.data = data;
69
+ }
70
+ }
71
+
72
+ export class SubcommandGroup {
73
+ data: Omit<ApplicationCommandSubGroupData, "options" | "type">;
74
+
75
+ constructor(data: typeof this.data) {
76
+ this.data = data;
77
+ }
78
+ }
79
+
80
+ export class Subcommand {
81
+ data: Omit<ApplicationCommandSubCommandData, "type">;
82
+ handler: Handler<ChatInputCommandInteraction>;
83
+
84
+ constructor({ handler, ...data }: typeof this.data & { handler: Subcommand["handler"] }) {
85
+ this.data = data;
86
+ this.handler = handler;
87
+ }
88
+ }
59
89
 
60
90
  abstract class ComponentHandler<T extends ModalSubmitInteraction | MessageComponentInteraction> {
61
91
  handler: Handler<T>;
@@ -83,6 +113,80 @@ export class EventHandler<T extends keyof ClientEvents> {
83
113
  }
84
114
  }
85
115
 
116
+ async function importAll(
117
+ { directory, recursive }: { directory: string; recursive: boolean },
118
+ consumer: (data: { file: Dirent<string>; relativePath: string; absolutePath: string; item: unknown }) => unknown,
119
+ ) {
120
+ const files = await fs.readdir(path.resolve(directory), { recursive, withFileTypes: true });
121
+
122
+ await Promise.all(
123
+ files.map(async (file) => {
124
+ if (file.isDirectory()) return;
125
+
126
+ const absolutePath = path.resolve(file.parentPath, file.name);
127
+ const relativePath = path.relative(path.resolve(directory), absolutePath);
128
+
129
+ const { default: item } = await import(absolutePath);
130
+
131
+ await consumer({ file, relativePath, absolutePath, item });
132
+ }),
133
+ );
134
+ }
135
+
136
+ export async function loadSubcommands(directory: string): Promise<{
137
+ options: ApplicationCommandSubCommandData[];
138
+ handlers: Map<string, Handler<ChatInputCommandInteraction>>;
139
+ }> {
140
+ if (!(await fs.exists(directory))) throw new Error(`Loading subcommands within a group failed: ${directory} is required but could not be found.`);
141
+
142
+ const options: ApplicationCommandSubCommandData[] = [];
143
+ const handlers = new Map<string, Handler<ChatInputCommandInteraction>>();
144
+
145
+ await importAll({ directory, recursive: false }, async ({ file, relativePath, item }) => {
146
+ if (item instanceof Subcommand) {
147
+ options.push({ ...item.data, type: ApplicationCommandOptionType.Subcommand });
148
+ handlers.set(item.data.name, item.handler);
149
+ } else throw new Error(`Loading commands failed: export from ${relativePath} (third-level in commands folder) was not an instance of Subcommand.`);
150
+
151
+ if (item.data.name !== file.name.replace(/.[^/.]+$/, ""))
152
+ throw new Error(`Code style enforcement: name exported from ${relativePath} does not match the filename`);
153
+ });
154
+
155
+ return { options, handlers };
156
+ }
157
+
158
+ export async function loadSubcommandsAndGroups(directory: string): Promise<{
159
+ options: (ApplicationCommandSubGroupData | ApplicationCommandSubCommandData)[];
160
+ handler: Handler<ChatInputCommandInteraction>;
161
+ }> {
162
+ if (!(await fs.exists(directory))) throw new Error(`Loading subcomands/groups failed: ${directory} is required but could not be found.`);
163
+
164
+ const options: (ApplicationCommandSubGroupData | ApplicationCommandSubCommandData)[] = [];
165
+ const handlers = new Map<string, Handler<ChatInputCommandInteraction>>();
166
+
167
+ await importAll({ directory, recursive: false }, async ({ file, relativePath, item }) => {
168
+ if (item instanceof Subcommand) {
169
+ options.push({ ...item.data, type: ApplicationCommandOptionType.Subcommand });
170
+ handlers.set(`/${item.data.name}`, item.handler);
171
+ } else if (item instanceof SubcommandGroup) {
172
+ const subcommands = await loadSubcommands(relativePath.replace(/\.[^/.]+$/, ""));
173
+ options.push({ ...item.data, type: ApplicationCommandOptionType.SubcommandGroup, options: subcommands.options });
174
+ subcommands.handlers.entries().forEach(([key, handler]) => handlers.set(`${item.data.name}/${key}`, handler));
175
+ } else
176
+ throw new Error(
177
+ `Loading commands failed: export from ${relativePath} (second-level in commands folder) was not an instance of SubcommandGroup or Subcommand.`,
178
+ );
179
+
180
+ if (item.data.name !== file.name.replace(/.[^/.]+$/, ""))
181
+ throw new Error(`Code style enforcement: name exported from ${relativePath} does not match the filename`);
182
+ });
183
+
184
+ return {
185
+ options,
186
+ handler: (cmd) => handlers.get(`${cmd.options.getSubcommandGroup(false) ?? ""}/${cmd.options.getSubcommand(true)}`)?.(cmd),
187
+ };
188
+ }
189
+
86
190
  export async function loadCommands(client: Client<true>, directory: string, guildId?: string) {
87
191
  const files = await fs.readdir(path.resolve(directory), { recursive: false, withFileTypes: true });
88
192
 
@@ -99,23 +203,30 @@ export async function loadCommands(client: Client<true>, directory: string, guil
99
203
  if (file.isDirectory()) return;
100
204
 
101
205
  const absolutePath = path.resolve(file.parentPath, file.name);
206
+ const relativePath = path.relative(path.resolve(directory), absolutePath);
102
207
 
103
208
  const { default: item } = await import(absolutePath);
104
209
 
105
210
  if (item instanceof SlashCommand) {
211
+ commandData.push({ ...item.data, type: ApplicationCommandType.ChatInput });
106
212
  slashCommandHandlers.set(item.data.name, item.handler);
107
213
  if (item.autocomplete) slashCommandAutocompletes.set(item.data.name, item.autocomplete);
108
214
  } else if (item instanceof UserCommand) {
215
+ commandData.push({ ...item.data, type: ApplicationCommandType.User });
109
216
  userCommandHandlers.set(item.data.name, item.handler);
110
217
  } else if (item instanceof MessageCommand) {
218
+ commandData.push({ ...item.data, type: ApplicationCommandType.Message });
111
219
  messageCommandHandlers.set(item.data.name, item.handler);
220
+ } else if (item instanceof SlashCommandWithSubcommands) {
221
+ const { options, handler } = await loadSubcommandsAndGroups(relativePath.replace(/\.[^/.]+$/, ""));
222
+ commandData.push({ ...item.data, options });
223
+ slashCommandHandlers.set(item.data.name, handler);
112
224
  } else {
113
- throw new Error(
114
- `Loading commands failed: export from ${path.relative(path.resolve(directory), absolutePath)} was not an instance of <Type>Command.`,
115
- );
225
+ throw new Error(`Loading commands failed: export from ${relativePath} was not an instance of <Type>Command.`);
116
226
  }
117
227
 
118
- commandData.push(item.data);
228
+ if (item.data.name !== file.name.replace(/.[^/.]+$/, ""))
229
+ throw new Error(`Code style enforcement: name exported from ${relativePath} does not match the filename`);
119
230
  }),
120
231
  );
121
232
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperneutrino/djs-lite",
3
3
  "private": false,
4
- "version": "1.2.4",
4
+ "version": "1.4.0",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {