@minesa-org/mini-interaction 0.0.2 → 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
  */
@@ -169,7 +187,7 @@ export declare class MiniInteraction {
169
187
  */
170
188
  private resolveComponentsDirectory;
171
189
  /**
172
- * Resolves a directory relative to the compiled file with optional overrides.
190
+ * Resolves a directory relative to the project "src" or "dist" folders with optional overrides.
173
191
  */
174
192
  private resolveDirectory;
175
193
  /**
@@ -1,13 +1,14 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { readdir, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { pathToFileURL } from "node:url";
4
5
  import { InteractionResponseType, InteractionType, } from "discord-api-types/v10";
5
6
  import { verifyKey } from "discord-interactions";
6
7
  import { DISCORD_BASE_URL } from "../utils/constants.js";
7
8
  import { createCommandInteraction } from "../utils/CommandInteractionOptions.js";
8
9
  import { createMessageComponentInteraction, } from "../utils/MessageComponentInteraction.js";
9
- /** File extensions that are treated as command modules when auto-loading. */
10
- 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([
11
12
  ".js",
12
13
  ".mjs",
13
14
  ".cjs",
@@ -25,14 +26,17 @@ export class MiniInteraction {
25
26
  fetchImpl;
26
27
  verifyKeyImpl;
27
28
  commandsDirectory;
29
+ componentsDirectory;
28
30
  commands = new Map();
29
31
  componentHandlers = new Map();
30
32
  commandsLoaded = false;
31
33
  loadCommandsPromise = null;
34
+ componentsLoaded = false;
35
+ loadComponentsPromise = null;
32
36
  /**
33
37
  * Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
34
38
  */
35
- constructor({ applicationId, publicKey, commandsDirectory, fetchImplementation, verifyKeyImplementation, }) {
39
+ constructor({ applicationId, publicKey, commandsDirectory, componentsDirectory, fetchImplementation, verifyKeyImplementation, }) {
36
40
  if (!applicationId) {
37
41
  throw new Error("[MiniInteraction] applicationId is required");
38
42
  }
@@ -52,6 +56,10 @@ export class MiniInteraction {
52
56
  commandsDirectory === false
53
57
  ? null
54
58
  : this.resolveCommandsDirectory(commandsDirectory);
59
+ this.componentsDirectory =
60
+ componentsDirectory === false
61
+ ? null
62
+ : this.resolveComponentsDirectory(componentsDirectory);
55
63
  }
56
64
  /**
57
65
  * Registers a single command handler with the client.
@@ -110,6 +118,42 @@ export class MiniInteraction {
110
118
  }
111
119
  return this;
112
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
+ }
113
157
  /**
114
158
  * Recursively loads commands from the configured commands directory.
115
159
  *
@@ -128,7 +172,7 @@ export class MiniInteraction {
128
172
  console.warn(`[MiniInteraction] Commands directory "${targetDirectory}" does not exist. Skipping command auto-load.`);
129
173
  return this;
130
174
  }
131
- const files = await this.collectCommandFiles(targetDirectory);
175
+ const files = await this.collectModuleFiles(targetDirectory);
132
176
  if (files.length === 0) {
133
177
  this.commandsLoaded = true;
134
178
  console.warn(`[MiniInteraction] No command files found under "${targetDirectory}".`);
@@ -384,7 +428,7 @@ export class MiniInteraction {
384
428
  /**
385
429
  * Recursively collects all command module file paths from the target directory.
386
430
  */
387
- async collectCommandFiles(directory) {
431
+ async collectModuleFiles(directory) {
388
432
  const entries = await readdir(directory, { withFileTypes: true });
389
433
  const files = [];
390
434
  for (const entry of entries) {
@@ -393,11 +437,11 @@ export class MiniInteraction {
393
437
  }
394
438
  const fullPath = path.join(directory, entry.name);
395
439
  if (entry.isDirectory()) {
396
- const nestedFiles = await this.collectCommandFiles(fullPath);
440
+ const nestedFiles = await this.collectModuleFiles(fullPath);
397
441
  files.push(...nestedFiles);
398
442
  continue;
399
443
  }
400
- if (entry.isFile() && this.isSupportedCommandFile(fullPath)) {
444
+ if (entry.isFile() && this.isSupportedModuleFile(fullPath)) {
401
445
  files.push(fullPath);
402
446
  }
403
447
  }
@@ -406,8 +450,8 @@ export class MiniInteraction {
406
450
  /**
407
451
  * Determines whether the provided file path matches a supported command file extension.
408
452
  */
409
- isSupportedCommandFile(filePath) {
410
- return SUPPORTED_COMMAND_EXTENSIONS.has(path.extname(filePath).toLowerCase());
453
+ isSupportedModuleFile(filePath) {
454
+ return SUPPORTED_MODULE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
411
455
  }
412
456
  /**
413
457
  * Dynamically imports and validates a command module from disk.
@@ -440,6 +484,47 @@ export class MiniInteraction {
440
484
  return null;
441
485
  }
442
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
+ }
443
528
  /**
444
529
  * Normalises the request body into a UTF-8 string for signature validation and parsing.
445
530
  */
@@ -449,6 +534,20 @@ export class MiniInteraction {
449
534
  }
450
535
  return Buffer.from(body).toString("utf8");
451
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
+ }
452
551
  /**
453
552
  * Ensures commands have been loaded from disk once before being accessed.
454
553
  */
@@ -467,27 +566,70 @@ export class MiniInteraction {
467
566
  * Resolves the absolute commands directory path from configuration.
468
567
  */
469
568
  resolveCommandsDirectory(commandsDirectory) {
470
- return this.resolveDirectory("../commands", commandsDirectory);
569
+ return this.resolveDirectory("commands", commandsDirectory);
471
570
  }
472
571
  /**
473
572
  * Resolves the absolute components directory path from configuration.
474
573
  */
475
574
  resolveComponentsDirectory(componentsDirectory) {
476
- return this.resolveDirectory("../components", componentsDirectory);
575
+ return this.resolveDirectory("components", componentsDirectory);
477
576
  }
478
577
  /**
479
- * Resolves a directory relative to the compiled file with optional overrides.
578
+ * Resolves a directory relative to the project "src" or "dist" folders with optional overrides.
480
579
  */
481
- resolveDirectory(defaultRelativePath, overrideDirectory) {
482
- const __filename = fileURLToPath(import.meta.url);
483
- const __dirname = path.dirname(__filename);
484
- const defaultDir = path.resolve(__dirname, defaultRelativePath);
485
- if (!overrideDirectory) {
486
- return defaultDir;
487
- }
488
- return path.isAbsolute(overrideDirectory)
489
- ? overrideDirectory
490
- : path.resolve(process.cwd(), overrideDirectory);
580
+ resolveDirectory(defaultFolder, overrideDirectory) {
581
+ const projectRoot = process.cwd();
582
+ const allowedRoots = ["src", "dist"].map((folder) => path.resolve(projectRoot, folder));
583
+ const candidates = [];
584
+ const isWithin = (parent, child) => {
585
+ const relative = path.relative(parent, child);
586
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
587
+ };
588
+ const pushCandidate = (candidate) => {
589
+ if (!candidates.includes(candidate)) {
590
+ candidates.push(candidate);
591
+ }
592
+ };
593
+ const ensureWithinAllowedRoots = (absolutePath) => {
594
+ if (!allowedRoots.some((root) => isWithin(root, absolutePath))) {
595
+ throw new Error(`[MiniInteraction] Directory overrides must be located within "${path.join(projectRoot, "src")}" or "${path.join(projectRoot, "dist")}". Received: ${absolutePath}`);
596
+ }
597
+ pushCandidate(absolutePath);
598
+ };
599
+ const addOverrideCandidates = (overridePath) => {
600
+ const trimmed = overridePath.trim();
601
+ if (!trimmed) {
602
+ return;
603
+ }
604
+ if (path.isAbsolute(trimmed)) {
605
+ ensureWithinAllowedRoots(trimmed);
606
+ return;
607
+ }
608
+ const normalised = trimmed.replace(/^[./\\]+/, "");
609
+ if (!normalised) {
610
+ return;
611
+ }
612
+ if (normalised.startsWith("src") || normalised.startsWith("dist")) {
613
+ const absolutePath = path.resolve(projectRoot, normalised);
614
+ ensureWithinAllowedRoots(absolutePath);
615
+ return;
616
+ }
617
+ for (const root of allowedRoots) {
618
+ ensureWithinAllowedRoots(path.resolve(root, normalised));
619
+ }
620
+ };
621
+ if (overrideDirectory) {
622
+ addOverrideCandidates(overrideDirectory);
623
+ }
624
+ for (const root of allowedRoots) {
625
+ pushCandidate(path.resolve(root, defaultFolder));
626
+ }
627
+ for (const candidate of candidates) {
628
+ if (existsSync(candidate)) {
629
+ return candidate;
630
+ }
631
+ }
632
+ return candidates[0];
491
633
  }
492
634
  /**
493
635
  * Handles execution of a message component interaction.
@@ -502,6 +644,7 @@ export class MiniInteraction {
502
644
  },
503
645
  };
504
646
  }
647
+ await this.ensureComponentsLoaded();
505
648
  const handler = this.componentHandlers.get(customId);
506
649
  if (!handler) {
507
650
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.0.2",
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",