@hyperneutrino/djs-lite 1.2.3 → 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 +114 -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
 
@@ -96,7 +199,10 @@ export async function loadCommands(client: Client<true>, directory: string, guil
96
199
 
97
200
  await Promise.all(
98
201
  files.map(async (file) => {
202
+ if (file.isDirectory()) return;
203
+
99
204
  const absolutePath = path.resolve(file.parentPath, file.name);
205
+ const relativePath = path.relative(path.resolve(directory), absolutePath);
100
206
 
101
207
  const { default: item } = await import(absolutePath);
102
208
 
@@ -107,12 +213,17 @@ export async function loadCommands(client: Client<true>, directory: string, guil
107
213
  userCommandHandlers.set(item.data.name, item.handler);
108
214
  } else if (item instanceof MessageCommand) {
109
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);
110
220
  } else {
111
- throw new Error(
112
- `Loading commands failed: export from ${path.relative(path.resolve(directory), absolutePath)} was not an instance of <Type>Command.`,
113
- );
221
+ throw new Error(`Loading commands failed: export from ${relativePath} was not an instance of <Type>Command.`);
114
222
  }
115
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
+
116
227
  commandData.push(item.data);
117
228
  }),
118
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.3",
4
+ "version": "1.3.0",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
7
  "devDependencies": {