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