diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts new file mode 100644 index 000000000000..235259201930 --- /dev/null +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -0,0 +1,96 @@ +import { APIActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9'; +import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src'; + +describe('Action Row Components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid components THEN do not throw', () => { + expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError(); + expect(() => new ActionRow().setComponents([new ButtonComponent()])).not.toThrowError(); + }); + + test('GIVEN valid JSON input THEN valid JSON output is given', () => { + const actionRowData: APIActionRowComponent = { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + label: 'button', + style: ButtonStyle.Primary, + custom_id: 'test', + }, + { + type: ComponentType.Button, + label: 'link', + style: ButtonStyle.Link, + url: 'https://google.com', + }, + { + type: ComponentType.SelectMenu, + placeholder: 'test', + custom_id: 'test', + options: [ + { + label: 'option', + value: 'option', + }, + ], + }, + ], + }; + + expect(new ActionRow(actionRowData).toJSON()).toEqual(actionRowData); + expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); + expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); + // @ts-expect-error + expect(() => createComponent({ type: 42, components: [] })).toThrowError(); + }); + test('GIVEN valid builder options THEN valid JSON output is given', () => { + const rowWithButtonData: APIActionRowComponent = { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + label: 'test', + custom_id: '123', + style: ButtonStyle.Primary, + }, + ], + }; + + const rowWithSelectMenuData: APIActionRowComponent = { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.SelectMenu, + custom_id: '1234', + options: [ + { + label: 'one', + value: 'one', + }, + { + label: 'two', + value: 'two', + }, + ], + max_values: 10, + min_values: 12, + }, + ], + }; + + const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); + const selectMenu = new SelectMenuComponent() + .setCustomId('1234') + .setMaxValues(10) + .setMinValues(12) + .setOptions([ + new SelectMenuOption().setLabel('one').setValue('one'), + new SelectMenuOption().setLabel('two').setValue('two'), + ]); + + expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData); + expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); + }); + }); +}); diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts new file mode 100644 index 000000000000..524332b3dcd5 --- /dev/null +++ b/packages/builders/__tests__/components/button.test.ts @@ -0,0 +1,146 @@ +import { + APIButtonComponentWithCustomId, + APIButtonComponentWithURL, + ButtonStyle, + ComponentType, +} from 'discord-api-types/v9'; +import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions'; +import { ButtonComponent } from '../../src/components/Button'; + +const buttonComponent = () => new ButtonComponent(); + +const longStr = + 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; + +describe('Button Components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid label THEN validator does not throw', () => { + expect(() => buttonLabelValidator.parse('foobar')).not.toThrowError(); + }); + + test('GIVEN invalid label THEN validator does throw', () => { + expect(() => buttonLabelValidator.parse(null)).toThrowError(); + expect(() => buttonLabelValidator.parse('')).toThrowError(); + + expect(() => buttonLabelValidator.parse(longStr)).toThrowError(); + }); + + test('GIVEN valid style THEN validator does not throw', () => { + expect(() => buttonStyleValidator.parse(3)).not.toThrowError(); + expect(() => buttonStyleValidator.parse(ButtonStyle.Secondary)).not.toThrowError(); + }); + + test('GIVEN invalid style THEN validator does not throw', () => { + expect(() => buttonStyleValidator.parse(7)).toThrowError(); + }); + + test('GIVEN valid fields THEN builder does not throw', () => { + expect(() => + buttonComponent().setCustomId('custom').setStyle(ButtonStyle.Primary).setLabel('test'), + ).not.toThrowError(); + + expect(() => { + const button = buttonComponent() + .setCustomId('custom') + .setStyle(ButtonStyle.Primary) + .setDisabled(true) + .setEmoji({ name: 'test' }); + + button.toJSON(); + }).not.toThrowError(); + + expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError(); + }); + + test('GIVEN invalid fields THEN build does throw', () => { + expect(() => { + buttonComponent().setCustomId(longStr); + }).toThrowError(); + + expect(() => { + const button = buttonComponent() + .setCustomId('custom') + .setStyle(ButtonStyle.Primary) + .setDisabled(true) + .setLabel('test') + .setURL('https://google.com') + .setEmoji({ name: 'test' }); + + button.toJSON(); + }).toThrowError(); + + expect(() => { + // @ts-expect-error + const button = buttonComponent().setEmoji('test'); + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent().setStyle(ButtonStyle.Primary); + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent().setStyle(ButtonStyle.Primary).setCustomId('test'); + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent().setStyle(ButtonStyle.Link); + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent().setStyle(ButtonStyle.Primary).setLabel('test').setURL('https://google.com'); + button.toJSON(); + }).toThrowError(); + + expect(() => { + const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test'); + button.toJSON(); + }).toThrowError(); + + expect(() => buttonComponent().setStyle(24)).toThrowError(); + expect(() => buttonComponent().setLabel(longStr)).toThrowError(); + // @ts-expect-error + expect(() => buttonComponent().setDisabled(0)).toThrowError(); + // @ts-expect-error + expect(() => buttonComponent().setEmoji('foo')).toThrowError(); + + expect(() => buttonComponent().setURL('foobar')).toThrowError(); + }); + + test('GiVEN valid input THEN valid JSON outputs are given', () => { + const interactionData: APIButtonComponentWithCustomId = { + type: ComponentType.Button, + custom_id: 'test', + label: 'test', + style: ButtonStyle.Primary, + disabled: true, + }; + + expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData); + + expect( + buttonComponent() + .setCustomId(interactionData.custom_id) + .setLabel(interactionData.label) + .setStyle(interactionData.style) + .setDisabled(interactionData.disabled) + .toJSON(), + ).toEqual(interactionData); + + const linkData: APIButtonComponentWithURL = { + type: ComponentType.Button, + label: 'test', + style: ButtonStyle.Link, + disabled: true, + url: 'https://google.com', + }; + + expect(new ButtonComponent(linkData).toJSON()).toEqual(linkData); + + expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url)); + }); + }); +}); diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts new file mode 100644 index 000000000000..0e00f6bcad58 --- /dev/null +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -0,0 +1,72 @@ +import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9'; +import { SelectMenuComponent, SelectMenuOption } from '../../src/index'; + +const selectMenu = () => new SelectMenuComponent(); +const selectMenuOption = () => new SelectMenuOption(); + +const longStr = + 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; + +describe('Button Components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid inputs THEN Select Menu does not throw', () => { + expect(() => selectMenu().setCustomId('foo')).not.toThrowError(); + expect(() => selectMenu().setMaxValues(10)).not.toThrowError(); + expect(() => selectMenu().setMinValues(3)).not.toThrowError(); + expect(() => selectMenu().setDisabled(true)).not.toThrowError(); + expect(() => selectMenu().setPlaceholder('description')).not.toThrowError(); + + const option = selectMenuOption() + .setLabel('test') + .setValue('test') + .setDefault(true) + .setEmoji({ name: 'test' }) + .setDescription('description'); + expect(() => selectMenu().addOptions(option)).not.toThrowError(); + expect(() => selectMenu().setOptions([option])).not.toThrowError(); + }); + + test('GIVEN invalid inputs THEN Select Menu does throw', () => { + expect(() => selectMenu().setCustomId(longStr)).toThrowError(); + expect(() => selectMenu().setMaxValues(30)).toThrowError(); + expect(() => selectMenu().setMinValues(-20)).toThrowError(); + // @ts-expect-error + expect(() => selectMenu().setDisabled(0)).toThrowError(); + expect(() => selectMenu().setPlaceholder(longStr)).toThrowError(); + + expect(() => { + selectMenuOption() + .setLabel(longStr) + .setValue(longStr) + // @ts-expect-error + .setDefault(-1) + // @ts-expect-error + .setEmoji({ name: 1 }) + .setDescription(longStr); + }).toThrowError(); + }); + + test('GIVEN valid JSON input THEN valid JSON history is correct', () => { + const selectMenuOptionData: APISelectMenuOption = { + label: 'test', + value: 'test', + emoji: { name: 'test' }, + default: true, + description: 'test', + }; + + const selectMenuData: APISelectMenuComponent = { + type: ComponentType.SelectMenu, + custom_id: 'test', + max_values: 10, + min_values: 3, + disabled: true, + options: [selectMenuOptionData], + placeholder: 'test', + }; + + expect(new SelectMenuComponent(selectMenuData).toJSON()).toEqual(selectMenuData); + expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); + }); + }); +}); diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts new file mode 100644 index 000000000000..d3eec578b061 --- /dev/null +++ b/packages/builders/src/components/ActionRow.ts @@ -0,0 +1,46 @@ +import { APIActionRowComponent, ComponentType } from 'discord-api-types/v9'; +import type { ButtonComponent, SelectMenuComponent } from '..'; +import type { Component } from './Component'; +import { createComponent } from './Components'; + +export type ActionRowComponent = ButtonComponent | SelectMenuComponent; + +// TODO: Add valid form component types + +/** + * Represents an action row component + */ +export class ActionRow implements Component { + public readonly components: T[] = []; + public readonly type = ComponentType.ActionRow; + + public constructor(data?: APIActionRowComponent) { + this.components = (data?.components.map(createComponent) ?? []) as T[]; + } + + /** + * Adds components to this action row. + * @param components The components to add to this action row. + * @returns + */ + public addComponents(...components: T[]) { + this.components.push(...components); + return this; + } + + /** + * Sets the components in this action row + * @param components The components to set this row to + */ + public setComponents(components: T[]) { + Reflect.set(this, 'components', [...components]); + return this; + } + + public toJSON(): APIActionRowComponent { + return { + ...this, + components: this.components.map((component) => component.toJSON()), + }; + } +} diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts new file mode 100644 index 000000000000..9348bc9763c7 --- /dev/null +++ b/packages/builders/src/components/Assertions.ts @@ -0,0 +1,64 @@ +import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9'; +import { z } from 'zod'; +import type { SelectMenuOption } from './selectMenu/SelectMenuOption'; + +export const customIdValidator = z.string().min(1).max(100); + +export const emojiValidator = z + .object({ + id: z.string(), + name: z.string(), + animated: z.boolean(), + }) + .partial() + .strict(); + +export const disabledValidator = z.boolean(); + +export const buttonLabelValidator = z.string().nonempty().max(80); + +export const buttonStyleValidator = z.number().int().min(ButtonStyle.Primary).max(ButtonStyle.Link); + +export const placeholderValidator = z.string().max(100); +export const minMaxValidator = z.number().int().min(0).max(25); + +export const optionsValidator = z.object({}).array().nonempty(); + +export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) { + customIdValidator.parse(customId); + optionsValidator.parse(options); +} + +export const labelValueValidator = z.string().min(1).max(100); +export const defaultValidator = z.boolean(); + +export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) { + labelValueValidator.parse(label); + labelValueValidator.parse(value); +} + +export const urlValidator = z.string().url(); + +export function validateRequiredButtonParameters( + style: ButtonStyle, + label?: string, + emoji?: APIMessageComponentEmoji, + customId?: string, + url?: string, +) { + if (url && customId) { + throw new RangeError('URL and custom id are mutually exclusive'); + } + + if (!label && !emoji) { + throw new RangeError('Buttons must have a label and/or an emoji'); + } + + if (style === ButtonStyle.Link) { + if (!url) { + throw new RangeError('Link buttons must have a url'); + } + } else if (url) { + throw new RangeError('Non-link buttons cannot have a url'); + } +} diff --git a/packages/builders/src/components/Button.ts b/packages/builders/src/components/Button.ts new file mode 100644 index 000000000000..3b7835dcd187 --- /dev/null +++ b/packages/builders/src/components/Button.ts @@ -0,0 +1,105 @@ +import { APIButtonComponent, APIMessageComponentEmoji, ButtonStyle, ComponentType } from 'discord-api-types/v9'; +import { + buttonLabelValidator, + buttonStyleValidator, + customIdValidator, + disabledValidator, + emojiValidator, + urlValidator, + validateRequiredButtonParameters, +} from './Assertions'; +import type { Component } from './Component'; + +export class ButtonComponent implements Component { + public readonly type = ComponentType.Button as const; + public readonly style!: ButtonStyle; + public readonly label?: string; + public readonly emoji?: APIMessageComponentEmoji; + public readonly disabled?: boolean; + public readonly custom_id!: string; + public readonly url!: string; + + public constructor(data?: APIButtonComponent) { + /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ + this.style = data?.style as ButtonStyle; + this.label = data?.label; + this.emoji = data?.emoji; + this.disabled = data?.disabled; + + // This if/else makes typescript happy + if (data?.style === ButtonStyle.Link) { + this.url = data.url; + } else { + this.custom_id = data?.custom_id as string; + } + + /* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */ + } + + /** + * Sets the style of this button + * @param style The style of the button + */ + public setStyle(style: ButtonStyle) { + buttonStyleValidator.parse(style); + Reflect.set(this, 'style', style); + return this; + } + + /** + * Sets the URL for this button + * @param url The URL to open when this button is clicked + */ + public setURL(url: string) { + urlValidator.parse(url); + Reflect.set(this, 'url', url); + return this; + } + + /** + * Sets the custom Id for this button + * @param customId The custom ID to use for this button + */ + public setCustomId(customId: string) { + customIdValidator.parse(customId); + Reflect.set(this, 'custom_id', customId); + return this; + } + + /** + * Sets the emoji to display on this button + * @param emoji The emoji to display on this button + */ + public setEmoji(emoji: APIMessageComponentEmoji) { + emojiValidator.parse(emoji); + Reflect.set(this, 'emoji', emoji); + return this; + } + + /** + * Sets whether this button is disable or not + * @param disabled Whether or not to disable this button or not + */ + public setDisabled(disabled: boolean) { + disabledValidator.parse(disabled); + Reflect.set(this, 'disabled', disabled); + return this; + } + + /** + * Sets the label for this button + * @param label The label to display on this button + */ + public setLabel(label: string) { + buttonLabelValidator.parse(label); + Reflect.set(this, 'label', label); + return this; + } + + public toJSON(): APIButtonComponent { + validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url); + return { + ...this, + }; + } +} diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts new file mode 100644 index 000000000000..eaa60a501d8d --- /dev/null +++ b/packages/builders/src/components/Component.ts @@ -0,0 +1,15 @@ +import type { APIMessageComponent, ComponentType } from 'discord-api-types/v9'; + +/** + * Represents a discord component + */ +export interface Component { + /** + * The type of this component + */ + readonly type: ComponentType; + /** + * Converts this component to an API-compatible JSON object + */ + toJSON: () => APIMessageComponent; +} diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts new file mode 100644 index 000000000000..7060fe245903 --- /dev/null +++ b/packages/builders/src/components/Components.ts @@ -0,0 +1,30 @@ +import { APIMessageComponent, ComponentType } from 'discord-api-types/v9'; +import { ActionRow, ButtonComponent, Component, SelectMenuComponent } from '../index'; +import type { ActionRowComponent } from './ActionRow'; + +export interface MappedComponentTypes { + [ComponentType.ActionRow]: ActionRow; + [ComponentType.Button]: ButtonComponent; + [ComponentType.SelectMenu]: SelectMenuComponent; +} + +/** + * Factory for creating components from API data + * @param data The api data to transform to a component class + */ +export function createComponent( + data: APIMessageComponent & { type: T }, +): MappedComponentTypes[T]; +export function createComponent(data: APIMessageComponent): Component { + switch (data.type) { + case ComponentType.ActionRow: + return new ActionRow(data); + case ComponentType.Button: + return new ButtonComponent(data); + case ComponentType.SelectMenu: + return new SelectMenuComponent(data); + default: + // @ts-expect-error + throw new Error(`Cannot serialize component type: ${data.type as number}`); + } +} diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts new file mode 100644 index 000000000000..b85c420416f8 --- /dev/null +++ b/packages/builders/src/components/selectMenu/SelectMenu.ts @@ -0,0 +1,111 @@ +import { APISelectMenuComponent, ComponentType } from 'discord-api-types/v9'; +import { + customIdValidator, + disabledValidator, + minMaxValidator, + placeholderValidator, + validateRequiredSelectMenuParameters, +} from '../Assertions'; +import type { Component } from '../Component'; +import { SelectMenuOption } from './SelectMenuOption'; + +/** + * Represents a select menu component + */ +export class SelectMenuComponent implements Component { + public readonly type = ComponentType.SelectMenu as const; + public readonly options: SelectMenuOption[]; + public readonly placeholder?: string; + public readonly min_values?: number; + public readonly max_values?: number; + public readonly custom_id!: string; + public readonly disabled?: boolean; + + public constructor(data?: APISelectMenuComponent) { + this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? []; + this.placeholder = data?.placeholder; + this.min_values = data?.min_values; + this.max_values = data?.max_values; + /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ + this.custom_id = data?.custom_id as string; + /* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */ + this.disabled = data?.disabled; + } + + /** + * Sets the placeholder for this select menu + * @param placeholder The placeholder to use for this select menu + */ + public setPlaceholder(placeholder: string) { + placeholderValidator.parse(placeholder); + Reflect.set(this, 'placeholder', placeholder); + return this; + } + + /** + * Sets thes minimum values that must be selected in the select menu + * @param minValues The minimum values that must be selected + */ + public setMinValues(minValues: number) { + minMaxValidator.parse(minValues); + Reflect.set(this, 'min_values', minValues); + return this; + } + + /** + * Sets thes maximum values that must be selected in the select menu + * @param minValues The maximum values that must be selected + */ + public setMaxValues(maxValues: number) { + minMaxValidator.parse(maxValues); + Reflect.set(this, 'max_values', maxValues); + return this; + } + + /** + * Sets the custom Id for this select menu + * @param customId The custom ID to use for this select menu + */ + public setCustomId(customId: string) { + customIdValidator.parse(customId); + Reflect.set(this, 'custom_id', customId); + return this; + } + + /** + * Sets whether or not this select menu is disabled + * @param disabled Whether or not this select menu is disabled + */ + public setDisabled(disabled: boolean) { + disabledValidator.parse(disabled); + Reflect.set(this, 'disabled', disabled); + return this; + } + + /** + * Adds options to this select menu + * @param options The options to add to this select menu + * @returns + */ + public addOptions(...options: SelectMenuOption[]) { + this.options.push(...options); + return this; + } + + /** + * Sets the options on this select menu + * @param options The options to set on this select menu + */ + public setOptions(options: SelectMenuOption[]) { + Reflect.set(this, 'options', [...options]); + return this; + } + + public toJSON(): APISelectMenuComponent { + validateRequiredSelectMenuParameters(this.options, this.custom_id); + return { + ...this, + options: this.options.map((option) => option.toJSON()), + }; + } +} diff --git a/packages/builders/src/components/selectMenu/SelectMenuOption.ts b/packages/builders/src/components/selectMenu/SelectMenuOption.ts new file mode 100644 index 000000000000..31b0ec9caaa4 --- /dev/null +++ b/packages/builders/src/components/selectMenu/SelectMenuOption.ts @@ -0,0 +1,83 @@ +import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v9'; +import { + defaultValidator, + emojiValidator, + labelValueValidator, + validateRequiredSelectMenuOptionParameters, +} from '../Assertions'; + +/** + * Represents an option within a select menu component + */ +export class SelectMenuOption { + public readonly label!: string; + public readonly value!: string; + public readonly description?: string; + public readonly emoji?: APIMessageComponentEmoji; + public readonly default?: boolean; + + public constructor(data?: APISelectMenuOption) { + /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ + this.label = data?.label as string; + this.value = data?.value as string; + /* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */ + this.description = data?.description; + this.emoji = data?.emoji; + this.default = data?.default; + } + + /** + * Sets the label of this option + * @param label The label to show on this option + */ + public setLabel(label: string) { + Reflect.set(this, 'label', label); + return this; + } + + /** + * Sets the value of this option + * @param value The value of this option + */ + public setValue(value: string) { + Reflect.set(this, 'value', value); + return this; + } + + /** + * Sets the description of this option. + * @param description The description of this option + */ + public setDescription(description: string) { + labelValueValidator.parse(description); + Reflect.set(this, 'description', description); + return this; + } + + /** + * Sets whether this option is selected by default + * @param isDefault Whether or not this option is selected by default + */ + public setDefault(isDefault: boolean) { + defaultValidator.parse(isDefault); + Reflect.set(this, 'default', isDefault); + return this; + } + + /** + * Sets the emoji to display on this button + * @param emoji The emoji to display on this button + */ + public setEmoji(emoji: APIMessageComponentEmoji) { + emojiValidator.parse(emoji); + Reflect.set(this, 'emoji', emoji); + return this; + } + + public toJSON(): APISelectMenuOption { + validateRequiredSelectMenuOptionParameters(this.label, this.value); + return { + ...this, + }; + } +} diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index fa51ade96a26..f5db03c01d52 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -2,6 +2,15 @@ export * as EmbedAssertions from './messages/embed/Assertions'; export * from './messages/embed/Embed'; export * from './messages/formatters'; +export * as ComponentAssertions from './components/Assertions'; +export * from './components/ActionRow'; +export * from './components/Button'; +export * from './components/Component'; +export * from './components/Components'; +export * from './components/Button'; +export * from './components/selectMenu/SelectMenu'; +export * from './components/selectMenu/SelectMenuOption'; + export * as SlashCommandAssertions from './interactions/slashCommands/Assertions'; export * from './interactions/slashCommands/SlashCommandBuilder'; export * from './interactions/slashCommands/SlashCommandSubcommands';