@minesa-org/mini-interaction 0.0.3 → 0.0.4

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.
@@ -8,6 +8,7 @@ export type MiniInteractionOptions = {
8
8
  applicationId: string;
9
9
  publicKey: string;
10
10
  commandsDirectory?: string | false;
11
+ componentsDirectory?: string | false;
11
12
  fetchImplementation?: typeof fetch;
12
13
  verifyKeyImplementation?: VerifyKeyFunction;
13
14
  };
@@ -58,14 +59,17 @@ export declare class MiniInteraction {
58
59
  private readonly fetchImpl;
59
60
  private readonly verifyKeyImpl;
60
61
  private readonly commandsDirectory;
62
+ private readonly componentsDirectory;
61
63
  private readonly commands;
62
64
  private readonly componentHandlers;
63
65
  private commandsLoaded;
64
66
  private loadCommandsPromise;
67
+ private componentsLoaded;
68
+ private loadComponentsPromise;
65
69
  /**
66
70
  * Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
67
71
  */
68
- constructor({ applicationId, publicKey, commandsDirectory, fetchImplementation, verifyKeyImplementation, }: MiniInteractionOptions);
72
+ constructor({ applicationId, publicKey, commandsDirectory, componentsDirectory, fetchImplementation, verifyKeyImplementation, }: MiniInteractionOptions);
69
73
  /**
70
74
  * Registers a single command handler with the client.
71
75
  *
@@ -90,6 +94,12 @@ export declare class MiniInteraction {
90
94
  * @param components - The component definitions to register.
91
95
  */
92
96
  useComponents(components: MiniInteractionComponent[]): this;
97
+ /**
98
+ * Recursively loads components from the configured components directory.
99
+ *
100
+ * @param directory - Optional directory override for component discovery.
101
+ */
102
+ loadComponentsFromDirectory(directory?: string): Promise<this>;
93
103
  /**
94
104
  * Recursively loads commands from the configured commands directory.
95
105
  *
@@ -143,19 +153,27 @@ export declare class MiniInteraction {
143
153
  /**
144
154
  * Recursively collects all command module file paths from the target directory.
145
155
  */
146
- private collectCommandFiles;
156
+ private collectModuleFiles;
147
157
  /**
148
158
  * Determines whether the provided file path matches a supported command file extension.
149
159
  */
150
- private isSupportedCommandFile;
160
+ private isSupportedModuleFile;
151
161
  /**
152
162
  * Dynamically imports and validates a command module from disk.
153
163
  */
154
164
  private importCommandModule;
165
+ /**
166
+ * Dynamically imports and validates a component module from disk.
167
+ */
168
+ private importComponentModule;
155
169
  /**
156
170
  * Normalises the request body into a UTF-8 string for signature validation and parsing.
157
171
  */
158
172
  private normalizeBody;
173
+ /**
174
+ * Ensures components have been loaded from disk once before being accessed.
175
+ */
176
+ private ensureComponentsLoaded;
159
177
  /**
160
178
  * Ensures commands have been loaded from disk once before being accessed.
161
179
  */
@@ -7,8 +7,8 @@ import { verifyKey } from "discord-interactions";
7
7
  import { DISCORD_BASE_URL } from "../utils/constants.js";
8
8
  import { createCommandInteraction } from "../utils/CommandInteractionOptions.js";
9
9
  import { createMessageComponentInteraction, } from "../utils/MessageComponentInteraction.js";
10
- /** File extensions that are treated as command modules when auto-loading. */
11
- const SUPPORTED_COMMAND_EXTENSIONS = new Set([
10
+ /** File extensions that are treated as loadable modules when auto-loading. */
11
+ const SUPPORTED_MODULE_EXTENSIONS = new Set([
12
12
  ".js",
13
13
  ".mjs",
14
14
  ".cjs",
@@ -26,14 +26,17 @@ export class MiniInteraction {
26
26
  fetchImpl;
27
27
  verifyKeyImpl;
28
28
  commandsDirectory;
29
+ componentsDirectory;
29
30
  commands = new Map();
30
31
  componentHandlers = new Map();
31
32
  commandsLoaded = false;
32
33
  loadCommandsPromise = null;
34
+ componentsLoaded = false;
35
+ loadComponentsPromise = null;
33
36
  /**
34
37
  * Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
35
38
  */
36
- constructor({ applicationId, publicKey, commandsDirectory, fetchImplementation, verifyKeyImplementation, }) {
39
+ constructor({ applicationId, publicKey, commandsDirectory, componentsDirectory, fetchImplementation, verifyKeyImplementation, }) {
37
40
  if (!applicationId) {
38
41
  throw new Error("[MiniInteraction] applicationId is required");
39
42
  }
@@ -53,6 +56,10 @@ export class MiniInteraction {
53
56
  commandsDirectory === false
54
57
  ? null
55
58
  : this.resolveCommandsDirectory(commandsDirectory);
59
+ this.componentsDirectory =
60
+ componentsDirectory === false
61
+ ? null
62
+ : this.resolveComponentsDirectory(componentsDirectory);
56
63
  }
57
64
  /**
58
65
  * Registers a single command handler with the client.
@@ -111,6 +118,42 @@ export class MiniInteraction {
111
118
  }
112
119
  return this;
113
120
  }
121
+ /**
122
+ * Recursively loads components from the configured components directory.
123
+ *
124
+ * @param directory - Optional directory override for component discovery.
125
+ */
126
+ async loadComponentsFromDirectory(directory) {
127
+ const targetDirectory = directory !== undefined
128
+ ? this.resolveComponentsDirectory(directory)
129
+ : this.componentsDirectory;
130
+ if (!targetDirectory) {
131
+ throw new Error("[MiniInteraction] Components directory support disabled. Provide a directory path.");
132
+ }
133
+ const exists = await this.pathExists(targetDirectory);
134
+ if (!exists) {
135
+ this.componentsLoaded = true;
136
+ console.warn(`[MiniInteraction] Components directory "${targetDirectory}" does not exist. Skipping component auto-load.`);
137
+ return this;
138
+ }
139
+ const files = await this.collectModuleFiles(targetDirectory);
140
+ if (files.length === 0) {
141
+ this.componentsLoaded = true;
142
+ console.warn(`[MiniInteraction] No component files found under "${targetDirectory}".`);
143
+ return this;
144
+ }
145
+ for (const file of files) {
146
+ const components = await this.importComponentModule(file);
147
+ if (components.length === 0) {
148
+ continue;
149
+ }
150
+ for (const component of components) {
151
+ this.useComponent(component);
152
+ }
153
+ }
154
+ this.componentsLoaded = true;
155
+ return this;
156
+ }
114
157
  /**
115
158
  * Recursively loads commands from the configured commands directory.
116
159
  *
@@ -129,7 +172,7 @@ export class MiniInteraction {
129
172
  console.warn(`[MiniInteraction] Commands directory "${targetDirectory}" does not exist. Skipping command auto-load.`);
130
173
  return this;
131
174
  }
132
- const files = await this.collectCommandFiles(targetDirectory);
175
+ const files = await this.collectModuleFiles(targetDirectory);
133
176
  if (files.length === 0) {
134
177
  this.commandsLoaded = true;
135
178
  console.warn(`[MiniInteraction] No command files found under "${targetDirectory}".`);
@@ -385,7 +428,7 @@ export class MiniInteraction {
385
428
  /**
386
429
  * Recursively collects all command module file paths from the target directory.
387
430
  */
388
- async collectCommandFiles(directory) {
431
+ async collectModuleFiles(directory) {
389
432
  const entries = await readdir(directory, { withFileTypes: true });
390
433
  const files = [];
391
434
  for (const entry of entries) {
@@ -394,11 +437,11 @@ export class MiniInteraction {
394
437
  }
395
438
  const fullPath = path.join(directory, entry.name);
396
439
  if (entry.isDirectory()) {
397
- const nestedFiles = await this.collectCommandFiles(fullPath);
440
+ const nestedFiles = await this.collectModuleFiles(fullPath);
398
441
  files.push(...nestedFiles);
399
442
  continue;
400
443
  }
401
- if (entry.isFile() && this.isSupportedCommandFile(fullPath)) {
444
+ if (entry.isFile() && this.isSupportedModuleFile(fullPath)) {
402
445
  files.push(fullPath);
403
446
  }
404
447
  }
@@ -407,8 +450,8 @@ export class MiniInteraction {
407
450
  /**
408
451
  * Determines whether the provided file path matches a supported command file extension.
409
452
  */
410
- isSupportedCommandFile(filePath) {
411
- return SUPPORTED_COMMAND_EXTENSIONS.has(path.extname(filePath).toLowerCase());
453
+ isSupportedModuleFile(filePath) {
454
+ return SUPPORTED_MODULE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
412
455
  }
413
456
  /**
414
457
  * Dynamically imports and validates a command module from disk.
@@ -441,6 +484,47 @@ export class MiniInteraction {
441
484
  return null;
442
485
  }
443
486
  }
487
+ /**
488
+ * Dynamically imports and validates a component module from disk.
489
+ */
490
+ async importComponentModule(absolutePath) {
491
+ try {
492
+ const moduleUrl = pathToFileURL(absolutePath).href;
493
+ const imported = await import(moduleUrl);
494
+ const candidate = imported.default ??
495
+ imported.component ??
496
+ imported.components ??
497
+ imported.componentDefinition ??
498
+ imported;
499
+ const candidates = Array.isArray(candidate)
500
+ ? candidate
501
+ : [candidate];
502
+ const components = [];
503
+ for (const item of candidates) {
504
+ if (!item || typeof item !== "object") {
505
+ continue;
506
+ }
507
+ const { customId, handler } = item;
508
+ if (typeof customId !== "string") {
509
+ console.warn(`[MiniInteraction] Component module "${absolutePath}" is missing "customId". Skipping.`);
510
+ continue;
511
+ }
512
+ if (typeof handler !== "function") {
513
+ console.warn(`[MiniInteraction] Component module "${absolutePath}" is missing a "handler" function. Skipping.`);
514
+ continue;
515
+ }
516
+ components.push({ customId, handler });
517
+ }
518
+ if (components.length === 0) {
519
+ console.warn(`[MiniInteraction] Component module "${absolutePath}" did not export any valid components. Skipping.`);
520
+ }
521
+ return components;
522
+ }
523
+ catch (error) {
524
+ console.error(`[MiniInteraction] Failed to load component module "${absolutePath}":`, error);
525
+ return [];
526
+ }
527
+ }
444
528
  /**
445
529
  * Normalises the request body into a UTF-8 string for signature validation and parsing.
446
530
  */
@@ -450,6 +534,20 @@ export class MiniInteraction {
450
534
  }
451
535
  return Buffer.from(body).toString("utf8");
452
536
  }
537
+ /**
538
+ * Ensures components have been loaded from disk once before being accessed.
539
+ */
540
+ async ensureComponentsLoaded() {
541
+ if (this.componentsLoaded || this.componentsDirectory === null) {
542
+ return;
543
+ }
544
+ if (!this.loadComponentsPromise) {
545
+ this.loadComponentsPromise = this.loadComponentsFromDirectory().then(() => {
546
+ this.loadComponentsPromise = null;
547
+ });
548
+ }
549
+ await this.loadComponentsPromise;
550
+ }
453
551
  /**
454
552
  * Ensures commands have been loaded from disk once before being accessed.
455
553
  */
@@ -546,6 +644,7 @@ export class MiniInteraction {
546
644
  },
547
645
  };
548
646
  }
647
+ await this.ensureComponentsLoaded();
549
648
  const handler = this.componentHandlers.get(customId);
550
649
  if (!handler) {
551
650
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",