@mostfeatured/dbi 0.2.17 → 0.2.18

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 (77) hide show
  1. package/dist/src/types/Event.d.ts +21 -13
  2. package/dist/src/types/Event.d.ts.map +1 -1
  3. package/dist/src/types/Event.js.map +1 -1
  4. package/dist/test/index.js +1 -1
  5. package/dist/test/index.js.map +1 -1
  6. package/generated/namespaceData.d.ts +3 -1
  7. package/package.json +6 -2
  8. package/.gitattributes +0 -2
  9. package/.hintrc +0 -8
  10. package/.vscode/settings.json +0 -3
  11. package/docs/ADVANCED_FEATURES.md +0 -840
  12. package/docs/API_REFERENCE.md +0 -929
  13. package/docs/CHAT_INPUT.md +0 -811
  14. package/docs/COMPONENTS.md +0 -1039
  15. package/docs/EVENTS.md +0 -568
  16. package/docs/GETTING_STARTED.md +0 -398
  17. package/docs/LOCALIZATION.md +0 -777
  18. package/docs/README.md +0 -345
  19. package/docs/SVELTE_COMPONENTS.md +0 -1111
  20. package/docs/llm/ADVANCED_FEATURES.txt +0 -521
  21. package/docs/llm/API_REFERENCE.txt +0 -659
  22. package/docs/llm/CHAT_INPUT.txt +0 -514
  23. package/docs/llm/COMPONENTS.txt +0 -595
  24. package/docs/llm/EVENTS.txt +0 -449
  25. package/docs/llm/GETTING_STARTED.txt +0 -296
  26. package/docs/llm/LOCALIZATION.txt +0 -501
  27. package/docs/llm/README.txt +0 -193
  28. package/docs/llm/SVELTE_COMPONENTS.txt +0 -566
  29. package/src/DBI.ts +0 -1007
  30. package/src/Events.ts +0 -189
  31. package/src/data/eventMap.json +0 -248
  32. package/src/index.ts +0 -23
  33. package/src/methods/handleMessageCommands.ts +0 -482
  34. package/src/methods/hookEventListeners.ts +0 -119
  35. package/src/methods/hookInteractionListeners.ts +0 -314
  36. package/src/methods/publishInteractions.ts +0 -256
  37. package/src/types/ApplicationRoleConnectionMetadata.ts +0 -19
  38. package/src/types/Builders/ButtonBuilder.ts +0 -53
  39. package/src/types/Builders/ChannelSelectMenuBuilder.ts +0 -53
  40. package/src/types/Builders/MentionableSelectMenuBuilder.ts +0 -53
  41. package/src/types/Builders/ModalBuilder.ts +0 -53
  42. package/src/types/Builders/RoleSelectMenuBuilder.ts +0 -53
  43. package/src/types/Builders/StringSelectMenuBuilder.ts +0 -53
  44. package/src/types/Builders/UserSelectMenuBuilder.ts +0 -53
  45. package/src/types/ChatInput/ChatInput.ts +0 -28
  46. package/src/types/ChatInput/ChatInputOptions.ts +0 -388
  47. package/src/types/Components/Button.ts +0 -39
  48. package/src/types/Components/ChannelSelectMenu.ts +0 -43
  49. package/src/types/Components/HTMLComponentsV2/HTMLComponentsV2Handlers.ts +0 -78
  50. package/src/types/Components/HTMLComponentsV2/index.ts +0 -800
  51. package/src/types/Components/HTMLComponentsV2/parser.ts +0 -649
  52. package/src/types/Components/HTMLComponentsV2/svelteParser.ts +0 -1503
  53. package/src/types/Components/HTMLComponentsV2/svelteRenderer.ts +0 -416
  54. package/src/types/Components/MentionableSelectMenu.ts +0 -43
  55. package/src/types/Components/Modal.ts +0 -46
  56. package/src/types/Components/RoleSelectMenu.ts +0 -43
  57. package/src/types/Components/StringSelectMenu.ts +0 -43
  58. package/src/types/Components/UserSelectMenu.ts +0 -43
  59. package/src/types/Event.ts +0 -145
  60. package/src/types/Interaction.ts +0 -100
  61. package/src/types/other/CustomEvent.ts +0 -19
  62. package/src/types/other/FakeMessageInteraction.ts +0 -408
  63. package/src/types/other/InteractionLocale.ts +0 -34
  64. package/src/types/other/Locale.ts +0 -70
  65. package/src/types/other/MessageContextMenu.ts +0 -27
  66. package/src/types/other/UserContextMenu.ts +0 -25
  67. package/src/utils/MemoryStore.ts +0 -28
  68. package/src/utils/UtilTypes.ts +0 -11
  69. package/src/utils/customId.ts +0 -49
  70. package/src/utils/permissions.ts +0 -5
  71. package/src/utils/recursiveImport.ts +0 -35
  72. package/src/utils/recursiveUnload.ts +0 -25
  73. package/src/utils/unloadModule.ts +0 -7
  74. package/test/index.ts +0 -176
  75. package/test/product-showcase.svelte +0 -558
  76. package/test/test.ts +0 -3
  77. package/tsconfig.json +0 -51
@@ -1,800 +0,0 @@
1
- import { NamespaceEnums } from "../../../../generated/namespaceData";
2
- import { DBI } from "../../../DBI";
3
- import { DBIBaseInteraction, DBIRateLimit, IDBIBaseExecuteCtx, TDBIReferencedData } from "../../Interaction";
4
- import { parseHTMLComponentsV2 } from "./parser";
5
- import { renderSvelteComponent, renderSvelteComponentFromFile, SvelteRenderResult } from "./svelteRenderer";
6
- import { parseSvelteComponent, createHandlerContext, HandlerContextResult } from "./svelteParser";
7
- import fs from "fs";
8
-
9
- /**
10
- * Parse Discord API error and provide helpful context about which HTML element caused the error
11
- */
12
- function parseDiscordAPIError(error: any, source?: string, componentName?: string): Error {
13
- // Check if it's a Discord API error with form body issues
14
- const message = error.message || '';
15
- const rawError = error.rawError || error;
16
-
17
- // Extract the path from error message like "data.components[0].components[0].accessory.media.url"
18
- const pathMatch = message.match(/data\.components(\[[^\]]+\](?:\.[^\[\s\[]+|\[[^\]]+\])*)/);
19
-
20
- if (!pathMatch) {
21
- return error; // Not a parseable Discord API error
22
- }
23
-
24
- const errorPath = pathMatch[1];
25
- const errorCode = message.match(/\[([A-Z_]+)\]/)?.[1] || 'UNKNOWN';
26
-
27
- // Parse the path to understand the component structure
28
- // e.g., "[0].components[0].accessory.media.url" -> component index 0, child 0, accessory.media.url
29
- const parts = errorPath.split(/\.|\[|\]/).filter(Boolean);
30
-
31
- // Build a human-readable path description
32
- let description = '';
33
- let elementHint = '';
34
- let currentPath = [];
35
-
36
- for (let i = 0; i < parts.length; i++) {
37
- const part = parts[i];
38
- if (!isNaN(Number(part))) {
39
- currentPath.push(`[${part}]`);
40
- } else {
41
- currentPath.push(part);
42
-
43
- // Provide hints based on known Discord component structure
44
- if (part === 'accessory') {
45
- elementHint = '<section> element\'s accessory (thumbnail/button)';
46
- } else if (part === 'media') {
47
- elementHint = '<thumbnail> or <media-gallery> element';
48
- } else if (part === 'url') {
49
- elementHint = 'media URL attribute';
50
- } else if (part === 'components') {
51
- // Skip, it's container
52
- } else if (part === 'content') {
53
- elementHint = 'text content of a <text-display> element';
54
- } else if (part === 'label') {
55
- elementHint = 'label attribute of a <button> or <option>';
56
- } else if (part === 'custom_id') {
57
- elementHint = 'name attribute of an interactive element';
58
- } else if (part === 'placeholder') {
59
- elementHint = 'placeholder attribute of a select menu';
60
- } else if (part === 'title') {
61
- elementHint = 'title attribute of a <modal> or <section>';
62
- } else if (part === 'options') {
63
- elementHint = '<option> elements inside a <string-select>';
64
- } else if (part === 'value') {
65
- elementHint = 'value attribute of an <option> or <text-input>';
66
- } else if (part === 'description') {
67
- elementHint = 'description attribute of an <option>';
68
- }
69
- }
70
- }
71
-
72
- // Map error codes to helpful messages
73
- const errorMessages: Record<string, string> = {
74
- 'BASE_TYPE_REQUIRED': 'This field is required but was empty or undefined',
75
- 'STRING_TYPE_REQUIRED': 'This field must be a string',
76
- 'NUMBER_TYPE_REQUIRED': 'This field must be a number',
77
- 'BOOLEAN_TYPE_REQUIRED': 'This field must be a boolean',
78
- 'INVALID_URL': 'The URL provided is not valid',
79
- 'MAX_LENGTH': 'The value exceeds the maximum allowed length',
80
- 'MIN_LENGTH': 'The value is shorter than the minimum required length',
81
- 'CHOICE_NOT_FOUND': 'The selected value is not in the list of options',
82
- };
83
-
84
- const errorExplanation = errorMessages[errorCode] || `Error code: ${errorCode}`;
85
-
86
- // Build the enhanced error message
87
- let enhancedMessage = `
88
- ╔══════════════════════════════════════════════════════════════╗
89
- ║ Discord API Error - Invalid Component Data ║
90
- ╚══════════════════════════════════════════════════════════════╝
91
-
92
- 📍 Component: ${componentName || 'unknown'}
93
- 📍 Error Path: data.components${errorPath}
94
- 📍 Error Code: ${errorCode}
95
-
96
- ❌ ${errorExplanation}
97
- ${elementHint ? `\n💡 This error is likely in: ${elementHint}` : ''}
98
-
99
- 🔍 What to check:
100
- `;
101
-
102
- // Add specific suggestions based on error type
103
- if (errorPath.includes('media.url')) {
104
- enhancedMessage += ` • Make sure the image/media URL is provided and valid
105
- • Check that the data property used for the image exists
106
- • Example: <thumbnail media={product?.image}> - is product.image defined?
107
- `;
108
- } else if (errorPath.includes('accessory')) {
109
- enhancedMessage += ` • The <section> element requires valid content in its accessory
110
- • If using <thumbnail>, ensure the media URL is valid
111
- `;
112
- } else if (errorPath.includes('content')) {
113
- enhancedMessage += ` • Text content cannot be empty or undefined
114
- • Check your template expressions like {variable?.property}
115
- `;
116
- } else if (errorPath.includes('options')) {
117
- enhancedMessage += ` • Select menu options must have valid value and label
118
- • Each <option> needs: value="..." and text content
119
- `;
120
- } else if (errorPath.includes('label') || errorPath.includes('custom_id')) {
121
- enhancedMessage += ` • Interactive elements (buttons, selects) need valid labels/names
122
- • Check that text content and name attributes are not empty
123
- `;
124
- }
125
-
126
- // If we have source, try to highlight relevant section
127
- if (source && elementHint) {
128
- const elementType = elementHint.match(/<(\w+-?\w*)>/)?.[1];
129
- if (elementType) {
130
- const elementRegex = new RegExp(`<${elementType}[^>]*>`, 'g');
131
- const matches = source.match(elementRegex);
132
- if (matches && matches.length > 0) {
133
- enhancedMessage += `\n📝 Found ${matches.length} <${elementType}> element(s) in template:`;
134
- matches.slice(0, 3).forEach((m, i) => {
135
- enhancedMessage += `\n ${i + 1}. ${m.substring(0, 80)}${m.length > 80 ? '...' : ''}`;
136
- });
137
- if (matches.length > 3) {
138
- enhancedMessage += `\n ... and ${matches.length - 3} more`;
139
- }
140
- }
141
- }
142
- }
143
-
144
- const enhancedError = new Error(enhancedMessage);
145
- (enhancedError as any).originalError = error;
146
- (enhancedError as any).type = 'discord-api-error';
147
- (enhancedError as any).path = errorPath;
148
- (enhancedError as any).code = errorCode;
149
-
150
- return enhancedError;
151
- }
152
-
153
- export type TDBIHTMLComponentsV2Omitted<TNamespace extends NamespaceEnums> = Omit<DBIHTMLComponentsV2<TNamespace>, "type" | "dbi" | "toJSON" | "description" | "send" | "destroy" | "destroyAll" | "__lastRenderModals__" | "_pendingModals" | "_initPromise"> & {
154
- /**
155
- * Use 'svelte' for Svelte 5 components, 'eta' for Eta templates (default)
156
- */
157
- mode?: 'svelte' | 'eta';
158
- /**
159
- * Callback executed when the component interaction is triggered
160
- */
161
- onExecute?: (ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) => void;
162
- };
163
-
164
- export type TDBIHTMLComponentsV2ToJSONArgs = {
165
- data?: Record<string, any>;
166
- }
167
-
168
- export interface TDBIHTMLComponentsV2SendOptions {
169
- data?: Record<string, any>;
170
- flags?: string[];
171
- content?: string;
172
- ephemeral?: boolean;
173
- /** If true, uses interaction.reply(). If false or unset, auto-detects based on target type */
174
- reply?: boolean;
175
- /** If true, uses interaction.followUp() instead of reply() */
176
- followUp?: boolean;
177
- }
178
-
179
- export interface IDBIHTMLComponentsV2ExecuteCtx<TNamespace extends NamespaceEnums> extends IDBIBaseExecuteCtx<TNamespace> {
180
- data: TDBIReferencedData[];
181
- }
182
-
183
- export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIBaseInteraction<TNamespace> {
184
- template?: string;
185
- file?: string;
186
- mode: 'svelte' | 'eta' = 'eta';
187
- private svelteComponentInfo: any = null;
188
- private _userOnExecute?: (ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) => void;
189
- /** The directory of the source file (used for resolving relative imports) */
190
- private _sourceDir?: string;
191
-
192
- // Store handler contexts per ref for lifecycle management
193
- // Key: ref id, Value: handlerContext with lifecycle hooks
194
- private _activeContexts: Map<string, any> = new Map();
195
-
196
- // Store pending modal promises for await showModal() support
197
- // Key: modal customId, Value: { resolve, reject } functions
198
- _pendingModals: Map<string, { resolve: (result: any) => void; reject: (error: any) => void }> = new Map();
199
-
200
- // Track initialization promise
201
- private _initPromise: Promise<void> | null = null;
202
-
203
- constructor(dbi: DBI<TNamespace>, args: TDBIHTMLComponentsV2Omitted<TNamespace>) {
204
- // Store user's onExecute callback before passing to super
205
- const userOnExecute = (args as any).onExecute;
206
-
207
- // Remove onExecute from args so it doesn't override the class method
208
- const argsWithoutOnExecute = { ...args };
209
- delete (argsWithoutOnExecute as any).onExecute;
210
-
211
- super(dbi, {
212
- ...(argsWithoutOnExecute as any),
213
- type: "HTMLComponentsV2",
214
- });
215
- this.template = args.template || (args.file ? fs.readFileSync(args.file, "utf-8") : undefined);
216
- this.file = args.file;
217
- this.name = args.name;
218
- this.handlers = args.handlers || [];
219
- this.mode = args.mode || 'eta';
220
-
221
- // Store source directory for resolving relative imports in Svelte components
222
- if (this.file) {
223
- const path = require("path");
224
- this._sourceDir = path.dirname(path.resolve(this.file));
225
- }
226
-
227
- // Store user's onExecute callback if provided
228
- if (userOnExecute) {
229
- this._userOnExecute = userOnExecute;
230
- }
231
-
232
- // Pre-extract Svelte handlers at registration time (lazy loaded)
233
- if (this.mode === 'svelte' && this.template) {
234
- // Store the promise so we can await it in _handleExecute
235
- this._initPromise = this._initSvelteComponent();
236
- }
237
-
238
- // Re-assign onExecute method after super() call because parent class sets it to undefined
239
- this.onExecute = this._handleExecute.bind(this);
240
- }
241
-
242
- private async _initSvelteComponent() {
243
- if (this.template && !this.svelteComponentInfo) {
244
- this.svelteComponentInfo = await parseSvelteComponent(this.template);
245
- }
246
- }
247
-
248
- private async _handleExecute(ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) {
249
- // Wait for Svelte component initialization if not yet completed
250
- if (this._initPromise) {
251
- await this._initPromise;
252
- }
253
-
254
- // Call user's onExecute callback first if provided
255
- if (this._userOnExecute) {
256
- this._userOnExecute(ctx);
257
- }
258
-
259
- // If using Svelte mode, find and execute the handler
260
- if (this.mode === 'svelte' && this.svelteComponentInfo) {
261
- const [elementName, ...handlerData] = ctx.data;
262
-
263
- if (typeof elementName === 'string') {
264
- // Check if this is a modal submit (elementName is the modal id)
265
- const modalHandlerInfo = this.svelteComponentInfo.modalHandlers.get(elementName);
266
-
267
- if (modalHandlerInfo) {
268
- // This is a modal submit - execute the onsubmit handler (or just resolve promise if no handler)
269
- this._executeModalSubmit(ctx, elementName, modalHandlerInfo.onsubmitHandler, handlerData);
270
- return;
271
- }
272
-
273
- // Find the handler info for this element (button, select, etc.)
274
- const handlerInfo = this.svelteComponentInfo.handlers.get(elementName);
275
-
276
- if (handlerInfo) {
277
- this._executeElementHandler(ctx, handlerInfo, handlerData);
278
- }
279
- }
280
- }
281
- }
282
-
283
- /**
284
- * Execute a modal submit handler
285
- */
286
- private _executeModalSubmit(
287
- ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>,
288
- modalId: string,
289
- handlerName: string | undefined,
290
- handlerData: any[]
291
- ) {
292
- // Extract current state from handlerData (refs that were passed)
293
- const currentState = handlerData[0] || {} as Record<string, any>;
294
-
295
- // Get ref id for lifecycle tracking (if available)
296
- const refId = (currentState as any)?.$ref || null;
297
-
298
- // Extract all field values from modal interaction (text inputs, selects, file uploads, etc.)
299
- const modalInteraction = ctx.interaction as any;
300
- const fields = this._extractModalFields(modalInteraction);
301
-
302
- // Check if there's a pending promise for this modal (from await showModal())
303
- // Use the modal's customId to find the pending promise
304
- const pendingKey = modalInteraction.customId;
305
- const pendingModal = this._pendingModals.get(pendingKey);
306
-
307
- if (pendingModal) {
308
- // Resolve the promise with fields and interaction
309
- pendingModal.resolve({
310
- fields,
311
- interaction: modalInteraction,
312
- ctx: ctx
313
- });
314
- this._pendingModals.delete(pendingKey);
315
- }
316
-
317
- // If no onsubmit handler defined, just return (promise-based usage)
318
- if (!handlerName) {
319
- return;
320
- }
321
-
322
- // Create handler context for modal submit
323
- const handlerContext = createHandlerContext(
324
- this.svelteComponentInfo!.scriptContent,
325
- typeof currentState === 'object' ? currentState : {},
326
- this,
327
- ctx,
328
- this._sourceDir
329
- );
330
-
331
- const handlerFn = handlerContext.handlers[handlerName];
332
-
333
- if (handlerFn && typeof handlerFn === 'function') {
334
- try {
335
- // Store context for lifecycle management
336
- if (refId) {
337
- const existingContext = this._activeContexts.get(refId);
338
- if (existingContext && existingContext.destroyCallbacks) {
339
- handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
340
- }
341
- this._activeContexts.set(refId, handlerContext);
342
- }
343
-
344
- handlerContext.setInHandler(true);
345
-
346
- try {
347
- // Call handler with ctx and fields object
348
- handlerFn.call(this, handlerContext.wrappedCtx, fields, ...handlerData.slice(1));
349
- } finally {
350
- handlerContext.setInHandler(false);
351
- }
352
-
353
- handlerContext.runEffects();
354
-
355
- if (handlerContext.hasPendingRender()) {
356
- handlerContext.flushRender();
357
- }
358
- } catch (error) {
359
- // Modal handler execution failed
360
- }
361
- }
362
- }
363
-
364
- /**
365
- * Execute an element handler (button, select, etc.)
366
- */
367
- private _executeElementHandler(
368
- ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>,
369
- handlerInfo: any,
370
- handlerData: any[]
371
- ) {
372
- // Extract current state from handlerData (refs that were passed)
373
- // The second element in data array contains the current state
374
- const currentState = handlerData[0] || {} as Record<string, any>;
375
-
376
- // Get ref id for lifecycle tracking (if available)
377
- const refId = (currentState as any)?.$ref || null;
378
-
379
- // Check if this is first execution for this ref
380
- const isFirstExecution = refId && !this._activeContexts.has(refId);
381
-
382
- // Get existing context if any (to preserve lifecycle callbacks like intervals)
383
- const existingContext = refId ? this._activeContexts.get(refId) : null;
384
-
385
- // Create a NEW handler context for each execution with the current state
386
- // This ensures each interaction has its own isolated state
387
- // Pass 'this' so handlers can access component methods like toJSON()
388
- // Pass ctx so handlers can access ctx.interaction, ctx.data, ctx.locale, etc.
389
- const handlerContext = createHandlerContext(
390
- this.svelteComponentInfo!.scriptContent,
391
- typeof currentState === 'object' ? currentState : {},
392
- this,
393
- ctx,
394
- this._sourceDir
395
- );
396
-
397
- const handlerFn = handlerContext.handlers[handlerInfo.handlerName];
398
-
399
- if (handlerFn && typeof handlerFn === 'function') {
400
- try {
401
- // Store context for lifecycle management
402
- if (refId) {
403
- // If there's an existing context, transfer its destroy callbacks to the new context
404
- // This preserves intervals/timers created in onMount
405
- if (existingContext && existingContext.destroyCallbacks) {
406
- // Merge existing destroy callbacks (don't run them!)
407
- handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
408
- }
409
-
410
- this._activeContexts.set(refId, handlerContext);
411
-
412
- // Wrap $unRef to call onDestroy when ref is deleted (only wrap once)
413
- const stateObj = currentState as any;
414
- if (stateObj.$unRef && !stateObj.__unRefWrapped__) {
415
- const originalUnRef = stateObj.$unRef.bind(stateObj);
416
- stateObj.$unRef = () => {
417
- // Run destroy callbacks before unref
418
- const ctx = this._activeContexts.get(refId);
419
- if (ctx && ctx.runDestroy) {
420
- ctx.runDestroy();
421
- }
422
- this._activeContexts.delete(refId);
423
- return originalUnRef();
424
- };
425
- stateObj.__unRefWrapped__ = true;
426
- }
427
- }
428
-
429
- // Run onMount callbacks only on first execution
430
- if (isFirstExecution && handlerContext.runMount) {
431
- handlerContext.runMount();
432
- }
433
-
434
- // Mark that we're inside handler execution (prevents auto-render during handler)
435
- handlerContext.setInHandler(true);
436
-
437
- try {
438
- // Bind 'this' to the DBIHTMLComponentsV2 instance so handlers can use this.toJSON()
439
- // Pass wrappedCtx so handlers use the proxy-wrapped ctx that tracks interaction calls
440
- // This ensures __asyncInteractionCalled__ flag is set when handler calls ctx.interaction.reply() etc.
441
- handlerFn.call(this, handlerContext.wrappedCtx, ...handlerData.slice(1));
442
- } finally {
443
- // Always reset handler execution flag
444
- handlerContext.setInHandler(false);
445
- }
446
-
447
- // Run effects after handler execution (state may have changed)
448
- handlerContext.runEffects();
449
-
450
- // If there are pending data changes and no interaction method was called,
451
- // flush the render now (synchronously uses interaction.update)
452
- if (handlerContext.hasPendingRender()) {
453
- handlerContext.flushRender();
454
- }
455
- } catch (error) {
456
- // Handler execution failed
457
- }
458
- }
459
- }
460
-
461
- // Store last render's modals for showModal() to access
462
- // Note: Used internally by handler context, not truly private
463
- __lastRenderModals__: Map<string, any> | null = null;
464
-
465
- override async toJSON(arg: TDBIHTMLComponentsV2ToJSONArgs = {}): Promise<any> {
466
- if (this.mode === 'svelte' && this.template) {
467
- // Render Svelte component
468
- const result = await renderSvelteComponent(
469
- this.dbi as any,
470
- this.template,
471
- this.name,
472
- {
473
- data: arg.data,
474
- ttl: this.ttl
475
- }
476
- );
477
-
478
- // Store modals for showModal() to access
479
- this.__lastRenderModals__ = result.modals;
480
-
481
- return result.components;
482
- } else {
483
- // Use Eta template parsing (default)
484
- return parseHTMLComponentsV2(
485
- this.dbi as any,
486
- this.template!,
487
- this.name,
488
- {
489
- data: arg.data,
490
- ttl: this.ttl
491
- }
492
- );
493
- }
494
- }
495
-
496
- /**
497
- * Send the component to an interaction or channel and initialize lifecycle hooks (onMount)
498
- * This is the recommended way to send Svelte components with intervals/timers
499
- *
500
- * @param target - Discord interaction or channel to send to
501
- * @param options - Send options including data, flags, content
502
- *
503
- * @example
504
- * ```ts
505
- * const showcase = dbi.interaction("product-showcase");
506
- *
507
- * // Send as interaction reply
508
- * await showcase.send(interaction, { data: { count: 0 } });
509
- *
510
- * // Send to a channel directly
511
- * await showcase.send(channel, { data: { count: 0 } });
512
- *
513
- * // Send as followUp (if already replied)
514
- * await showcase.send(interaction, { data: { count: 0 }, followUp: true });
515
- * ```
516
- */
517
- async send(target: any, options: TDBIHTMLComponentsV2SendOptions = {}): Promise<any> {
518
- // Wait for Svelte component initialization if not yet completed
519
- if (this._initPromise) {
520
- await this._initPromise;
521
- }
522
-
523
- const { data = {}, flags = ["IsComponentsV2"], content, ephemeral, reply, followUp } = options;
524
-
525
- // Render components (toJSON is async) - this also creates $ref in data if not present
526
- const components = await this.toJSON({ data });
527
-
528
- // Build message options
529
- const messageOptions: any = { components, flags };
530
- if (content) messageOptions.content = content;
531
- if (ephemeral) messageOptions.flags = [...flags, "Ephemeral"];
532
-
533
- // Detect target type and send accordingly
534
- let message: any;
535
- const isInteraction = target.reply && target.user; // Interactions have both reply method and user property
536
- const isChannel = target.send && !target.user; // Channels have send but no user
537
-
538
- try {
539
- if (isInteraction) {
540
- if (followUp) {
541
- message = await target.followUp(messageOptions);
542
- } else {
543
- message = await target.reply(messageOptions);
544
- }
545
- } else if (isChannel) {
546
- message = await target.send(messageOptions);
547
- } else {
548
- throw new Error("Invalid target: must be an interaction or channel");
549
- }
550
- } catch (error: any) {
551
- // Check if it's a Discord API error and enhance it with helpful context
552
- if (error.code || error.rawError || (error.message && error.message.includes('Invalid Form Body'))) {
553
- const source = this.file ? fs.readFileSync(this.file, 'utf-8') : this.template;
554
- throw parseDiscordAPIError(error, source, this.name);
555
- }
556
- throw error;
557
- }
558
-
559
- // If Svelte mode, create initial handler context and run onMount
560
- // After toJSON, data.$ref is guaranteed to exist (created in renderSvelteComponent)
561
- if (this.mode === 'svelte' && this.svelteComponentInfo && data.$ref) {
562
- const refId = data.$ref;
563
-
564
- // Create handler context with a fake ctx that has the message
565
- const fakeCtx = {
566
- interaction: {
567
- replied: true,
568
- deferred: false,
569
- message: message,
570
- }
571
- };
572
-
573
- const handlerContext = createHandlerContext(
574
- this.svelteComponentInfo.scriptContent,
575
- data,
576
- this,
577
- fakeCtx,
578
- this._sourceDir
579
- );
580
-
581
- // Store the context for lifecycle management
582
- this._activeContexts.set(refId, handlerContext);
583
-
584
- // Wrap $unRef to call onDestroy when ref is deleted
585
- if (data.$unRef) {
586
- const originalUnRef = data.$unRef.bind(data);
587
- data.$unRef = () => {
588
- if (handlerContext.runDestroy) {
589
- handlerContext.runDestroy();
590
- }
591
- this._activeContexts.delete(refId);
592
- return originalUnRef();
593
- };
594
- }
595
-
596
- // Run onMount callbacks
597
- handlerContext.runMount();
598
-
599
- // Run initial effects
600
- handlerContext.runEffects();
601
- }
602
-
603
- return message;
604
- }
605
-
606
- /**
607
- * Destroy a component instance by ref ID or data object
608
- * This runs onDestroy callbacks (clears intervals/timers) and removes the ref
609
- *
610
- * @param refOrData - Either a ref ID string or the data object with $ref
611
- * @returns true if destroyed, false if not found
612
- *
613
- * @example
614
- * ```ts
615
- * // Destroy by data object
616
- * showcase.destroy(data);
617
- *
618
- * // Destroy by ref ID
619
- * showcase.destroy(data.$ref);
620
- *
621
- * // Destroy all active instances of this component
622
- * showcase.destroyAll();
623
- * ```
624
- */
625
- destroy(refOrData: string | Record<string, any>): boolean {
626
- const refId = typeof refOrData === 'string' ? refOrData : refOrData?.$ref;
627
-
628
- if (!refId) {
629
- return false;
630
- }
631
-
632
- const context = this._activeContexts.get(refId);
633
- if (context) {
634
- // Run destroy callbacks (clears intervals, timers, etc.)
635
- if (context.runDestroy) {
636
- context.runDestroy();
637
- }
638
- this._activeContexts.delete(refId);
639
- }
640
-
641
- // Also delete from DBI refs store
642
- this.dbi.data.refs.delete(refId);
643
-
644
- return true;
645
- }
646
-
647
- /**
648
- * Extract all field values from a modal interaction
649
- * Supports text inputs, select menus (string, user, role, mentionable, channel), and file uploads
650
- *
651
- * Returns an object where keys are the custom_id of each component and values are:
652
- * - For text inputs: string value
653
- * - For string selects: string[] of selected values
654
- * - For user selects: string[] of user IDs
655
- * - For role selects: string[] of role IDs
656
- * - For mentionable selects: { users: string[], roles: string[] }
657
- * - For channel selects: string[] of channel IDs
658
- * - For file uploads: attachment objects array
659
- */
660
- private _extractModalFields(modalInteraction: any): Record<string, any> {
661
- const fields: Record<string, any> = {};
662
-
663
- // Handle classic text input fields via ModalSubmitFields
664
- if (modalInteraction.fields && modalInteraction.fields.fields) {
665
- for (const [customId, field] of modalInteraction.fields.fields) {
666
- // Text input - field.value is the string
667
- // For select menus, value might be an array
668
- fields[customId] = field.values || field.value;
669
- }
670
- }
671
-
672
- // Handle new modal components from interaction.data.components
673
- // The new modal structure uses Label wrappers with nested components
674
- const data = modalInteraction.data || (modalInteraction as any).data;
675
- if (data && data.components) {
676
- this._extractFieldsFromComponents(data.components, fields);
677
- }
678
-
679
- // Also check for components directly on the interaction (some Discord.js versions)
680
- if (modalInteraction.components && Array.isArray(modalInteraction.components)) {
681
- this._extractFieldsFromInteractionComponents(modalInteraction.components, fields);
682
- }
683
-
684
- return fields;
685
- }
686
-
687
- /**
688
- * Extract fields from Discord.js style interaction components (ActionRow objects)
689
- */
690
- private _extractFieldsFromInteractionComponents(components: any[], fields: Record<string, any>) {
691
- for (const row of components) {
692
- // ActionRow has components property
693
- const rowComponents = row.components || [];
694
- for (const component of rowComponents) {
695
- const customId = component.customId || component.custom_id || component.id;
696
- if (!customId) continue;
697
-
698
- // Check component type
699
- const type = component.type;
700
-
701
- // Text Input (type 4)
702
- if (type === 4 || component.value !== undefined) {
703
- fields[customId] = component.value || '';
704
- }
705
-
706
- // Select menus (types 3, 5, 6, 7, 8) - check for values array
707
- if (component.values !== undefined) {
708
- fields[customId] = component.values;
709
- }
710
- }
711
- }
712
- }
713
-
714
- /**
715
- * Recursively extract field values from component structure
716
- */
717
- private _extractFieldsFromComponents(components: any[], fields: Record<string, any>) {
718
- for (const component of components) {
719
- const type = component.type;
720
- // Support both custom_id and id for backward compatibility
721
- const customId = component.custom_id || component.id;
722
-
723
- // Type 18 = Label/Field - can have nested component or components
724
- if (type === 18) {
725
- if (component.component) {
726
- this._extractFieldsFromComponents([component.component], fields);
727
- }
728
- if (component.components) {
729
- this._extractFieldsFromComponents(component.components, fields);
730
- }
731
- continue;
732
- }
733
-
734
- // Type 1 = Action Row - has nested components array
735
- if (type === 1 && component.components) {
736
- this._extractFieldsFromComponents(component.components, fields);
737
- continue;
738
- }
739
-
740
- if (!customId) continue;
741
-
742
- switch (type) {
743
- case 4: // Text Input
744
- fields[customId] = component.value || '';
745
- break;
746
-
747
- case 3: // String Select
748
- fields[customId] = component.values || [];
749
- break;
750
-
751
- case 5: // User Select
752
- fields[customId] = component.values || [];
753
- break;
754
-
755
- case 6: // Role Select
756
- fields[customId] = component.values || [];
757
- break;
758
-
759
- case 7: // Mentionable Select - can have both users and roles
760
- // Discord returns resolved data for mentionables
761
- fields[customId] = {
762
- values: component.values || [],
763
- users: component.resolved?.users ? Object.keys(component.resolved.users) : [],
764
- roles: component.resolved?.roles ? Object.keys(component.resolved.roles) : []
765
- };
766
- break;
767
-
768
- case 8: // Channel Select
769
- fields[customId] = component.values || [];
770
- break;
771
-
772
- case 19: // File Upload
773
- // File uploads come through as attachments
774
- fields[customId] = component.attachments || [];
775
- break;
776
- }
777
- }
778
- }
779
-
780
- /**
781
- * Destroy all active instances of this component
782
- * Useful for cleanup when the bot shuts down or component is unloaded
783
- *
784
- * @returns Number of instances destroyed
785
- */
786
- destroyAll(): number {
787
- let count = 0;
788
- for (const [refId, context] of this._activeContexts) {
789
- if (context.runDestroy) {
790
- context.runDestroy();
791
- }
792
- this.dbi.data.refs.delete(refId);
793
- count++;
794
- }
795
- this._activeContexts.clear();
796
- return count;
797
- }
798
-
799
- handlers?: any[] = [];
800
- }