From a8781b6c583cab8abb16ee0f409902a4e77de34f Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:58:28 +0100 Subject: [PATCH 01/22] feat(builders): components v2 in builders v1 --- .../__tests__/components/actionRow.test.ts | 12 +++--- .../__tests__/components/components.test.ts | 4 +- packages/builders/package.json | 2 +- packages/builders/src/components/ActionRow.ts | 10 ++--- packages/builders/src/components/Component.ts | 4 +- .../builders/src/components/Components.ts | 2 +- .../builders/src/interactions/modals/Modal.ts | 4 +- pnpm-lock.yaml | 37 +++++++++++-------- 8 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts index b9f63b501529..9e1244fdec83 100644 --- a/packages/builders/__tests__/components/actionRow.test.ts +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -2,7 +2,7 @@ import { ButtonStyle, ComponentType, type APIActionRowComponent, - type APIMessageActionRowComponent, + type APIComponentInMessageActionRow, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { @@ -13,7 +13,7 @@ import { StringSelectMenuOptionBuilder, } from '../../src/index.js'; -const rowWithButtonData: APIActionRowComponent = { +const rowWithButtonData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -25,7 +25,7 @@ const rowWithButtonData: APIActionRowComponent = { ], }; -const rowWithSelectMenuData: APIActionRowComponent = { +const rowWithSelectMenuData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -57,7 +57,7 @@ describe('Action Row Components', () => { }); test('GIVEN valid JSON input THEN valid JSON output is given', () => { - const actionRowData: APIActionRowComponent = { + const actionRowData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -92,7 +92,7 @@ describe('Action Row Components', () => { }); test('GIVEN valid builder options THEN valid JSON output is given', () => { - const rowWithButtonData: APIActionRowComponent = { + const rowWithButtonData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -104,7 +104,7 @@ describe('Action Row Components', () => { ], }; - const rowWithSelectMenuData: APIActionRowComponent = { + const rowWithSelectMenuData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { diff --git a/packages/builders/__tests__/components/components.test.ts b/packages/builders/__tests__/components/components.test.ts index fa0bd4607f65..ea53b9eeb8ee 100644 --- a/packages/builders/__tests__/components/components.test.ts +++ b/packages/builders/__tests__/components/components.test.ts @@ -3,7 +3,7 @@ import { ComponentType, TextInputStyle, type APIButtonComponent, - type APIMessageActionRowComponent, + type APIComponentInMessageActionRow, type APISelectMenuComponent, type APITextInputComponent, type APIActionRowComponent, @@ -27,7 +27,7 @@ describe('createComponentBuilder', () => { ); test('GIVEN an action row component THEN returns a ActionRowBuilder', () => { - const actionRow: APIActionRowComponent = { + const actionRow: APIActionRowComponent = { components: [], type: ComponentType.ActionRow, }; diff --git a/packages/builders/package.json b/packages/builders/package.json index 56d1df0608a7..90268f1a8b8e 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -68,7 +68,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/util": "workspace:^", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.37.119", + "discord-api-types": "0.38.0-next-1740095508888", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index ade84ac4690c..c7b1f5547349 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -3,9 +3,9 @@ import { type APIActionRowComponent, ComponentType, - type APIMessageActionRowComponent, - type APIModalActionRowComponent, - type APIActionRowComponentTypes, + type APIComponentInMessageActionRow, + type APIComponentInModalActionRow, + type APIComponentInActionRow, } from 'discord-api-types/v10'; import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js'; import { ComponentBuilder } from './Component.js'; @@ -57,7 +57,7 @@ export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalAction * @typeParam ComponentType - The types of components this action row holds */ export class ActionRowBuilder extends ComponentBuilder< - APIActionRowComponent + APIActionRowComponent > { /** * The components within this action row. @@ -98,7 +98,7 @@ export class ActionRowBuilder extends * .addComponents(button2, button3); * ``` */ - public constructor({ components, ...data }: Partial> = {}) { + public constructor({ components, ...data }: Partial> = {}) { super({ type: ComponentType.ActionRow, ...data }); this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[]; } diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index e5e59638dfb9..e5306dccc156 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -1,7 +1,7 @@ import type { JSONEncodable } from '@discordjs/util'; import type { APIActionRowComponent, - APIActionRowComponentTypes, + APIComponentInActionRow, APIBaseComponent, ComponentType, } from 'discord-api-types/v10'; @@ -9,7 +9,7 @@ import type { /** * Any action row component data represented as an object. */ -export type AnyAPIActionRowComponent = APIActionRowComponent | APIActionRowComponentTypes; +export type AnyAPIActionRowComponent = APIActionRowComponent | APIComponentInActionRow; /** * The base component builder that contains common symbols for all sorts of components. diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 18b0dff6dd77..351ef587bebb 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -98,7 +98,7 @@ export function createComponentBuilder( case ComponentType.ChannelSelect: return new ChannelSelectMenuBuilder(data); default: - // @ts-expect-error This case can still occur if we get a newer unsupported component type + // TODO: add again when components v2 gets implemented @ts-expect-error This case can still occur if we get a newer unsupported component type throw new Error(`Cannot properly serialize component type: ${data.type}`); } } diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index 948d774df203..513b367657b3 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -3,7 +3,7 @@ import type { JSONEncodable } from '@discordjs/util'; import type { APIActionRowComponent, - APIModalActionRowComponent, + APIComponentInModalActionRow, APIModalInteractionResponseCallbackData, } from 'discord-api-types/v10'; import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js'; @@ -64,7 +64,7 @@ export class ModalBuilder implements JSONEncodable | APIActionRowComponent + ActionRowBuilder | APIActionRowComponent > ) { this.components.push( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f432077f05c..1e7744d68062 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,8 +680,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 discord-api-types: - specifier: ^0.37.119 - version: 0.37.119 + specifier: 0.38.0-next-1740095508888 + version: 0.38.0-next-1740095508888 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -2601,12 +2601,12 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@definitelytyped/header-parser@0.2.16': - resolution: {integrity: sha512-UFsgPft5bhZn07UNGz/9ck4AhdKgLFEOmi2DNr7gXcGL89zbe3u5oVafKUT8j1HOtSBjT8ZEQsXHKlbq+wwF/Q==} + '@definitelytyped/header-parser@0.2.17': + resolution: {integrity: sha512-U0juKFkTOcbkSfO83WSzMEJHYDwoBFiq0tf/JszulL3+7UoSiqunpGmxXS54bm3eGqy7GWjV8AqPQHdeoEaWBQ==} engines: {node: '>=18.18.0'} - '@definitelytyped/typescript-versions@0.1.6': - resolution: {integrity: sha512-gQpXFteIKrOw4ldmBZQfBrD3WobaIG1SwOr/3alXWkcYbkOWa2NRxQbiaYQ2IvYTGaZK26miJw0UOAFiuIs4gA==} + '@definitelytyped/typescript-versions@0.1.7': + resolution: {integrity: sha512-sBzBi1SBn79OkSr8V0H+FzR7QumHk23syPyRxod/VRBrSkgN9rCliIe+nqLoWRAKN8EeKbp00ketnJNLZhucdA==} engines: {node: '>=18.18.0'} '@definitelytyped/utils@0.1.8': @@ -7638,6 +7638,9 @@ packages: discord-api-types@0.37.83: resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} + discord-api-types@0.38.0-next-1740095508888: + resolution: {integrity: sha512-J9ZpbCe8sO/TJIhjUdrfZmR+VPqD/x0WzkiAv4u6H/4Cazo0nE3lYU31wRFbaiXDKcw6+hUjkVVfp5dWLrGdNA==} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -8226,6 +8229,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@10.1.0: @@ -12313,6 +12317,7 @@ packages: stream-connect@1.0.2: resolution: {integrity: sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==} engines: {node: '>=0.10.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. stream-to-array@2.3.0: resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} @@ -14846,13 +14851,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@definitelytyped/header-parser@0.2.16': + '@definitelytyped/header-parser@0.2.17': dependencies: - '@definitelytyped/typescript-versions': 0.1.6 + '@definitelytyped/typescript-versions': 0.1.7 '@definitelytyped/utils': 0.1.8 semver: 7.6.3 - '@definitelytyped/typescript-versions@0.1.6': {} + '@definitelytyped/typescript-versions@0.1.7': {} '@definitelytyped/utils@0.1.8': dependencies: @@ -18927,7 +18932,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/conventional-commits-parser@5.0.0': dependencies: @@ -19132,7 +19137,7 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/serve-static@1.15.7': dependencies: @@ -21509,6 +21514,8 @@ snapshots: discord-api-types@0.37.83: {} + discord-api-types@0.38.0-next-1740095508888: {} + dlv@1.1.3: {} dmd@6.2.3: @@ -21558,7 +21565,7 @@ snapshots: dts-critic@3.3.11(typescript@5.5.4): dependencies: - '@definitelytyped/header-parser': 0.2.16 + '@definitelytyped/header-parser': 0.2.17 command-exists: 1.2.9 rimraf: 3.0.2 semver: 6.3.1 @@ -21568,8 +21575,8 @@ snapshots: dtslint@4.2.1(typescript@5.5.4): dependencies: - '@definitelytyped/header-parser': 0.2.16 - '@definitelytyped/typescript-versions': 0.1.6 + '@definitelytyped/header-parser': 0.2.17 + '@definitelytyped/typescript-versions': 0.1.7 '@definitelytyped/utils': 0.1.8 dts-critic: 3.3.11(typescript@5.5.4) fs-extra: 6.0.1 @@ -26753,7 +26760,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 18.19.45 + '@types/node': 18.19.74 long: 5.2.3 proxy-addr@2.0.7: From d53cf7521088d59048861bad8870c1a9a2e78c74 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:40:42 +0100 Subject: [PATCH 02/22] feat: implemented the first components --- .../__tests__/components/v2/file.test.ts | 45 ++++++++++++ .../__tests__/components/v2/separator.test.ts | 35 +++++++++ .../components/v2/textDisplay.test.ts | 23 ++++++ .../__tests__/components/v2/thumbnail.test.ts | 71 +++++++++++++++++++ .../builders/src/components/Assertions.ts | 8 +++ packages/builders/src/components/Component.ts | 17 ++++- .../builders/src/components/Components.ts | 3 + .../builders/src/components/v2/Assertions.ts | 41 +++++++++++ packages/builders/src/components/v2/File.ts | 38 ++++++++++ .../builders/src/components/v2/Separator.ts | 48 +++++++++++++ .../builders/src/components/v2/TextDisplay.ts | 32 +++++++++ .../builders/src/components/v2/Thumbnail.ts | 53 ++++++++++++++ 12 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 packages/builders/__tests__/components/v2/file.test.ts create mode 100644 packages/builders/__tests__/components/v2/separator.test.ts create mode 100644 packages/builders/__tests__/components/v2/textDisplay.test.ts create mode 100644 packages/builders/__tests__/components/v2/thumbnail.test.ts create mode 100644 packages/builders/src/components/v2/Assertions.ts create mode 100644 packages/builders/src/components/v2/File.ts create mode 100644 packages/builders/src/components/v2/Separator.ts create mode 100644 packages/builders/src/components/v2/TextDisplay.ts create mode 100644 packages/builders/src/components/v2/Thumbnail.ts diff --git a/packages/builders/__tests__/components/v2/file.test.ts b/packages/builders/__tests__/components/v2/file.test.ts new file mode 100644 index 000000000000..37acfc21ae1b --- /dev/null +++ b/packages/builders/__tests__/components/v2/file.test.ts @@ -0,0 +1,45 @@ +import { ComponentType } from 'discord-api-types/v10'; +import { describe, expect, test } from 'vitest'; +import { FileBuilder } from '../../../src/components/v2/File'; + +const dummy = { + type: ComponentType.File as const, + file: { url: 'attachment://owo.png' }, +}; + +describe('File', () => { + describe('File url', () => { + test('GIVEN a file with a pre-defined url THEN return valid toJSON data', () => { + const file = new FileBuilder({ file: { url: 'attachment://owo.png' } }); + expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://owo.png' } }); + }); + + test('GIVEN a file using File#setURL THEN return valid toJSON data', () => { + const file = new FileBuilder(); + file.setURL('attachment://uwu.png'); + + expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://uwu.png' } }); + }); + + test('GIVEN a file with an invalid url THEN throws error', () => { + const file = new FileBuilder(); + file.setURL('https://google.com'); + + expect(() => file.toJSON()).toThrowError(); + }); + }); + + describe('File spoiler', () => { + test('GIVEN a file with a pre-defined spoiler status THEN return valid toJSON data', () => { + const file = new FileBuilder({ ...dummy, spoiler: true }); + expect(file.toJSON()).toEqual({ ...dummy, spoiler: true }); + }); + + test('GIVEN a file using File#setSpoiler THEN return valid toJSON data', () => { + const file = new FileBuilder({ ...dummy }); + file.setSpoiler(false); + + expect(file.toJSON()).toEqual({ ...dummy, spoiler: false }); + }); + }); +}); diff --git a/packages/builders/__tests__/components/v2/separator.test.ts b/packages/builders/__tests__/components/v2/separator.test.ts new file mode 100644 index 000000000000..73743548463f --- /dev/null +++ b/packages/builders/__tests__/components/v2/separator.test.ts @@ -0,0 +1,35 @@ +import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10'; +import { describe, expect, test } from 'vitest'; +import { SeparatorBuilder } from '../../../src/components/v2/Separator'; + +describe('Separator', () => { + describe('Divider', () => { + test('GIVEN a separator with a pre-defined divider THEN return valid toJSON data', () => { + const separator = new SeparatorBuilder({ divider: true }); + expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: true }); + }); + + test('GIVEN a separator with a set divider THEN return valid toJSON data', () => { + const separator = new SeparatorBuilder().setDivider(false); + expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: false }); + }); + }); + + describe('Spacing', () => { + test('GIVEN a separator with a pre-defined spacing THEN return valid toJSON data', () => { + const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small }); + expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Small }); + }); + + test('GIVEN a separator with a set spacing THEN return valid toJSON data', () => { + const separator = new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large); + expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Large }); + }); + + test('GIVEN a separator with a set spacing THEN clear spacing THEN return valid toJSON data', () => { + const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small }); + separator.clearSpacing(); + expect(separator.toJSON()).toEqual({ type: ComponentType.Separator }); + }); + }); +}); diff --git a/packages/builders/__tests__/components/v2/textDisplay.test.ts b/packages/builders/__tests__/components/v2/textDisplay.test.ts new file mode 100644 index 000000000000..01495c8af9a7 --- /dev/null +++ b/packages/builders/__tests__/components/v2/textDisplay.test.ts @@ -0,0 +1,23 @@ +import { ComponentType } from 'discord-api-types/v10'; +import { describe, expect, test } from 'vitest'; +import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay'; + +describe('TextDisplay', () => { + describe('TextDisplay content', () => { + test('GIVEN a text display with a pre-defined content THEN return valid toJSON data', () => { + const textDisplay = new TextDisplayBuilder({ content: 'foo' }); + expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' }); + }); + + test('GIVEN a text display with a set content THEN return valid toJSON data', () => { + const textDisplay = new TextDisplayBuilder().setContent('foo'); + expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' }); + }); + + test('GIVEN a text display with a pre-defined content THEN overwritten content THEN return valid toJSON data', () => { + const textDisplay = new TextDisplayBuilder({ content: 'foo' }); + textDisplay.setContent('bar'); + expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'bar' }); + }); + }); +}); diff --git a/packages/builders/__tests__/components/v2/thumbnail.test.ts b/packages/builders/__tests__/components/v2/thumbnail.test.ts new file mode 100644 index 000000000000..be6f49d44112 --- /dev/null +++ b/packages/builders/__tests__/components/v2/thumbnail.test.ts @@ -0,0 +1,71 @@ +import { ComponentType } from 'discord-api-types/v10'; +import { describe, expect, test } from 'vitest'; +import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail'; + +const dummy = { + type: ComponentType.Thumbnail as const, + media: { url: 'https://google.com' }, +}; + +describe('Thumbnail', () => { + describe('Thumbnail url', () => { + test('GIVEN a thumbnail with a pre-defined url THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder({ media: { url: 'https://google.com' } }); + expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } }); + }); + + test('GIVEN a thumbnail with a set url THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder().setURL('https://google.com'); + expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } }); + }); + + test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL (%s) THEN throws error', (input) => { + const thumbnail = new ThumbnailBuilder(); + + thumbnail.setURL(input); + expect(() => thumbnail.toJSON()).toThrowError(); + }); + }); + + describe('Thumbnail description', () => { + test('GIVEN a thumbnail with a pre-defined description THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder({ ...dummy, description: 'foo' }); + expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' }); + }); + + test('GIVEN a thumbnail with a set description THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder({ ...dummy }); + thumbnail.setDescription('foo'); + + expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' }); + }); + + test('GIVEN a thumbnail with a pre-defined description THEN unset description THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder({ description: 'foo', ...dummy }); + thumbnail.setDescription(); + + expect(thumbnail.toJSON()).toEqual({ ...dummy }); + }); + + test('GIVEN a thumbnail with an invalid description THEN throws error', () => { + const thumbnail = new ThumbnailBuilder(); + + thumbnail.setDescription('a'.repeat(1_025)); + expect(() => thumbnail.toJSON()).toThrowError(); + }); + }); + + describe('Thumbnail spoiler', () => { + test('GIVEN a thumbnail with a pre-defined spoiler status THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder({ ...dummy, spoiler: true }); + expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: true }); + }); + + test('GIVEN a thumbnail with a set spoiler status THEN return valid toJSON data', () => { + const thumbnail = new ThumbnailBuilder({ ...dummy }); + thumbnail.setSpoiler(false); + + expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: false }); + }); + }); +}); diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 926159eedc08..26165c4fffb1 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -3,6 +3,14 @@ import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord import { isValidationEnabled } from '../util/validation.js'; import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js'; +export const idValidator = s + .number() + .int() + .greaterThanOrEqual(1) + .lessThan(1 << 32) + .optional() + .setValidationEnabled(isValidationEnabled); + export const customIdValidator = s .string() .lengthGreaterThanOrEqual(1) diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index e5306dccc156..c88c2eddc560 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -4,12 +4,17 @@ import type { APIComponentInActionRow, APIBaseComponent, ComponentType, + APIMessageComponent, } from 'discord-api-types/v10'; +import { idValidator } from './Assertions'; /** * Any action row component data represented as an object. */ -export type AnyAPIActionRowComponent = APIActionRowComponent | APIComponentInActionRow; +export type AnyAPIActionRowComponent = + | APIActionRowComponent + | APIComponentInActionRow + | APIMessageComponent; /** * The base component builder that contains common symbols for all sorts of components. @@ -42,4 +47,14 @@ export abstract class ComponentBuilder< public constructor(data: Partial) { this.data = data; } + + /** + * Sets the id (not the custom id) for this component. + * + * @param id - The id for this component + */ + public setId(id?: number | undefined) { + this.data.id = idValidator.parse(id); + return this; + } } diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 351ef587bebb..70abaa28a4b0 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -13,6 +13,7 @@ import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js'; import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js'; import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js'; import { TextInputBuilder } from './textInput/TextInput.js'; +import { FileBuilder } from './v2/File.js'; /** * Components here are mapped to their respective builder. @@ -97,6 +98,8 @@ export function createComponentBuilder( return new MentionableSelectMenuBuilder(data); case ComponentType.ChannelSelect: return new ChannelSelectMenuBuilder(data); + case ComponentType.File: + return new FileBuilder(data); default: // TODO: add again when components v2 gets implemented @ts-expect-error This case can still occur if we get a newer unsupported component type throw new Error(`Cannot properly serialize component type: ${data.type}`); diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts new file mode 100644 index 000000000000..4c5cd07f5e38 --- /dev/null +++ b/packages/builders/src/components/v2/Assertions.ts @@ -0,0 +1,41 @@ +import { s } from '@sapphire/shapeshift'; +import { SeparatorSpacingSize } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation'; + +export const unfurledMediaItemPredicate = s + .object({ + url: s + .string() + .url( + { allowedProtocols: ['http:', 'https:', 'attachment:'] }, + { message: 'Invalid protocol for media URL. Must be http:, https:, or attachment:' }, + ), + }) + .setValidationEnabled(isValidationEnabled); + +export const thumbnailDescriptionPredicate = s + .string() + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(1_024) + .optional() + .setValidationEnabled(isValidationEnabled); + +export const filePredicate = s + .object({ + url: s + .string() + .url({ allowedProtocols: ['attachment:'] }, { message: 'Invalid protocol for file URL. Must be attachment:' }), + }) + .setValidationEnabled(isValidationEnabled); + +export const spoilerPredicate = s.boolean(); + +export const dividerPredicate = s.boolean(); + +export const spacingPredicate = s.nativeEnum(SeparatorSpacingSize); + +export const textDisplayContentPredicate = s + .string() + .lengthGreaterThanOrEqual(1) + .lengthLessThanOrEqual(4_000) + .setValidationEnabled(isValidationEnabled); diff --git a/packages/builders/src/components/v2/File.ts b/packages/builders/src/components/v2/File.ts new file mode 100644 index 000000000000..6e18145ae39c --- /dev/null +++ b/packages/builders/src/components/v2/File.ts @@ -0,0 +1,38 @@ +import { ComponentType, type APIFileComponent } from 'discord-api-types/v10'; +import { ComponentBuilder } from '../Component'; +import { filePredicate, spoilerPredicate } from './Assertions'; + +export class FileBuilder extends ComponentBuilder { + public constructor(data: Partial = {}) { + super({ type: ComponentType.File, ...data, file: data.file ? { url: data.file.url } : undefined }); + } + + /** + * Sets the spoiler status of this file. + * + * @param spoiler - The spoiler status to use + */ + public setSpoiler(spoiler: boolean) { + this.data.spoiler = spoilerPredicate.parse(spoiler); + return this; + } + + /** + * Sets the media URL of this file. + * + * @param url - The URL to use + */ + public setURL(url: string) { + this.data.file = filePredicate.parse({ url }); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(): APIFileComponent { + filePredicate.parse(this.data.file); + + return { ...this.data, file: { ...this.data.file } } as APIFileComponent; + } +} diff --git a/packages/builders/src/components/v2/Separator.ts b/packages/builders/src/components/v2/Separator.ts new file mode 100644 index 000000000000..039c113a3098 --- /dev/null +++ b/packages/builders/src/components/v2/Separator.ts @@ -0,0 +1,48 @@ +import type { SeparatorSpacingSize, APISeparatorComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { ComponentBuilder } from '../Component'; +import { dividerPredicate, spacingPredicate } from './Assertions'; + +export class SeparatorBuilder extends ComponentBuilder { + public constructor(data: Partial = {}) { + super({ + type: ComponentType.Separator, + ...data, + }); + } + + /** + * Sets whether this separator should show a divider line. + * + * @param divider - Whether to show a divider line + */ + public setDivider(divider: boolean) { + this.data.divider = dividerPredicate.parse(divider); + return this; + } + + /** + * Sets the spacing of this separator. + * + * @param spacing - The spacing to use + */ + public setSpacing(spacing: SeparatorSpacingSize) { + this.data.spacing = spacingPredicate.parse(spacing); + return this; + } + + /** + * Clears the spacing of this separator. + */ + public clearSpacing() { + this.data.spacing = undefined; + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(): APISeparatorComponent { + return { ...this.data } as APISeparatorComponent; + } +} diff --git a/packages/builders/src/components/v2/TextDisplay.ts b/packages/builders/src/components/v2/TextDisplay.ts new file mode 100644 index 000000000000..f2e0419d83bd --- /dev/null +++ b/packages/builders/src/components/v2/TextDisplay.ts @@ -0,0 +1,32 @@ +import type { APITextDisplayComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { ComponentBuilder } from '../Component'; +import { textDisplayContentPredicate } from './Assertions'; + +export class TextDisplayBuilder extends ComponentBuilder { + public constructor(data: Partial = {}) { + super({ + type: ComponentType.TextDisplay, + ...data, + }); + } + + /** + * Sets the text of this text display. + * + * @param content - The text to use + */ + public setContent(content: string) { + this.data.content = textDisplayContentPredicate.parse(content); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(): APITextDisplayComponent { + textDisplayContentPredicate.parse(this.data.content); + + return { ...this.data } as APITextDisplayComponent; + } +} diff --git a/packages/builders/src/components/v2/Thumbnail.ts b/packages/builders/src/components/v2/Thumbnail.ts new file mode 100644 index 000000000000..bd6e562fa29f --- /dev/null +++ b/packages/builders/src/components/v2/Thumbnail.ts @@ -0,0 +1,53 @@ +import type { APIThumbnailComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { ComponentBuilder } from '../Component'; +import { thumbnailDescriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions'; + +export class ThumbnailBuilder extends ComponentBuilder { + public constructor(data: Partial = {}) { + super({ + type: ComponentType.Thumbnail, + ...data, + media: data.media ? { url: data.media.url } : undefined, + }); + } + + /** + * Sets the description of this thumbnail. + * + * @param description - The description to use + */ + public setDescription(description?: string | undefined) { + this.data.description = thumbnailDescriptionPredicate.parse(description); + return this; + } + + /** + * Sets the spoiler status of this thumbnail. + * + * @param spoiler - The spoiler status to use + */ + public setSpoiler(spoiler: boolean) { + this.data.spoiler = spoilerPredicate.parse(spoiler); + return this; + } + + /** + * Sets the media URL of this thumbnail. + * + * @param url - The URL to use + */ + public setURL(url: string) { + this.data.media = unfurledMediaItemPredicate.parse({ url }); + return this; + } + + /** + * {@inheritdoc ComponentBuilder.toJSON} + */ + public override toJSON(): APIThumbnailComponent { + unfurledMediaItemPredicate.parse(this.data.media); + + return { ...this.data } as APIThumbnailComponent; + } +} From 49eeb628f26d20971043b254bfb374a8a83266f1 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:51:18 +0100 Subject: [PATCH 03/22] fix: tests --- packages/builders/__tests__/components/v2/file.test.ts | 3 +-- .../builders/__tests__/components/v2/thumbnail.test.ts | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/builders/__tests__/components/v2/file.test.ts b/packages/builders/__tests__/components/v2/file.test.ts index 37acfc21ae1b..96bd7c250e6e 100644 --- a/packages/builders/__tests__/components/v2/file.test.ts +++ b/packages/builders/__tests__/components/v2/file.test.ts @@ -23,9 +23,8 @@ describe('File', () => { test('GIVEN a file with an invalid url THEN throws error', () => { const file = new FileBuilder(); - file.setURL('https://google.com'); - expect(() => file.toJSON()).toThrowError(); + expect(() => file.setURL('https://google.com')).toThrowError(); }); }); diff --git a/packages/builders/__tests__/components/v2/thumbnail.test.ts b/packages/builders/__tests__/components/v2/thumbnail.test.ts index be6f49d44112..fc7cdf6f7693 100644 --- a/packages/builders/__tests__/components/v2/thumbnail.test.ts +++ b/packages/builders/__tests__/components/v2/thumbnail.test.ts @@ -19,11 +19,10 @@ describe('Thumbnail', () => { expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } }); }); - test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL (%s) THEN throws error', (input) => { + test.each(['owo', 'discord://user'])('GIVEN a thumbnail with an invalid URL (%s) THEN throws error', (input) => { const thumbnail = new ThumbnailBuilder(); - thumbnail.setURL(input); - expect(() => thumbnail.toJSON()).toThrowError(); + expect(() => thumbnail.setURL(input)).toThrowError(); }); }); @@ -50,8 +49,7 @@ describe('Thumbnail', () => { test('GIVEN a thumbnail with an invalid description THEN throws error', () => { const thumbnail = new ThumbnailBuilder(); - thumbnail.setDescription('a'.repeat(1_025)); - expect(() => thumbnail.toJSON()).toThrowError(); + expect(() => thumbnail.setDescription('a'.repeat(1_025))).toThrowError(); }); }); From fed2a0c2cf3b9427656ae40c7322f9e0fedecee3 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Mar 2025 20:16:10 +0100 Subject: [PATCH 04/22] fix: tests --- packages/discord.js/package.json | 2 +- packages/discord.js/typings/index.d.ts | 42 ++++++++++++--------- packages/discord.js/typings/index.test-d.ts | 6 +-- pnpm-lock.yaml | 4 +- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 9ec812c7e3b5..5f642cf03c2a 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -72,7 +72,7 @@ "@discordjs/util": "workspace:^", "@discordjs/ws": "1.1.1", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.37.119", + "discord-api-types": "0.38.0-next-1740095508888", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 4e26514046a0..766d81e1c71d 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -111,13 +111,13 @@ import { AuditLogEvent, APIMessageComponentEmoji, EmbedType, - APIActionRowComponentTypes, + APIComponentInActionRow, APIModalInteractionResponseCallbackData, APIModalSubmitInteraction, - APIMessageActionRowComponent, + APIComponentInMessageActionRow, TextInputStyle, APITextInputComponent, - APIModalActionRowComponent, + APIComponentInModalActionRow, APIModalComponent, APISelectMenuOption, APIEmbedField, @@ -285,7 +285,7 @@ export interface BaseComponentData { } export type MessageActionRowComponentData = - | JSONEncodable + | JSONEncodable | ButtonComponentData | StringSelectMenuComponentData | UserSelectMenuComponentData @@ -293,13 +293,13 @@ export type MessageActionRowComponentData = | MentionableSelectMenuComponentData | ChannelSelectMenuComponentData; -export type ModalActionRowComponentData = JSONEncodable | TextInputComponentData; +export type ModalActionRowComponentData = JSONEncodable | TextInputComponentData; export type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData; export type ActionRowComponent = MessageActionRowComponent | ModalActionRowComponent; -export interface ActionRowData | ActionRowComponentData> +export interface ActionRowData | ActionRowComponentData> extends BaseComponentData { components: readonly ComponentType[]; } @@ -309,8 +309,8 @@ export class ActionRowBuilder< > extends BuilderActionRow { public constructor( data?: Partial< - | ActionRowData> - | APIActionRowComponent + | ActionRowData> + | APIActionRowComponent >, ); public static from( @@ -330,9 +330,9 @@ export type MessageActionRowComponent = export type ModalActionRowComponent = TextInputComponent; export class ActionRow extends Component< - APIActionRowComponent + APIActionRowComponent > { - private constructor(data: APIActionRowComponent); + private constructor(data: APIActionRowComponent); public readonly components: ComponentType[]; public toJSON(): APIActionRowComponent>; } @@ -740,7 +740,7 @@ export class ButtonInteraction extends Mes export type AnyComponent = | APIMessageComponent | APIModalComponent - | APIActionRowComponent; + | APIActionRowComponent; export class Component { public readonly data: Readonly; @@ -2086,7 +2086,13 @@ export interface MessageCall { participants: readonly Snowflake[]; } -export type MessageComponentType = Exclude; +export type MessageComponentType = + | ComponentType.Button + | ComponentType.ChannelSelect + | ComponentType.MentionableSelect + | ComponentType.RoleSelect + | ComponentType.StringSelect + | ComponentType.UserSelect; export interface MessageCollectorOptionsParams< ComponentType extends MessageComponentType, @@ -2278,9 +2284,9 @@ export class MessageComponentInteraction e public get component(): CacheTypeReducer< Cached, MessageActionRowComponent, - Exclude>, - MessageActionRowComponent | Exclude>, - MessageActionRowComponent | Exclude> + Exclude>, + MessageActionRowComponent | Exclude>, + MessageActionRowComponent | Exclude> >; public componentType: Exclude; public customId: string; @@ -2457,7 +2463,7 @@ export interface ModalComponentData { customId: string; title: string; components: readonly ( - | JSONEncodable> + | JSONEncodable> | ActionRowData )[]; } @@ -6454,9 +6460,9 @@ export interface BaseMessageOptions { | AttachmentPayload )[]; components?: readonly ( - | JSONEncodable> + | JSONEncodable> | ActionRowData - | APIActionRowComponent + | APIActionRowComponent )[]; poll?: PollData; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 02011850550a..a0271423f950 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -25,7 +25,7 @@ import { ApplicationCommandType, APIMessage, APIActionRowComponent, - APIActionRowComponentTypes, + APIComponentInActionRow, APIStringSelectComponent, APIUserSelectComponent, APIRoleSelectComponent, @@ -2347,7 +2347,7 @@ EmbedBuilder.from(embedData); declare const embedComp: Embed; EmbedBuilder.from(embedComp); -declare const actionRowData: APIActionRowComponent; +declare const actionRowData: APIActionRowComponent; ActionRowBuilder.from(actionRowData); declare const actionRowComp: ActionRow; @@ -2359,7 +2359,7 @@ declare const buttonsActionRowComp: ActionRow; expectType>(ActionRowBuilder.from(buttonsActionRowData)); expectType>(ActionRowBuilder.from(buttonsActionRowComp)); -declare const anyComponentsActionRowData: APIActionRowComponent; +declare const anyComponentsActionRowData: APIActionRowComponent; declare const anyComponentsActionRowComp: ActionRow; expectType(ActionRowBuilder.from(anyComponentsActionRowData)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e7744d68062..00657558e014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -941,8 +941,8 @@ importers: specifier: 3.5.3 version: 3.5.3 discord-api-types: - specifier: ^0.37.119 - version: 0.37.119 + specifier: 0.38.0-next-1740095508888 + version: 0.38.0-next-1740095508888 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 From 4b893bb96688949f8768775887e5f69daf19cff0 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Mar 2025 20:31:17 +0100 Subject: [PATCH 05/22] fix: export the new stuff --- packages/builders/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index 53908612197a..2193ecd47360 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -34,6 +34,12 @@ export { export * from './components/selectMenu/StringSelectMenuOption.js'; export * from './components/selectMenu/UserSelectMenu.js'; +export * as ComponentsV2Assertions from './components/v2/Assertions.js'; +export * from './components/v2/File.js'; +export * from './components/v2/Separator.js'; +export * from './components/v2/TextDisplay.js'; +export * from './components/v2/Thumbnail.js'; + export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js'; export * from './interactions/slashCommands/SlashCommandBuilder.js'; export * from './interactions/slashCommands/SlashCommandSubcommands.js'; From 9d82fd8df5ff40cbb23f7d52f02702692f7d460b Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 2 Mar 2025 23:43:01 +0100 Subject: [PATCH 06/22] feat: add rest of components --- .../__tests__/components/v2/container.test.ts | 160 ++++++++++++++++++ .../components/v2/mediagallery.test.ts | 100 +++++++++++ .../__tests__/components/v2/section.test.ts | 152 +++++++++++++++++ packages/builders/src/components/ActionRow.ts | 7 - .../builders/src/components/Assertions.ts | 4 +- .../builders/src/components/Components.ts | 62 ++++++- .../builders/src/components/v2/Assertions.ts | 14 +- .../builders/src/components/v2/Container.ts | 127 ++++++++++++++ .../src/components/v2/MediaGallery.ts | 86 ++++++++++ .../src/components/v2/MediaGalleryItem.ts | 61 +++++++ .../builders/src/components/v2/Section.ts | 102 +++++++++++ .../builders/src/components/v2/Thumbnail.ts | 4 +- packages/builders/src/index.ts | 4 + 13 files changed, 868 insertions(+), 15 deletions(-) create mode 100644 packages/builders/__tests__/components/v2/container.test.ts create mode 100644 packages/builders/__tests__/components/v2/mediagallery.test.ts create mode 100644 packages/builders/__tests__/components/v2/section.test.ts create mode 100644 packages/builders/src/components/v2/Container.ts create mode 100644 packages/builders/src/components/v2/MediaGallery.ts create mode 100644 packages/builders/src/components/v2/MediaGalleryItem.ts create mode 100644 packages/builders/src/components/v2/Section.ts diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts new file mode 100644 index 000000000000..f7cd120d08f2 --- /dev/null +++ b/packages/builders/__tests__/components/v2/container.test.ts @@ -0,0 +1,160 @@ +import { type APIContainerComponent, ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10'; +import { describe, test, expect } from 'vitest'; +import { createComponentBuilder } from '../../../src/components/Components.js'; +import { ContainerBuilder } from '../../../src/components/v2/Container.js'; +import { SeparatorBuilder } from '../../../src/components/v2/Separator.js'; +import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js'; + +const containerWithTextDisplay: APIContainerComponent = { + type: ComponentType.Container, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + id: 123, + }, + ], +}; + +const containerWithSeparatorData: APIContainerComponent = { + type: ComponentType.Container, + components: [ + { + type: ComponentType.Separator, + id: 1_234, + spacing: SeparatorSpacingSize.Small, + divider: false, + }, + ], + accent_color: 0x00ff00, +}; + +const containerWithSeparatorDataNoColor: APIContainerComponent = { + type: ComponentType.Container, + components: [ + { + type: ComponentType.Separator, + id: 1_234, + spacing: SeparatorSpacingSize.Small, + divider: false, + }, + ], +}; + +describe('Container Components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid components THEN do not throw', () => { + expect(() => new ContainerBuilder().addComponents(new SeparatorBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().setComponents(new SeparatorBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().addComponents([new SeparatorBuilder()])).not.toThrowError(); + expect(() => new ContainerBuilder().setComponents([new SeparatorBuilder()])).not.toThrowError(); + }); + + test('GIVEN valid JSON input THEN valid JSON output is given', () => { + const containerData: APIContainerComponent = { + type: ComponentType.Container, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + id: 3, + }, + { + type: ComponentType.Separator, + spacing: SeparatorSpacingSize.Large, + divider: true, + id: 4, + }, + { + type: ComponentType.File, + file: { + url: 'attachment://file.png', + }, + spoiler: false, + }, + ], + accent_color: 0xff00ff, + spoiler: true, + }; + + expect(new ContainerBuilder(containerData).toJSON()).toEqual(containerData); + expect(new ContainerBuilder().toJSON()).toEqual({ type: ComponentType.Container, components: [] }); + expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError(); + }); + + test('GIVEN valid builder options THEN valid JSON output is given', () => { + const containerWithTextDisplay: APIContainerComponent = { + type: ComponentType.Container, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + id: 123, + }, + ], + }; + + const containerWithSeparatorData: APIContainerComponent = { + type: ComponentType.Container, + components: [ + { + type: ComponentType.Separator, + id: 1_234, + spacing: SeparatorSpacingSize.Small, + divider: false, + }, + ], + accent_color: 0x00ff00, + }; + + expect(new ContainerBuilder(containerWithTextDisplay).toJSON()).toEqual(containerWithTextDisplay); + expect(new ContainerBuilder(containerWithSeparatorData).toJSON()).toEqual(containerWithSeparatorData); + expect(new ContainerBuilder().toJSON()).toEqual({ type: ComponentType.Container, components: [] }); + expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError(); + }); + + test('GIVEN valid builder options THEN valid JSON output is given 2', () => { + const textDisplay = new TextDisplayBuilder().setContent('test').setId(123); + const separator = new SeparatorBuilder().setId(1_234).setSpacing(SeparatorSpacingSize.Small).setDivider(false); + + expect(new ContainerBuilder().addComponents(textDisplay).toJSON()).toEqual(containerWithTextDisplay); + expect(new ContainerBuilder().addComponents(separator).toJSON()).toEqual(containerWithSeparatorDataNoColor); + expect(new ContainerBuilder().addComponents([textDisplay]).toJSON()).toEqual(containerWithTextDisplay); + expect(new ContainerBuilder().addComponents([separator]).toJSON()).toEqual(containerWithSeparatorDataNoColor); + }); + + test('GIVEN valid accent color THEN valid JSON output is given', () => { + expect(new ContainerBuilder().setAccentColor([255, 0, 255]).toJSON()).toEqual({ + type: ComponentType.Container, + components: [], + accent_color: 0xff00ff, + }); + expect(new ContainerBuilder().setAccentColor(0xff00ff).toJSON()).toEqual({ + type: ComponentType.Container, + components: [], + accent_color: 0xff00ff, + }); + expect(new ContainerBuilder().setAccentColor([255, 0, 255]).setAccentColor(null).toJSON()).toEqual({ + type: ComponentType.Container, + components: [], + }); + expect(new ContainerBuilder(containerWithSeparatorData).setAccentColor(null).toJSON()).toEqual( + containerWithSeparatorDataNoColor, + ); + }); + + test('GIVEN valid method parameters THEN valid JSON is given', () => { + expect(new ContainerBuilder().setSpoiler(true).toJSON()).toEqual({ + type: ComponentType.Container, + components: [], + spoiler: true, + }); + expect(new ContainerBuilder().setSpoiler(false).setId(5).toJSON()).toEqual({ + type: ComponentType.Container, + components: [], + spoiler: false, + id: 5, + }); + }); + }); +}); diff --git a/packages/builders/__tests__/components/v2/mediagallery.test.ts b/packages/builders/__tests__/components/v2/mediagallery.test.ts new file mode 100644 index 000000000000..e86f1ecef4b0 --- /dev/null +++ b/packages/builders/__tests__/components/v2/mediagallery.test.ts @@ -0,0 +1,100 @@ +import { type APIMediaGalleryComponent, ComponentType } from 'discord-api-types/v10'; +import { describe, test, expect } from 'vitest'; +import { createComponentBuilder } from '../../../src/components/Components.js'; +import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js'; +import { MediaGalleryItemBuilder } from '../../../src/components/v2/MediaGalleryItem.js'; + +const galleryHttpsDisplay: APIMediaGalleryComponent = { + type: ComponentType.MediaGallery, + items: [ + { + description: 'test', + spoiler: false, + media: { url: 'https://discord.com/logo.png' }, + }, + ], +}; + +const galleryAttachmentData: APIMediaGalleryComponent = { + type: ComponentType.MediaGallery, + items: [ + { + media: { url: 'attachment://file.png' }, + }, + ], + id: 123, +}; + +describe('Media Gallery Components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid items THEN do not throw', () => { + expect(() => new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder())).not.toThrowError(); + expect(() => new MediaGalleryBuilder().setItems(new MediaGalleryItemBuilder())).not.toThrowError(); + expect(() => new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder()])).not.toThrowError(); + expect(() => new MediaGalleryBuilder().setItems([new MediaGalleryItemBuilder()])).not.toThrowError(); + }); + + test('GIVEN valid JSON input THEN valid JSON output is given', () => { + const mediaGalleryData: APIMediaGalleryComponent = { + type: ComponentType.MediaGallery, + items: [ + { + media: { url: 'attachment://file.png' }, + description: 'test', + spoiler: false, + }, + { + media: { url: 'https://discord.js.org/logo.jpg' }, + spoiler: true, + }, + ], + id: 1_234, + }; + + expect(new MediaGalleryBuilder(mediaGalleryData).toJSON()).toEqual(mediaGalleryData); + expect(new MediaGalleryBuilder().toJSON()).toEqual({ type: ComponentType.MediaGallery, items: [] }); + expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError(); + }); + + test('GIVEN valid builder options THEN valid JSON output is given', () => { + const galleryHttpsDisplay: APIMediaGalleryComponent = { + type: ComponentType.MediaGallery, + items: [ + { + description: 'test', + spoiler: false, + media: { url: 'https://discord.com/logo.png' }, + }, + ], + }; + + const galleryAttachmentData: APIMediaGalleryComponent = { + type: ComponentType.MediaGallery, + items: [ + { + media: { url: 'attachment://file.png' }, + }, + ], + id: 123, + }; + + expect(new MediaGalleryBuilder(galleryHttpsDisplay).toJSON()).toEqual(galleryHttpsDisplay); + expect(new MediaGalleryBuilder(galleryAttachmentData).toJSON()).toEqual(galleryAttachmentData); + expect(new MediaGalleryBuilder().toJSON()).toEqual({ type: ComponentType.MediaGallery, items: [] }); + expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError(); + }); + + test('GIVEN valid builder options THEN valid JSON output is given 2', () => { + const item1 = new MediaGalleryItemBuilder() + .setDescription('test') + .setSpoiler(false) + .setURL('https://discord.com/logo.png'); + const item2 = new MediaGalleryItemBuilder().setURL('attachment://file.png'); + + expect(new MediaGalleryBuilder().addItems(item1).toJSON()).toEqual(galleryHttpsDisplay); + expect(new MediaGalleryBuilder().addItems(item2).setId(123).toJSON()).toEqual(galleryAttachmentData); + expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay); + expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData); + }); + }); +}); diff --git a/packages/builders/__tests__/components/v2/section.test.ts b/packages/builders/__tests__/components/v2/section.test.ts new file mode 100644 index 000000000000..bf8552616b4d --- /dev/null +++ b/packages/builders/__tests__/components/v2/section.test.ts @@ -0,0 +1,152 @@ +import { type APISectionComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10'; +import { describe, test, expect } from 'vitest'; +import { createComponentBuilder } from '../../../src/components/Components.js'; +import { ButtonBuilder } from '../../../src/components/button/Button.js'; +import { SectionBuilder } from '../../../src/components/v2/Section.js'; +import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js'; +import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail.js'; + +const sectionWithButtonData: APISectionComponent = { + type: ComponentType.Section, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + accessory: { + type: ComponentType.Button, + label: 'test', + custom_id: '123', + style: ButtonStyle.Primary, + }, +}; + +const sectionWithThumbnailData: APISectionComponent = { + type: ComponentType.Section, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + accessory: { + type: ComponentType.Thumbnail, + media: { url: 'attachment://file.png' }, + spoiler: true, + description: 'test', + }, +}; + +describe('Section Components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid components THEN do not throw', () => { + expect(() => new SectionBuilder().addComponents(new TextDisplayBuilder())).not.toThrowError(); + expect(() => new SectionBuilder().setComponents(new TextDisplayBuilder())).not.toThrowError(); + expect(() => new SectionBuilder().addComponents([new TextDisplayBuilder()])).not.toThrowError(); + expect(() => new SectionBuilder().setComponents([new TextDisplayBuilder()])).not.toThrowError(); + }); + + test('GIVEN valid JSON input THEN valid JSON output is given', () => { + const sectionData: APISectionComponent = { + type: ComponentType.Section, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + id: 123, + }, + { + type: ComponentType.TextDisplay, + content: 'test', + }, + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + accessory: { + type: ComponentType.Thumbnail, + media: { url: 'attachment://file.png' }, + }, + }; + + expect(new SectionBuilder(sectionData).toJSON()).toEqual(sectionData); + expect(() => + createComponentBuilder({ + type: ComponentType.Section, + components: [], + accessory: { type: ComponentType.Thumbnail, media: { url: 'https://discord.com/logo.png' } }, + }), + ).not.toThrowError(); + }); + + test('GIVEN valid builder options THEN valid JSON output is given', () => { + const sectionWithButtonData: APISectionComponent = { + type: ComponentType.Section, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + accessory: { + type: ComponentType.Button, + label: 'test', + custom_id: '123', + style: ButtonStyle.Primary, + }, + }; + + const sectionWithThumbnailData: APISectionComponent = { + type: ComponentType.Section, + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + accessory: { + type: ComponentType.Thumbnail, + media: { url: 'attachment://file.png' }, + spoiler: true, + description: 'test', + }, + }; + + expect(new SectionBuilder(sectionWithButtonData).toJSON()).toEqual(sectionWithButtonData); + expect(new SectionBuilder(sectionWithThumbnailData).toJSON()).toEqual(sectionWithThumbnailData); + expect(() => + createComponentBuilder({ + type: ComponentType.Section, + components: [], + accessory: { + type: ComponentType.Button, + label: 'test', + custom_id: '123', + style: ButtonStyle.Primary, + }, + }), + ).not.toThrowError(); + }); + + test('GIVEN valid builder options THEN valid JSON output is given 2', () => { + const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); + const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler(true).setURL('attachment://file.png'); + const textDisplay = new TextDisplayBuilder().setContent('test'); + + expect(new SectionBuilder().addComponents(textDisplay).setAccessory(button).toJSON()).toEqual( + sectionWithButtonData, + ); + expect(new SectionBuilder().addComponents(textDisplay).setAccessory(thumbnail).toJSON()).toEqual( + sectionWithThumbnailData, + ); + expect(new SectionBuilder().addComponents([textDisplay]).setAccessory(button).toJSON()).toEqual( + sectionWithButtonData, + ); + expect(new SectionBuilder().addComponents([textDisplay]).setAccessory(thumbnail).toJSON()).toEqual( + sectionWithThumbnailData, + ); + }); + }); +}); diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index c7b1f5547349..6953d5f8d572 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -18,13 +18,6 @@ import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js'; import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js'; import type { TextInputBuilder } from './textInput/TextInput.js'; -/** - * The builders that may be used for messages. - */ -export type MessageComponentBuilder = - | ActionRowBuilder - | MessageActionRowComponentBuilder; - /** * The builders that may be used for modals. */ diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 26165c4fffb1..64a44e2f282e 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -5,9 +5,9 @@ import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOpti export const idValidator = s .number() - .int() + .safeInt() .greaterThanOrEqual(1) - .lessThan(1 << 32) + .lessThan(4_294_967_296) .optional() .setValidationEnabled(isValidationEnabled); diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 70abaa28a4b0..139d3367322a 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -1,8 +1,8 @@ import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10'; import { ActionRowBuilder, + type MessageActionRowComponentBuilder, type AnyComponentBuilder, - type MessageComponentBuilder, type ModalComponentBuilder, } from './ActionRow.js'; import { ComponentBuilder } from './Component.js'; @@ -13,7 +13,25 @@ import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js'; import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js'; import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js'; import { TextInputBuilder } from './textInput/TextInput.js'; +import { ContainerBuilder } from './v2/Container.js'; import { FileBuilder } from './v2/File.js'; +import { MediaGalleryBuilder } from './v2/MediaGallery.js'; +import { SectionBuilder } from './v2/Section.js'; +import { SeparatorBuilder } from './v2/Separator.js'; +import { TextDisplayBuilder } from './v2/TextDisplay.js'; +import { ThumbnailBuilder } from './v2/Thumbnail.js'; + +/** + * The builders that may be used for messages. + */ +export type MessageComponentBuilder = + | ActionRowBuilder + | ContainerBuilder + | FileBuilder + | MessageActionRowComponentBuilder + | SeparatorBuilder + | TextDisplayBuilder + | ThumbnailBuilder; /** * Components here are mapped to their respective builder. @@ -51,6 +69,34 @@ export interface MappedComponentTypes { * The channel select component type is associated with a {@link ChannelSelectMenuBuilder}. */ [ComponentType.ChannelSelect]: ChannelSelectMenuBuilder; + /** + * The file component type is associated with a {@link FileBuilder}. + */ + [ComponentType.File]: FileBuilder; + /** + * The separator component type is associated with a {@link SeparatorBuilder}. + */ + [ComponentType.Separator]: SeparatorBuilder; + /** + * The container component type is associated with a {@link ContainerBuilder}. + */ + [ComponentType.Container]: ContainerBuilder; + /** + * The text display component type is associated with a {@link TextDisplayBuilder}. + */ + [ComponentType.TextDisplay]: TextDisplayBuilder; + /** + * The thumbnail component type is associated with a {@link ThumbnailBuilder}. + */ + [ComponentType.Thumbnail]: ThumbnailBuilder; + /** + * The section component type is associated with a {@link SectionBuilder}. + */ + [ComponentType.Section]: SectionBuilder; + /** + * The media gallery component type is associated with a {@link MediaGalleryBuilder}. + */ + [ComponentType.MediaGallery]: MediaGalleryBuilder; } /** @@ -100,8 +146,20 @@ export function createComponentBuilder( return new ChannelSelectMenuBuilder(data); case ComponentType.File: return new FileBuilder(data); + case ComponentType.Container: + return new ContainerBuilder(data); + case ComponentType.Section: + return new SectionBuilder(data); + case ComponentType.Separator: + return new SeparatorBuilder(data); + case ComponentType.TextDisplay: + return new TextDisplayBuilder(data); + case ComponentType.Thumbnail: + return new ThumbnailBuilder(data); + case ComponentType.MediaGallery: + return new MediaGalleryBuilder(data); default: - // TODO: add again when components v2 gets implemented @ts-expect-error This case can still occur if we get a newer unsupported component type + // @ts-expect-error This case can still occur if we get a newer unsupported component type throw new Error(`Cannot properly serialize component type: ${data.type}`); } } diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts index 4c5cd07f5e38..23c794330ce7 100644 --- a/packages/builders/src/components/v2/Assertions.ts +++ b/packages/builders/src/components/v2/Assertions.ts @@ -1,6 +1,9 @@ import { s } from '@sapphire/shapeshift'; import { SeparatorSpacingSize } from 'discord-api-types/v10'; +import { colorPredicate } from '../../messages/embed/Assertions'; import { isValidationEnabled } from '../../util/validation'; +import { ButtonBuilder } from '../button/Button'; +import { ThumbnailBuilder } from './Thumbnail'; export const unfurledMediaItemPredicate = s .object({ @@ -13,11 +16,11 @@ export const unfurledMediaItemPredicate = s }) .setValidationEnabled(isValidationEnabled); -export const thumbnailDescriptionPredicate = s +export const descriptionPredicate = s .string() .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(1_024) - .optional() + .nullish() .setValidationEnabled(isValidationEnabled); export const filePredicate = s @@ -34,8 +37,15 @@ export const dividerPredicate = s.boolean(); export const spacingPredicate = s.nativeEnum(SeparatorSpacingSize); +export const containerColorPredicate = colorPredicate.nullish(); + export const textDisplayContentPredicate = s .string() .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(4_000) .setValidationEnabled(isValidationEnabled); + +export const accessoryPredicate = s + .instance(ButtonBuilder) + .or(s.instance(ThumbnailBuilder)) + .setValidationEnabled(isValidationEnabled); diff --git a/packages/builders/src/components/v2/Container.ts b/packages/builders/src/components/v2/Container.ts new file mode 100644 index 000000000000..e9f8a359116d --- /dev/null +++ b/packages/builders/src/components/v2/Container.ts @@ -0,0 +1,127 @@ +/* eslint-disable jsdoc/check-param-names */ + +import type { APIContainerComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import type { RGBTuple } from '../../index.js'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; +import type { ActionRowBuilder, AnyComponentBuilder } from '../ActionRow.js'; +import { ComponentBuilder } from '../Component.js'; +import { createComponentBuilder } from '../Components.js'; +import { containerColorPredicate, spoilerPredicate } from './Assertions.js'; +import type { FileBuilder } from './File.js'; +import type { SeparatorBuilder } from './Separator.js'; +import type { TextDisplayBuilder } from './TextDisplay.js'; + +/** + * The builders that may be used within a container. + */ +export type ContainerComponentBuilder = + | ActionRowBuilder + | FileBuilder + | SeparatorBuilder + | TextDisplayBuilder; + +/** + * A builder that creates API-compatible JSON data for a container. + */ +export class ContainerBuilder extends ComponentBuilder { + /** + * The components within this container. + */ + public readonly components: ContainerComponentBuilder[]; + + /** + * Creates a new container from API data. + * + * @param data - The API data to create this container with + * @example + * Creating a container from an API data object: + * ```ts + * const container = new ContainerBuilder({ + * components: [ + * { + * content: "Some text here", + * type: ComponentType.TextDisplay, + * }, + * ], + * }); + * ``` + * @example + * Creating a container using setters and API data: + * ```ts + * const container = new ContainerBuilder({ + * components: [ + * { + * content: "# Heading", + * type: ComponentType.TextDisplay, + * }, + * ], + * }) + * .addComponents(separator, section); + * ``` + */ + public constructor({ components, ...data }: Partial = {}) { + super({ type: ComponentType.Container, ...data }); + this.components = (components?.map((component) => createComponentBuilder(component)) ?? + []) as ContainerComponentBuilder[]; + } + + /** + * Sets the accent color of this container. + * + * @param color - The color to use + */ + public setAccentColor(color?: RGBTuple | number | null | undefined): this { + // Data assertions + containerColorPredicate.parse(color); + + if (Array.isArray(color)) { + const [red, green, blue] = color; + this.data.accent_color = (red << 16) + (green << 8) + blue; + return this; + } + + this.data.accent_color = color ?? undefined; + return this; + } + + /** + * Adds components to this container. + * + * @param components - The components to add + */ + public addComponents(...components: RestOrArray) { + this.components.push(...normalizeArray(components)); + return this; + } + + /** + * Sets components for this container. + * + * @param components - The components to set + */ + public setComponents(...components: RestOrArray) { + this.components.splice(0, this.components.length, ...normalizeArray(components)); + return this; + } + + /** + * Sets the spoiler status of this container. + * + * @param spoiler - The spoiler status to use + */ + public setSpoiler(spoiler: boolean) { + this.data.spoiler = spoilerPredicate.parse(spoiler); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public toJSON(): APIContainerComponent { + return { + ...this.data, + components: this.components.map((component) => component.toJSON()), + } as APIContainerComponent; + } +} diff --git a/packages/builders/src/components/v2/MediaGallery.ts b/packages/builders/src/components/v2/MediaGallery.ts new file mode 100644 index 000000000000..6f0ed47f679d --- /dev/null +++ b/packages/builders/src/components/v2/MediaGallery.ts @@ -0,0 +1,86 @@ +/* eslint-disable jsdoc/check-param-names */ + +import type { APIMediaGalleryComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; +import { ComponentBuilder } from '../Component.js'; +import { MediaGalleryItemBuilder } from './MediaGalleryItem.js'; + +/** + * A builder that creates API-compatible JSON data for a container. + */ +export class MediaGalleryBuilder extends ComponentBuilder { + /** + * The components within this container. + */ + public readonly items: MediaGalleryItemBuilder[]; + + /** + * Creates a new media gallery from API data. + * + * @param data - The API data to create this container with + * @example + * Creating a media gallery from an API data object: + * ```ts + * const mediaGallery = new MediaGalleryBuilder({ + * items: [ + * { + * description: "Some text here", + * media: { + * url: 'https://cdn.discordapp.com/embed/avatars/2.png', + * }, + * }, + * ], + * }); + * ``` + * @example + * Creating a media gallery using setters and API data: + * ```ts + * const mediaGallery = new MediaGalleryBuilder({ + * items: [ + * { + * description: "alt text", + * media: { + * url: 'https://cdn.discordapp.com/embed/avatars/5.png', + * }, + * }, + * ], + * }) + * .addItems(item2, item3); + * ``` + */ + public constructor({ items, ...data }: Partial = {}) { + super({ type: ComponentType.MediaGallery, ...data }); + this.items = items?.map((item) => new MediaGalleryItemBuilder(item)) ?? []; + } + + /** + * Adds items to this media gallery. + * + * @param items - The items to add + */ + public addItems(...items: RestOrArray) { + this.items.push(...normalizeArray(items)); + return this; + } + + /** + * Sets items for this media gallery. + * + * @param items - The items to set + */ + public setItems(...items: RestOrArray) { + this.items.splice(0, this.items.length, ...normalizeArray(items)); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public toJSON(): APIMediaGalleryComponent { + return { + ...this.data, + items: this.items.map((item) => item.toJSON()), + } as APIMediaGalleryComponent; + } +} diff --git a/packages/builders/src/components/v2/MediaGalleryItem.ts b/packages/builders/src/components/v2/MediaGalleryItem.ts new file mode 100644 index 000000000000..b41e8eb3eeb5 --- /dev/null +++ b/packages/builders/src/components/v2/MediaGalleryItem.ts @@ -0,0 +1,61 @@ +import type { APIMediaGalleryItem } from 'discord-api-types/v10'; +import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions'; + +export class MediaGalleryItemBuilder { + /** + * The API data associated with this component. + */ + public readonly data: Partial; + + /** + * Constructs a new kind of component. + * + * @param data - The data to construct a component out of + */ + public constructor(data: Partial = {}) { + this.data = data; + } + + /** + * Sets the description of this media gallery item. + * + * @param description - The description to use + */ + public setDescription(description?: string | undefined) { + this.data.description = descriptionPredicate.parse(description); + return this; + } + + /** + * Sets the spoiler status of this media gallery item. + * + * @param spoiler - The spoiler status to use + */ + public setSpoiler(spoiler: boolean) { + this.data.spoiler = spoilerPredicate.parse(spoiler); + return this; + } + + /** + * Sets the media URL of this media gallery item. + * + * @param url - The URL to use + */ + public setURL(url: string) { + this.data.media = unfurledMediaItemPredicate.parse({ url }); + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * @remarks + * This method runs validations on the data before serializing it. + * As such, it may throw an error if the data is invalid. + */ + public toJSON(): APIMediaGalleryItem { + unfurledMediaItemPredicate.parse(this.data.media); + + return { ...this.data } as APIMediaGalleryItem; + } +} diff --git a/packages/builders/src/components/v2/Section.ts b/packages/builders/src/components/v2/Section.ts new file mode 100644 index 000000000000..c5ba35d24bcb --- /dev/null +++ b/packages/builders/src/components/v2/Section.ts @@ -0,0 +1,102 @@ +/* eslint-disable jsdoc/check-param-names */ + +import type { APISectionComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import type { ButtonBuilder, ThumbnailBuilder } from '../../index.js'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; +import { ComponentBuilder } from '../Component.js'; +import { createComponentBuilder } from '../Components.js'; +import { accessoryPredicate } from './Assertions.js'; +import type { TextDisplayBuilder } from './TextDisplay.js'; + +/** + * A builder that creates API-compatible JSON data for a section. + */ +export class SectionBuilder extends ComponentBuilder { + /** + * The components within this section. + */ + public readonly components: ComponentBuilder[]; + + /** + * The accessory of this section. + */ + private accessory: ButtonBuilder | ThumbnailBuilder | null; + + /** + * Creates a new container from API data. + * + * @param data - The API data to create this container with + * @example + * Creating a container from an API data object: + * ```ts + * const container = new ContainerBuilder({ + * components: [ + * { + * content: "Some text here", + * type: ComponentType.TextDisplay, + * }, + * ], + * }); + * ``` + * @example + * Creating a container using setters and API data: + * ```ts + * const container = new ContainerBuilder({ + * components: [ + * { + * content: "# Heading", + * type: ComponentType.TextDisplay, + * }, + * ], + * }) + * .addComponents(separator, section); + * ``` + */ + public constructor({ components, accessory, ...data }: Partial = {}) { + super({ type: ComponentType.Section, ...data }); + this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentBuilder[]; + this.accessory = accessory ? (createComponentBuilder(accessory) ?? null) : null; + } + + /** + * Sets the accessory of this section. + * + * @param accessory - The accessory to use + */ + public setAccessory(accessory: ButtonBuilder | ThumbnailBuilder): this { + this.accessory = accessory; + return this; + } + + /** + * Adds components to this container. + * + * @param components - The components to add + */ + public addComponents(...components: RestOrArray) { + this.components.push(...normalizeArray(components)); + return this; + } + + /** + * Sets components for this container. + * + * @param components - The components to set + */ + public setComponents(...components: RestOrArray) { + this.components.splice(0, this.components.length, ...normalizeArray(components)); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public toJSON(): APISectionComponent { + return { + ...this.data, + components: this.components.map((component) => component.toJSON()), + accessory: accessoryPredicate.parse(this.accessory).toJSON(), + } as APISectionComponent; + } +} diff --git a/packages/builders/src/components/v2/Thumbnail.ts b/packages/builders/src/components/v2/Thumbnail.ts index bd6e562fa29f..fe179e987ffb 100644 --- a/packages/builders/src/components/v2/Thumbnail.ts +++ b/packages/builders/src/components/v2/Thumbnail.ts @@ -1,7 +1,7 @@ import type { APIThumbnailComponent } from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; import { ComponentBuilder } from '../Component'; -import { thumbnailDescriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions'; +import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions'; export class ThumbnailBuilder extends ComponentBuilder { public constructor(data: Partial = {}) { @@ -18,7 +18,7 @@ export class ThumbnailBuilder extends ComponentBuilder { * @param description - The description to use */ public setDescription(description?: string | undefined) { - this.data.description = thumbnailDescriptionPredicate.parse(description); + this.data.description = descriptionPredicate.parse(description); return this; } diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index 2193ecd47360..62030af0dc42 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -35,7 +35,11 @@ export * from './components/selectMenu/StringSelectMenuOption.js'; export * from './components/selectMenu/UserSelectMenu.js'; export * as ComponentsV2Assertions from './components/v2/Assertions.js'; +export * from './components/v2/Container.js'; export * from './components/v2/File.js'; +export * from './components/v2/MediaGallery.js'; +export * from './components/v2/MediaGalleryItem.js'; +export * from './components/v2/Section.js'; export * from './components/v2/Separator.js'; export * from './components/v2/TextDisplay.js'; export * from './components/v2/Thumbnail.js'; From d4d271e97415993b337aa5c3206d2b459eb19f54 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 07:16:16 +0100 Subject: [PATCH 07/22] feat: add callback syntax --- .../components/v2/mediagallery.test.ts | 31 +++++++++++++++++++ .../builders/src/components/v2/Assertions.ts | 8 +++++ .../src/components/v2/MediaGallery.ts | 29 ++++++++++++++--- .../builders/src/components/v2/Section.ts | 2 +- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/builders/__tests__/components/v2/mediagallery.test.ts b/packages/builders/__tests__/components/v2/mediagallery.test.ts index e86f1ecef4b0..3e7bf03a6e5d 100644 --- a/packages/builders/__tests__/components/v2/mediagallery.test.ts +++ b/packages/builders/__tests__/components/v2/mediagallery.test.ts @@ -96,5 +96,36 @@ describe('Media Gallery Components', () => { expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay); expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData); }); + + test('GIVEN valid builder callback THEN valid JSON output is given', () => { + const item1 = new MediaGalleryItemBuilder() + .setDescription('test') + .setSpoiler(false) + .setURL('https://discord.com/logo.png'); + const item2 = new MediaGalleryItemBuilder().setURL('attachment://file.png'); + + expect( + new MediaGalleryBuilder() + .addItems((item) => item.setDescription('test').setSpoiler(false).setURL('https://discord.com/logo.png')) + .toJSON(), + ).toEqual(galleryHttpsDisplay); + expect( + new MediaGalleryBuilder() + .setItems((item) => item.setURL('attachment://file.png')) + .setId(123) + .toJSON(), + ).toEqual(galleryAttachmentData); + expect( + new MediaGalleryBuilder() + .addItems([(item) => item.setDescription('test').setSpoiler(false).setURL('https://discord.com/logo.png')]) + .toJSON(), + ).toEqual(galleryHttpsDisplay); + expect( + new MediaGalleryBuilder() + .setItems([(item) => item.setURL('attachment://file.png')]) + .setId(123) + .toJSON(), + ).toEqual(galleryAttachmentData); + }); }); }); diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts index 23c794330ce7..42330cceb34d 100644 --- a/packages/builders/src/components/v2/Assertions.ts +++ b/packages/builders/src/components/v2/Assertions.ts @@ -3,6 +3,7 @@ import { SeparatorSpacingSize } from 'discord-api-types/v10'; import { colorPredicate } from '../../messages/embed/Assertions'; import { isValidationEnabled } from '../../util/validation'; import { ButtonBuilder } from '../button/Button'; +import type { MediaGalleryItemBuilder } from './MediaGalleryItem'; import { ThumbnailBuilder } from './Thumbnail'; export const unfurledMediaItemPredicate = s @@ -49,3 +50,10 @@ export const accessoryPredicate = s .instance(ButtonBuilder) .or(s.instance(ThumbnailBuilder)) .setValidationEnabled(isValidationEnabled); + +export function assertReturnOfBuilder( + input: unknown, + ExpectedInstanceOf: new () => ReturnType, +): asserts input is ReturnType { + s.instance(ExpectedInstanceOf).parse(input); +} diff --git a/packages/builders/src/components/v2/MediaGallery.ts b/packages/builders/src/components/v2/MediaGallery.ts index 6f0ed47f679d..5ecbf4210859 100644 --- a/packages/builders/src/components/v2/MediaGallery.ts +++ b/packages/builders/src/components/v2/MediaGallery.ts @@ -4,6 +4,7 @@ import type { APIMediaGalleryComponent } from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { ComponentBuilder } from '../Component.js'; +import { assertReturnOfBuilder } from './Assertions.js'; import { MediaGalleryItemBuilder } from './MediaGalleryItem.js'; /** @@ -59,8 +60,17 @@ export class MediaGalleryBuilder extends ComponentBuilder) { - this.items.push(...normalizeArray(items)); + public addItems( + ...items: RestOrArray MediaGalleryItemBuilder)> + ) { + this.items.push( + ...normalizeArray(items).map((input) => { + const result = typeof input === 'function' ? input(new MediaGalleryItemBuilder()) : input; + + assertReturnOfBuilder(result, MediaGalleryItemBuilder); + return result; + }), + ); return this; } @@ -69,8 +79,19 @@ export class MediaGalleryBuilder extends ComponentBuilder) { - this.items.splice(0, this.items.length, ...normalizeArray(items)); + public setItems( + ...items: RestOrArray MediaGalleryItemBuilder)> + ) { + this.items.splice( + 0, + this.items.length, + ...normalizeArray(items).map((input) => { + const result = typeof input === 'function' ? input(new MediaGalleryItemBuilder()) : input; + + assertReturnOfBuilder(result, MediaGalleryItemBuilder); + return result; + }), + ); return this; } diff --git a/packages/builders/src/components/v2/Section.ts b/packages/builders/src/components/v2/Section.ts index c5ba35d24bcb..10369eda9a6a 100644 --- a/packages/builders/src/components/v2/Section.ts +++ b/packages/builders/src/components/v2/Section.ts @@ -56,7 +56,7 @@ export class SectionBuilder extends ComponentBuilder { public constructor({ components, accessory, ...data }: Partial = {}) { super({ type: ComponentType.Section, ...data }); this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentBuilder[]; - this.accessory = accessory ? (createComponentBuilder(accessory) ?? null) : null; + this.accessory = accessory ? createComponentBuilder(accessory) : null; } /** From 662a8ebd870e7668c6804fe1de1cb88dd16dbeb0 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 07:22:15 +0100 Subject: [PATCH 08/22] feat: callback syntax for section --- .../__tests__/components/v2/section.test.ts | 29 +++++++++++++++++ .../builders/src/components/v2/Assertions.ts | 3 +- .../builders/src/components/v2/Section.ts | 32 +++++++++++++++---- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/builders/__tests__/components/v2/section.test.ts b/packages/builders/__tests__/components/v2/section.test.ts index bf8552616b4d..26e305341698 100644 --- a/packages/builders/__tests__/components/v2/section.test.ts +++ b/packages/builders/__tests__/components/v2/section.test.ts @@ -148,5 +148,34 @@ describe('Section Components', () => { sectionWithThumbnailData, ); }); + + test('GIVEN valid builder callback THEN valid JSON output is given', () => { + const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); + + expect( + new SectionBuilder() + .addComponents((textDisplay) => textDisplay.setContent('test')) + .setAccessory(button) + .toJSON(), + ).toEqual(sectionWithButtonData); + expect( + new SectionBuilder() + .setComponents((textDisplay) => textDisplay.setContent('test')) + .setAccessory(button) + .toJSON(), + ).toEqual(sectionWithButtonData); + expect( + new SectionBuilder() + .addComponents([(textDisplay) => textDisplay.setContent('test')]) + .setAccessory(button) + .toJSON(), + ).toEqual(sectionWithButtonData); + expect( + new SectionBuilder() + .setComponents([(textDisplay) => textDisplay.setContent('test')]) + .setAccessory(button) + .toJSON(), + ).toEqual(sectionWithButtonData); + }); }); }); diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts index 42330cceb34d..ac8728c6f980 100644 --- a/packages/builders/src/components/v2/Assertions.ts +++ b/packages/builders/src/components/v2/Assertions.ts @@ -4,6 +4,7 @@ import { colorPredicate } from '../../messages/embed/Assertions'; import { isValidationEnabled } from '../../util/validation'; import { ButtonBuilder } from '../button/Button'; import type { MediaGalleryItemBuilder } from './MediaGalleryItem'; +import type { TextDisplayBuilder } from './TextDisplay'; import { ThumbnailBuilder } from './Thumbnail'; export const unfurledMediaItemPredicate = s @@ -51,7 +52,7 @@ export const accessoryPredicate = s .or(s.instance(ThumbnailBuilder)) .setValidationEnabled(isValidationEnabled); -export function assertReturnOfBuilder( +export function assertReturnOfBuilder( input: unknown, ExpectedInstanceOf: new () => ReturnType, ): asserts input is ReturnType { diff --git a/packages/builders/src/components/v2/Section.ts b/packages/builders/src/components/v2/Section.ts index 10369eda9a6a..dd5ad2c968ea 100644 --- a/packages/builders/src/components/v2/Section.ts +++ b/packages/builders/src/components/v2/Section.ts @@ -6,8 +6,8 @@ import type { ButtonBuilder, ThumbnailBuilder } from '../../index.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { ComponentBuilder } from '../Component.js'; import { createComponentBuilder } from '../Components.js'; -import { accessoryPredicate } from './Assertions.js'; -import type { TextDisplayBuilder } from './TextDisplay.js'; +import { accessoryPredicate, assertReturnOfBuilder } from './Assertions.js'; +import { TextDisplayBuilder } from './TextDisplay.js'; /** * A builder that creates API-compatible JSON data for a section. @@ -74,8 +74,17 @@ export class SectionBuilder extends ComponentBuilder { * * @param components - The components to add */ - public addComponents(...components: RestOrArray) { - this.components.push(...normalizeArray(components)); + public addComponents( + ...components: RestOrArray TextDisplayBuilder)> + ) { + this.components.push( + ...normalizeArray(components).map((input) => { + const result = typeof input === 'function' ? input(new TextDisplayBuilder()) : input; + + assertReturnOfBuilder(result, TextDisplayBuilder); + return result; + }), + ); return this; } @@ -84,8 +93,19 @@ export class SectionBuilder extends ComponentBuilder { * * @param components - The components to set */ - public setComponents(...components: RestOrArray) { - this.components.splice(0, this.components.length, ...normalizeArray(components)); + public setComponents( + ...components: RestOrArray TextDisplayBuilder)> + ) { + this.components.splice( + 0, + this.components.length, + ...normalizeArray(components).map((input) => { + const result = typeof input === 'function' ? input(new TextDisplayBuilder()) : input; + + assertReturnOfBuilder(result, TextDisplayBuilder); + return result; + }), + ); return this; } From 0c97dd61cf09c747d56b28581e49ec6a081b122b Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 09:52:47 +0100 Subject: [PATCH 09/22] fix: missing implements --- packages/builders/src/components/v2/MediaGalleryItem.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builders/src/components/v2/MediaGalleryItem.ts b/packages/builders/src/components/v2/MediaGalleryItem.ts index b41e8eb3eeb5..f44a50e494dd 100644 --- a/packages/builders/src/components/v2/MediaGalleryItem.ts +++ b/packages/builders/src/components/v2/MediaGalleryItem.ts @@ -1,7 +1,8 @@ +import type { JSONEncodable } from '@discordjs/util'; import type { APIMediaGalleryItem } from 'discord-api-types/v10'; import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions'; -export class MediaGalleryItemBuilder { +export class MediaGalleryItemBuilder implements JSONEncodable { /** * The API data associated with this component. */ From ced763a3d48d576781e7f192c37129f52b9f43d7 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:33:54 +0100 Subject: [PATCH 10/22] fix: accessory property --- packages/builders/src/components/v2/Section.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/builders/src/components/v2/Section.ts b/packages/builders/src/components/v2/Section.ts index dd5ad2c968ea..66809c336445 100644 --- a/packages/builders/src/components/v2/Section.ts +++ b/packages/builders/src/components/v2/Section.ts @@ -21,7 +21,7 @@ export class SectionBuilder extends ComponentBuilder { /** * The accessory of this section. */ - private accessory: ButtonBuilder | ThumbnailBuilder | null; + public readonly accessory: ButtonBuilder | ThumbnailBuilder; /** * Creates a new container from API data. @@ -56,7 +56,7 @@ export class SectionBuilder extends ComponentBuilder { public constructor({ components, accessory, ...data }: Partial = {}) { super({ type: ComponentType.Section, ...data }); this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentBuilder[]; - this.accessory = accessory ? createComponentBuilder(accessory) : null; + this.accessory = accessory ? createComponentBuilder(accessory) : undefined!; } /** @@ -65,7 +65,7 @@ export class SectionBuilder extends ComponentBuilder { * @param accessory - The accessory to use */ public setAccessory(accessory: ButtonBuilder | ThumbnailBuilder): this { - this.accessory = accessory; + Reflect.set(this, 'accessory', accessoryPredicate.parse(accessory)); return this; } From a789651275ce28fb87a63636617015009fd4beb5 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:21:43 +0100 Subject: [PATCH 11/22] fix: apply suggestions from v2 PR --- packages/builders/__tests__/components/v2/container.test.ts | 2 +- packages/builders/__tests__/components/v2/section.test.ts | 2 +- packages/builders/src/components/v2/Container.ts | 2 +- packages/builders/src/components/v2/File.ts | 2 +- packages/builders/src/components/v2/MediaGalleryItem.ts | 2 +- packages/builders/src/components/v2/Section.ts | 4 ++-- packages/builders/src/components/v2/Separator.ts | 2 +- packages/builders/src/components/v2/Thumbnail.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts index f7cd120d08f2..45b9b578e2ad 100644 --- a/packages/builders/__tests__/components/v2/container.test.ts +++ b/packages/builders/__tests__/components/v2/container.test.ts @@ -144,7 +144,7 @@ describe('Container Components', () => { }); test('GIVEN valid method parameters THEN valid JSON is given', () => { - expect(new ContainerBuilder().setSpoiler(true).toJSON()).toEqual({ + expect(new ContainerBuilder().setSpoiler().toJSON()).toEqual({ type: ComponentType.Container, components: [], spoiler: true, diff --git a/packages/builders/__tests__/components/v2/section.test.ts b/packages/builders/__tests__/components/v2/section.test.ts index 26e305341698..5b74435030bc 100644 --- a/packages/builders/__tests__/components/v2/section.test.ts +++ b/packages/builders/__tests__/components/v2/section.test.ts @@ -132,7 +132,7 @@ describe('Section Components', () => { test('GIVEN valid builder options THEN valid JSON output is given 2', () => { const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); - const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler(true).setURL('attachment://file.png'); + const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler().setURL('attachment://file.png'); const textDisplay = new TextDisplayBuilder().setContent('test'); expect(new SectionBuilder().addComponents(textDisplay).setAccessory(button).toJSON()).toEqual( diff --git a/packages/builders/src/components/v2/Container.ts b/packages/builders/src/components/v2/Container.ts index e9f8a359116d..41b4f5c9025d 100644 --- a/packages/builders/src/components/v2/Container.ts +++ b/packages/builders/src/components/v2/Container.ts @@ -110,7 +110,7 @@ export class ContainerBuilder extends ComponentBuilder { * * @param spoiler - The spoiler status to use */ - public setSpoiler(spoiler: boolean) { + public setSpoiler(spoiler = true) { this.data.spoiler = spoilerPredicate.parse(spoiler); return this; } diff --git a/packages/builders/src/components/v2/File.ts b/packages/builders/src/components/v2/File.ts index 6e18145ae39c..f77d6dc74f21 100644 --- a/packages/builders/src/components/v2/File.ts +++ b/packages/builders/src/components/v2/File.ts @@ -12,7 +12,7 @@ export class FileBuilder extends ComponentBuilder { * * @param spoiler - The spoiler status to use */ - public setSpoiler(spoiler: boolean) { + public setSpoiler(spoiler = true) { this.data.spoiler = spoilerPredicate.parse(spoiler); return this; } diff --git a/packages/builders/src/components/v2/MediaGalleryItem.ts b/packages/builders/src/components/v2/MediaGalleryItem.ts index f44a50e494dd..4439a1f096f9 100644 --- a/packages/builders/src/components/v2/MediaGalleryItem.ts +++ b/packages/builders/src/components/v2/MediaGalleryItem.ts @@ -32,7 +32,7 @@ export class MediaGalleryItemBuilder implements JSONEncodable { /** * The accessory of this section. */ - public readonly accessory: ButtonBuilder | ThumbnailBuilder; + public readonly accessory?: ButtonBuilder | ThumbnailBuilder; /** * Creates a new container from API data. @@ -56,7 +56,7 @@ export class SectionBuilder extends ComponentBuilder { public constructor({ components, accessory, ...data }: Partial = {}) { super({ type: ComponentType.Section, ...data }); this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentBuilder[]; - this.accessory = accessory ? createComponentBuilder(accessory) : undefined!; + this.accessory = accessory ? createComponentBuilder(accessory) : undefined; } /** diff --git a/packages/builders/src/components/v2/Separator.ts b/packages/builders/src/components/v2/Separator.ts index 039c113a3098..1e506eaa34cc 100644 --- a/packages/builders/src/components/v2/Separator.ts +++ b/packages/builders/src/components/v2/Separator.ts @@ -16,7 +16,7 @@ export class SeparatorBuilder extends ComponentBuilder { * * @param divider - Whether to show a divider line */ - public setDivider(divider: boolean) { + public setDivider(divider = true) { this.data.divider = dividerPredicate.parse(divider); return this; } diff --git a/packages/builders/src/components/v2/Thumbnail.ts b/packages/builders/src/components/v2/Thumbnail.ts index fe179e987ffb..1259451c4142 100644 --- a/packages/builders/src/components/v2/Thumbnail.ts +++ b/packages/builders/src/components/v2/Thumbnail.ts @@ -27,7 +27,7 @@ export class ThumbnailBuilder extends ComponentBuilder { * * @param spoiler - The spoiler status to use */ - public setSpoiler(spoiler: boolean) { + public setSpoiler(spoiler = true) { this.data.spoiler = spoilerPredicate.parse(spoiler); return this; } From 4e968b5f28492d08d9e775c7d4afa50f7cd37636 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:21:56 +0100 Subject: [PATCH 12/22] chore: bring in line with builders v2 --- .../__tests__/components/v2/container.test.ts | 95 ++++++++++++++++--- .../components/v2/mediagallery.test.ts | 15 +-- .../__tests__/components/v2/section.test.ts | 26 ++--- .../__tests__/components/v2/thumbnail.test.ts | 2 +- .../builders/src/components/Components.ts | 23 +++++ .../builders/src/components/v2/Assertions.ts | 12 +++ .../builders/src/components/v2/Container.ts | 35 +++++-- packages/builders/src/components/v2/File.ts | 25 +++++ .../src/components/v2/MediaGallery.ts | 24 +++-- .../src/components/v2/MediaGalleryItem.ts | 36 ++++++- .../builders/src/components/v2/Section.ts | 56 ++++++----- .../builders/src/components/v2/Separator.ts | 21 ++++ .../builders/src/components/v2/TextDisplay.ts | 20 ++++ .../builders/src/components/v2/Thumbnail.ts | 35 ++++++- 14 files changed, 348 insertions(+), 77 deletions(-) diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts index 45b9b578e2ad..01fe25a0d5dc 100644 --- a/packages/builders/__tests__/components/v2/container.test.ts +++ b/packages/builders/__tests__/components/v2/container.test.ts @@ -45,9 +45,11 @@ describe('Container Components', () => { describe('Assertion Tests', () => { test('GIVEN valid components THEN do not throw', () => { expect(() => new ContainerBuilder().addComponents(new SeparatorBuilder())).not.toThrowError(); - expect(() => new ContainerBuilder().setComponents(new SeparatorBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder())).not.toThrowError(); expect(() => new ContainerBuilder().addComponents([new SeparatorBuilder()])).not.toThrowError(); - expect(() => new ContainerBuilder().setComponents([new SeparatorBuilder()])).not.toThrowError(); + expect(() => + new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]), + ).not.toThrowError(); }); test('GIVEN valid JSON input THEN valid JSON output is given', () => { @@ -78,7 +80,6 @@ describe('Container Components', () => { }; expect(new ContainerBuilder(containerData).toJSON()).toEqual(containerData); - expect(new ContainerBuilder().toJSON()).toEqual({ type: ComponentType.Container, components: [] }); expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError(); }); @@ -109,7 +110,6 @@ describe('Container Components', () => { expect(new ContainerBuilder(containerWithTextDisplay).toJSON()).toEqual(containerWithTextDisplay); expect(new ContainerBuilder(containerWithSeparatorData).toJSON()).toEqual(containerWithSeparatorData); - expect(new ContainerBuilder().toJSON()).toEqual({ type: ComponentType.Container, components: [] }); expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError(); }); @@ -124,19 +124,68 @@ describe('Container Components', () => { }); test('GIVEN valid accent color THEN valid JSON output is given', () => { - expect(new ContainerBuilder().setAccentColor([255, 0, 255]).toJSON()).toEqual({ + expect( + new ContainerBuilder({ + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + }) + .setAccentColor([255, 0, 255]) + .toJSON(), + ).toEqual({ type: ComponentType.Container, - components: [], + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], accent_color: 0xff00ff, }); - expect(new ContainerBuilder().setAccentColor(0xff00ff).toJSON()).toEqual({ + expect( + new ContainerBuilder({ + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + }) + .setAccentColor(0xff00ff) + .toJSON(), + ).toEqual({ type: ComponentType.Container, - components: [], + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], accent_color: 0xff00ff, }); - expect(new ContainerBuilder().setAccentColor([255, 0, 255]).setAccentColor(null).toJSON()).toEqual({ + expect( + new ContainerBuilder({ + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], + }) + .setAccentColor([255, 0, 255]) + .setAccentColor(null) + .toJSON(), + ).toEqual({ type: ComponentType.Container, - components: [], + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], }); expect(new ContainerBuilder(containerWithSeparatorData).setAccentColor(null).toJSON()).toEqual( containerWithSeparatorDataNoColor, @@ -144,14 +193,32 @@ describe('Container Components', () => { }); test('GIVEN valid method parameters THEN valid JSON is given', () => { - expect(new ContainerBuilder().setSpoiler().toJSON()).toEqual({ + expect( + new ContainerBuilder().addComponents(new TextDisplayBuilder().setContent('test')).setSpoiler().toJSON(), + ).toEqual({ type: ComponentType.Container, - components: [], + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], spoiler: true, }); - expect(new ContainerBuilder().setSpoiler(false).setId(5).toJSON()).toEqual({ + expect( + new ContainerBuilder() + .addComponents({ type: ComponentType.TextDisplay, content: 'test' }) + .setSpoiler(false) + .setId(5) + .toJSON(), + ).toEqual({ type: ComponentType.Container, - components: [], + components: [ + { + type: ComponentType.TextDisplay, + content: 'test', + }, + ], spoiler: false, id: 5, }); diff --git a/packages/builders/__tests__/components/v2/mediagallery.test.ts b/packages/builders/__tests__/components/v2/mediagallery.test.ts index 3e7bf03a6e5d..82a1ce827f67 100644 --- a/packages/builders/__tests__/components/v2/mediagallery.test.ts +++ b/packages/builders/__tests__/components/v2/mediagallery.test.ts @@ -27,11 +27,16 @@ const galleryAttachmentData: APIMediaGalleryComponent = { describe('Media Gallery Components', () => { describe('Assertion Tests', () => { + test('GIVEN an empty media gallery THEN throws error', () => { + const gallery = new MediaGalleryBuilder(); + expect(() => gallery.toJSON()).toThrow(); + }); + test('GIVEN valid items THEN do not throw', () => { expect(() => new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder())).not.toThrowError(); - expect(() => new MediaGalleryBuilder().setItems(new MediaGalleryItemBuilder())).not.toThrowError(); + expect(() => new MediaGalleryBuilder().spliceItems(0, 0, new MediaGalleryItemBuilder())).not.toThrowError(); expect(() => new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder()])).not.toThrowError(); - expect(() => new MediaGalleryBuilder().setItems([new MediaGalleryItemBuilder()])).not.toThrowError(); + expect(() => new MediaGalleryBuilder().spliceItems(0, 0, [new MediaGalleryItemBuilder()])).not.toThrowError(); }); test('GIVEN valid JSON input THEN valid JSON output is given', () => { @@ -52,7 +57,6 @@ describe('Media Gallery Components', () => { }; expect(new MediaGalleryBuilder(mediaGalleryData).toJSON()).toEqual(mediaGalleryData); - expect(new MediaGalleryBuilder().toJSON()).toEqual({ type: ComponentType.MediaGallery, items: [] }); expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError(); }); @@ -80,7 +84,6 @@ describe('Media Gallery Components', () => { expect(new MediaGalleryBuilder(galleryHttpsDisplay).toJSON()).toEqual(galleryHttpsDisplay); expect(new MediaGalleryBuilder(galleryAttachmentData).toJSON()).toEqual(galleryAttachmentData); - expect(new MediaGalleryBuilder().toJSON()).toEqual({ type: ComponentType.MediaGallery, items: [] }); expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError(); }); @@ -111,7 +114,7 @@ describe('Media Gallery Components', () => { ).toEqual(galleryHttpsDisplay); expect( new MediaGalleryBuilder() - .setItems((item) => item.setURL('attachment://file.png')) + .spliceItems(0, 0, (item) => item.setURL('attachment://file.png')) .setId(123) .toJSON(), ).toEqual(galleryAttachmentData); @@ -122,7 +125,7 @@ describe('Media Gallery Components', () => { ).toEqual(galleryHttpsDisplay); expect( new MediaGalleryBuilder() - .setItems([(item) => item.setURL('attachment://file.png')]) + .spliceItems(0, 0, [(item) => item.setDescription('test').clearDescription().setURL('attachment://file.png')]) .setId(123) .toJSON(), ).toEqual(galleryAttachmentData); diff --git a/packages/builders/__tests__/components/v2/section.test.ts b/packages/builders/__tests__/components/v2/section.test.ts index 5b74435030bc..cbea1a9b48b2 100644 --- a/packages/builders/__tests__/components/v2/section.test.ts +++ b/packages/builders/__tests__/components/v2/section.test.ts @@ -41,10 +41,12 @@ const sectionWithThumbnailData: APISectionComponent = { describe('Section Components', () => { describe('Assertion Tests', () => { test('GIVEN valid components THEN do not throw', () => { - expect(() => new SectionBuilder().addComponents(new TextDisplayBuilder())).not.toThrowError(); - expect(() => new SectionBuilder().setComponents(new TextDisplayBuilder())).not.toThrowError(); - expect(() => new SectionBuilder().addComponents([new TextDisplayBuilder()])).not.toThrowError(); - expect(() => new SectionBuilder().setComponents([new TextDisplayBuilder()])).not.toThrowError(); + expect(() => new SectionBuilder().addTextDisplayComponents(new TextDisplayBuilder())).not.toThrowError(); + expect(() => new SectionBuilder().spliceTextDisplayComponents(0, 0, new TextDisplayBuilder())).not.toThrowError(); + expect(() => new SectionBuilder().addTextDisplayComponents([new TextDisplayBuilder()])).not.toThrowError(); + expect(() => + new SectionBuilder().spliceTextDisplayComponents(0, 0, [new TextDisplayBuilder()]), + ).not.toThrowError(); }); test('GIVEN valid JSON input THEN valid JSON output is given', () => { @@ -135,16 +137,16 @@ describe('Section Components', () => { const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler().setURL('attachment://file.png'); const textDisplay = new TextDisplayBuilder().setContent('test'); - expect(new SectionBuilder().addComponents(textDisplay).setAccessory(button).toJSON()).toEqual( + expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setAccessory(button).toJSON()).toEqual( sectionWithButtonData, ); - expect(new SectionBuilder().addComponents(textDisplay).setAccessory(thumbnail).toJSON()).toEqual( + expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setAccessory(thumbnail).toJSON()).toEqual( sectionWithThumbnailData, ); - expect(new SectionBuilder().addComponents([textDisplay]).setAccessory(button).toJSON()).toEqual( + expect(new SectionBuilder().addTextDisplayComponents([textDisplay]).setAccessory(button).toJSON()).toEqual( sectionWithButtonData, ); - expect(new SectionBuilder().addComponents([textDisplay]).setAccessory(thumbnail).toJSON()).toEqual( + expect(new SectionBuilder().addTextDisplayComponents([textDisplay]).setAccessory(thumbnail).toJSON()).toEqual( sectionWithThumbnailData, ); }); @@ -154,25 +156,25 @@ describe('Section Components', () => { expect( new SectionBuilder() - .addComponents((textDisplay) => textDisplay.setContent('test')) + .addTextDisplayComponents((textDisplay) => textDisplay.setContent('test')) .setAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); expect( new SectionBuilder() - .setComponents((textDisplay) => textDisplay.setContent('test')) + .spliceTextDisplayComponents(0, 0, (textDisplay) => textDisplay.setContent('test')) .setAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); expect( new SectionBuilder() - .addComponents([(textDisplay) => textDisplay.setContent('test')]) + .addTextDisplayComponents([(textDisplay) => textDisplay.setContent('test')]) .setAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); expect( new SectionBuilder() - .setComponents([(textDisplay) => textDisplay.setContent('test')]) + .spliceTextDisplayComponents(0, 0, [(textDisplay) => textDisplay.setContent('test')]) .setAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); diff --git a/packages/builders/__tests__/components/v2/thumbnail.test.ts b/packages/builders/__tests__/components/v2/thumbnail.test.ts index fc7cdf6f7693..5bcd23af75bd 100644 --- a/packages/builders/__tests__/components/v2/thumbnail.test.ts +++ b/packages/builders/__tests__/components/v2/thumbnail.test.ts @@ -41,7 +41,7 @@ describe('Thumbnail', () => { test('GIVEN a thumbnail with a pre-defined description THEN unset description THEN return valid toJSON data', () => { const thumbnail = new ThumbnailBuilder({ description: 'foo', ...dummy }); - thumbnail.setDescription(); + thumbnail.clearDescription(); expect(thumbnail.toJSON()).toEqual({ ...dummy }); }); diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 139d3367322a..95ac1eafdd4f 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -1,3 +1,4 @@ +import type { JSONEncodable } from '@discordjs/util'; import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10'; import { ActionRowBuilder, @@ -163,3 +164,25 @@ export function createComponentBuilder( throw new Error(`Cannot properly serialize component type: ${data.type}`); } } + +function isBuilder>( + builder: unknown, + Constructor: new () => Builder, +): builder is Builder { + return builder instanceof Constructor; +} + +export function resolveBuilder, Builder extends JSONEncodable>( + builder: Builder | ComponentType | ((builder: Builder) => Builder), + Constructor: new (data?: ComponentType) => Builder, +) { + if (isBuilder(builder, Constructor)) { + return builder; + } + + if (typeof builder === 'function') { + return builder(new Constructor()); + } + + return new Constructor(builder); +} diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts index ac8728c6f980..85890108195b 100644 --- a/packages/builders/src/components/v2/Assertions.ts +++ b/packages/builders/src/components/v2/Assertions.ts @@ -2,7 +2,9 @@ import { s } from '@sapphire/shapeshift'; import { SeparatorSpacingSize } from 'discord-api-types/v10'; import { colorPredicate } from '../../messages/embed/Assertions'; import { isValidationEnabled } from '../../util/validation'; +import { ComponentBuilder } from '../Component'; import { ButtonBuilder } from '../button/Button'; +import type { ContainerComponentBuilder } from './Container'; import type { MediaGalleryItemBuilder } from './MediaGalleryItem'; import type { TextDisplayBuilder } from './TextDisplay'; import { ThumbnailBuilder } from './Thumbnail'; @@ -58,3 +60,13 @@ export function assertReturnOfBuilder(input: unknown, min: number, max: number, ExpectedInstanceOf?: new () => ReturnType): asserts input is ReturnType[] { + (ExpectedInstanceOf ? s.instance(ExpectedInstanceOf) : s.instance(ComponentBuilder)) + .array() + .lengthGreaterThanOrEqual(min) + .lengthLessThanOrEqual(max) + .parse(input); +} diff --git a/packages/builders/src/components/v2/Container.ts b/packages/builders/src/components/v2/Container.ts index 41b4f5c9025d..a4658c09365b 100644 --- a/packages/builders/src/components/v2/Container.ts +++ b/packages/builders/src/components/v2/Container.ts @@ -1,13 +1,13 @@ /* eslint-disable jsdoc/check-param-names */ -import type { APIContainerComponent } from 'discord-api-types/v10'; +import type { APIComponentInContainer, APIContainerComponent } from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; -import type { RGBTuple } from '../../index.js'; +import type { MediaGalleryBuilder, RGBTuple, SectionBuilder } from '../../index.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import type { ActionRowBuilder, AnyComponentBuilder } from '../ActionRow.js'; import { ComponentBuilder } from '../Component.js'; import { createComponentBuilder } from '../Components.js'; -import { containerColorPredicate, spoilerPredicate } from './Assertions.js'; +import { containerColorPredicate, spoilerPredicate, validateComponentArray } from './Assertions.js'; import type { FileBuilder } from './File.js'; import type { SeparatorBuilder } from './Separator.js'; import type { TextDisplayBuilder } from './TextDisplay.js'; @@ -18,6 +18,8 @@ import type { TextDisplayBuilder } from './TextDisplay.js'; export type ContainerComponentBuilder = | ActionRowBuilder | FileBuilder + | MediaGalleryBuilder + | SectionBuilder | SeparatorBuilder | TextDisplayBuilder; @@ -90,18 +92,34 @@ export class ContainerBuilder extends ComponentBuilder { * * @param components - The components to add */ - public addComponents(...components: RestOrArray) { - this.components.push(...normalizeArray(components)); + public addComponents(...components: RestOrArray) { + this.components.push( + ...normalizeArray(components).map((component) => + component instanceof ComponentBuilder ? component : createComponentBuilder(component), + ), + ); return this; } /** - * Sets components for this container. + * Removes, replaces, or inserts components for this container. * + * @param index - The index to start removing, replacing or inserting components + * @param deleteCount - The amount of components to remove * @param components - The components to set */ - public setComponents(...components: RestOrArray) { - this.components.splice(0, this.components.length, ...normalizeArray(components)); + public spliceComponents( + index: number, + deleteCount: number, + ...components: RestOrArray + ) { + this.components.splice( + index, + deleteCount, + ...normalizeArray(components).map((component) => + component instanceof ComponentBuilder ? component : createComponentBuilder(component), + ), + ); return this; } @@ -119,6 +137,7 @@ export class ContainerBuilder extends ComponentBuilder { * {@inheritDoc ComponentBuilder.toJSON} */ public toJSON(): APIContainerComponent { + validateComponentArray(this.components, 1, 10); return { ...this.data, components: this.components.map((component) => component.toJSON()), diff --git a/packages/builders/src/components/v2/File.ts b/packages/builders/src/components/v2/File.ts index f77d6dc74f21..fb92a82d8ffc 100644 --- a/packages/builders/src/components/v2/File.ts +++ b/packages/builders/src/components/v2/File.ts @@ -3,6 +3,31 @@ import { ComponentBuilder } from '../Component'; import { filePredicate, spoilerPredicate } from './Assertions'; export class FileBuilder extends ComponentBuilder { + /** + * Creates a new file from API data. + * + * @param data - The API data to create this file with + * @example + * Creating a file from an API data object: + * ```ts + * const file = new FileBuilder({ + * spoiler: true, + * file: { + * url: 'attachment://file.png', + * }, + * }); + * ``` + * @example + * Creating a file using setters and API data: + * ```ts + * const file = new FileBuilder({ + * file: { + * url: 'attachment://image.jpg', + * }, + * }) + * .setSpoiler(false); + * ``` + */ public constructor(data: Partial = {}) { super({ type: ComponentType.File, ...data, file: data.file ? { url: data.file.url } : undefined }); } diff --git a/packages/builders/src/components/v2/MediaGallery.ts b/packages/builders/src/components/v2/MediaGallery.ts index 5ecbf4210859..c71db6285312 100644 --- a/packages/builders/src/components/v2/MediaGallery.ts +++ b/packages/builders/src/components/v2/MediaGallery.ts @@ -4,7 +4,8 @@ import type { APIMediaGalleryComponent } from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { ComponentBuilder } from '../Component.js'; -import { assertReturnOfBuilder } from './Assertions.js'; +import { resolveBuilder } from '../Components.js'; +import { assertReturnOfBuilder, validateComponentArray } from './Assertions.js'; import { MediaGalleryItemBuilder } from './MediaGalleryItem.js'; /** @@ -19,7 +20,7 @@ export class MediaGalleryBuilder extends ComponentBuilder { - const result = typeof input === 'function' ? input(new MediaGalleryItemBuilder()) : input; + const result = resolveBuilder(input, MediaGalleryItemBuilder); assertReturnOfBuilder(result, MediaGalleryItemBuilder); return result; @@ -75,18 +76,22 @@ export class MediaGalleryBuilder extends ComponentBuilder MediaGalleryItemBuilder)> ) { this.items.splice( - 0, - this.items.length, + index, + deleteCount, ...normalizeArray(items).map((input) => { - const result = typeof input === 'function' ? input(new MediaGalleryItemBuilder()) : input; + const result = resolveBuilder(input, MediaGalleryItemBuilder); assertReturnOfBuilder(result, MediaGalleryItemBuilder); return result; @@ -99,6 +104,7 @@ export class MediaGalleryBuilder extends ComponentBuilder item.toJSON()), diff --git a/packages/builders/src/components/v2/MediaGalleryItem.ts b/packages/builders/src/components/v2/MediaGalleryItem.ts index 4439a1f096f9..9e33298b79e4 100644 --- a/packages/builders/src/components/v2/MediaGalleryItem.ts +++ b/packages/builders/src/components/v2/MediaGalleryItem.ts @@ -4,14 +4,34 @@ import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } fr export class MediaGalleryItemBuilder implements JSONEncodable { /** - * The API data associated with this component. + * The API data associated with this media gallery item. */ public readonly data: Partial; /** - * Constructs a new kind of component. + * Creates a new media gallery item from API data. * - * @param data - The data to construct a component out of + * @param data - The API data to create this media gallery item with + * @example + * Creating a media gallery item from an API data object: + * ```ts + * const item = new MediaGalleryItemBuilder({ + * description: "Some text here", + * media: { + * url: 'https://cdn.discordapp.com/embed/avatars/2.png', + * }, + * }); + * ``` + * @example + * Creating a media gallery item using setters and API data: + * ```ts + * const item = new MediaGalleryItemBuilder({ + * media: { + * url: 'https://cdn.discordapp.com/embed/avatars/5.png', + * }, + * }) + * .setDescription("alt text"); + * ``` */ public constructor(data: Partial = {}) { this.data = data; @@ -22,11 +42,19 @@ export class MediaGalleryItemBuilder implements JSONEncodable { public readonly accessory?: ButtonBuilder | ThumbnailBuilder; /** - * Creates a new container from API data. + * Creates a new section from API data. * - * @param data - The API data to create this container with + * @param data - The API data to create this section with * @example - * Creating a container from an API data object: + * Creating a section from an API data object: * ```ts - * const container = new ContainerBuilder({ + * const section = new SectionBuilder({ * components: [ * { * content: "Some text here", * type: ComponentType.TextDisplay, * }, * ], + * accessory: { + * media: { + * url: 'https://cdn.discordapp.com/embed/avatars/3.png', + * }, + * } * }); * ``` * @example - * Creating a container using setters and API data: + * Creating a section using setters and API data: * ```ts - * const container = new ContainerBuilder({ + * const section = new SectionBuilder({ * components: [ * { * content: "# Heading", @@ -50,7 +55,7 @@ export class SectionBuilder extends ComponentBuilder { * }, * ], * }) - * .addComponents(separator, section); + * .setPrimaryButtonAccessory(button); * ``` */ public constructor({ components, accessory, ...data }: Partial = {}) { @@ -70,16 +75,16 @@ export class SectionBuilder extends ComponentBuilder { } /** - * Adds components to this container. + * Adds text display components to this section. * - * @param components - The components to add + * @param components - The text display components to add */ - public addComponents( - ...components: RestOrArray TextDisplayBuilder)> + public addTextDisplayComponents( + ...components: RestOrArray TextDisplayBuilder)> ) { this.components.push( ...normalizeArray(components).map((input) => { - const result = typeof input === 'function' ? input(new TextDisplayBuilder()) : input; + const result = resolveBuilder(input, TextDisplayBuilder); assertReturnOfBuilder(result, TextDisplayBuilder); return result; @@ -89,18 +94,24 @@ export class SectionBuilder extends ComponentBuilder { } /** - * Sets components for this container. + * Removes, replaces, or inserts text display components for this section. * - * @param components - The components to set + * @param index - The index to start removing, replacing or inserting text display components + * @param deleteCount - The amount of text display components to remove + * @param components - The text display components to insert */ - public setComponents( - ...components: RestOrArray TextDisplayBuilder)> + public spliceTextDisplayComponents( + index: number, + deleteCount: number, + ...components: RestOrArray< + APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder) + > ) { this.components.splice( - 0, - this.components.length, + index, + deleteCount, ...normalizeArray(components).map((input) => { - const result = typeof input === 'function' ? input(new TextDisplayBuilder()) : input; + const result = resolveBuilder(input, TextDisplayBuilder); assertReturnOfBuilder(result, TextDisplayBuilder); return result; @@ -113,6 +124,7 @@ export class SectionBuilder extends ComponentBuilder { * {@inheritDoc ComponentBuilder.toJSON} */ public toJSON(): APISectionComponent { + validateComponentArray(this.components, 1, 3, TextDisplayBuilder); return { ...this.data, components: this.components.map((component) => component.toJSON()), diff --git a/packages/builders/src/components/v2/Separator.ts b/packages/builders/src/components/v2/Separator.ts index 1e506eaa34cc..579921885684 100644 --- a/packages/builders/src/components/v2/Separator.ts +++ b/packages/builders/src/components/v2/Separator.ts @@ -4,6 +4,27 @@ import { ComponentBuilder } from '../Component'; import { dividerPredicate, spacingPredicate } from './Assertions'; export class SeparatorBuilder extends ComponentBuilder { + /** + * Creates a new separator from API data. + * + * @param data - The API data to create this separator with + * @example + * Creating a separator from an API data object: + * ```ts + * const separator = new SeparatorBuilder({ + * spacing: SeparatorSpacingSize.Small, + * divider: true, + * }); + * ``` + * @example + * Creating a separator using setters and API data: + * ```ts + * const separator = new SeparatorBuilder({ + * spacing: SeparatorSpacingSize.Large, + * }) + * .setDivider(false); + * ``` + */ public constructor(data: Partial = {}) { super({ type: ComponentType.Separator, diff --git a/packages/builders/src/components/v2/TextDisplay.ts b/packages/builders/src/components/v2/TextDisplay.ts index f2e0419d83bd..61bfefa4f3e4 100644 --- a/packages/builders/src/components/v2/TextDisplay.ts +++ b/packages/builders/src/components/v2/TextDisplay.ts @@ -4,6 +4,26 @@ import { ComponentBuilder } from '../Component'; import { textDisplayContentPredicate } from './Assertions'; export class TextDisplayBuilder extends ComponentBuilder { + /** + * Creates a new text display from API data. + * + * @param data - The API data to create this text display with + * @example + * Creating a text display from an API data object: + * ```ts + * const textDisplay = new TextDisplayBuilder({ + * content: 'some text', + * }); + * ``` + * @example + * Creating a text display using setters and API data: + * ```ts + * const textDisplay = new TextDisplayBuilder({ + * content: 'old text', + * }) + * .setContent('new text'); + * ``` + */ public constructor(data: Partial = {}) { super({ type: ComponentType.TextDisplay, diff --git a/packages/builders/src/components/v2/Thumbnail.ts b/packages/builders/src/components/v2/Thumbnail.ts index 1259451c4142..58823bad623e 100644 --- a/packages/builders/src/components/v2/Thumbnail.ts +++ b/packages/builders/src/components/v2/Thumbnail.ts @@ -4,6 +4,31 @@ import { ComponentBuilder } from '../Component'; import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions'; export class ThumbnailBuilder extends ComponentBuilder { + /** + * Creates a new thumbnail from API data. + * + * @param data - The API data to create this thumbnail with + * @example + * Creating a thumbnail from an API data object: + * ```ts + * const thumbnaik = new ThumbnailBuilder({ + * description: 'some text', + * media: { + * url: 'https://cdn.discordapp.com/embed/assets/4.png', + * }, + * }); + * ``` + * @example + * Creating a thumbnail using setters and API data: + * ```ts + * const thumbnail = new ThumbnailBuilder({ + * media: { + * url: 'attachment://image.png', + * }, + * }) + * .setDescription('alt text'); + * ``` + */ public constructor(data: Partial = {}) { super({ type: ComponentType.Thumbnail, @@ -17,11 +42,19 @@ export class ThumbnailBuilder extends ComponentBuilder { * * @param description - The description to use */ - public setDescription(description?: string | undefined) { + public setDescription(description: string) { this.data.description = descriptionPredicate.parse(description); return this; } + /** + * Clears the description of this thumbnail. + */ + public clearDescription() { + this.data.description = undefined; + return this; + } + /** * Sets the spoiler status of this thumbnail. * From 3dcfbbad14cfffa48baa1465ee2b3083eedbbad0 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:32:39 +0100 Subject: [PATCH 13/22] fix: add missing type --- packages/builders/src/components/Components.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 95ac1eafdd4f..a2a03ff8b96c 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -29,7 +29,9 @@ export type MessageComponentBuilder = | ActionRowBuilder | ContainerBuilder | FileBuilder + | MediaGalleryBuilder | MessageActionRowComponentBuilder + | SectionBuilder | SeparatorBuilder | TextDisplayBuilder | ThumbnailBuilder; From c1ceef30d4d8a9753043c9aa834a9a90659b3f93 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:45:43 +0100 Subject: [PATCH 14/22] chore: split accessory methods --- .../__tests__/components/v2/section.test.ts | 36 +++++++++++-------- .../builders/src/components/v2/Section.ts | 29 ++++++++++++--- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/builders/__tests__/components/v2/section.test.ts b/packages/builders/__tests__/components/v2/section.test.ts index cbea1a9b48b2..05af792e0153 100644 --- a/packages/builders/__tests__/components/v2/section.test.ts +++ b/packages/builders/__tests__/components/v2/section.test.ts @@ -137,18 +137,26 @@ describe('Section Components', () => { const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler().setURL('attachment://file.png'); const textDisplay = new TextDisplayBuilder().setContent('test'); - expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setAccessory(button).toJSON()).toEqual( + expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setButtonAccessory(button).toJSON()).toEqual( sectionWithButtonData, ); - expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setAccessory(thumbnail).toJSON()).toEqual( - sectionWithThumbnailData, - ); - expect(new SectionBuilder().addTextDisplayComponents([textDisplay]).setAccessory(button).toJSON()).toEqual( - sectionWithButtonData, - ); - expect(new SectionBuilder().addTextDisplayComponents([textDisplay]).setAccessory(thumbnail).toJSON()).toEqual( - sectionWithThumbnailData, - ); + expect( + new SectionBuilder().addTextDisplayComponents(textDisplay).setThumbnailAccessory(thumbnail).toJSON(), + ).toEqual(sectionWithThumbnailData); + expect( + new SectionBuilder() + .addTextDisplayComponents([textDisplay]) + .setButtonAccessory((button) => button.setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123')) + .toJSON(), + ).toEqual(sectionWithButtonData); + expect( + new SectionBuilder() + .addTextDisplayComponents([textDisplay]) + .setThumbnailAccessory((thumbnail) => + thumbnail.setDescription('test').setSpoiler().setURL('attachment://file.png'), + ) + .toJSON(), + ).toEqual(sectionWithThumbnailData); }); test('GIVEN valid builder callback THEN valid JSON output is given', () => { @@ -157,25 +165,25 @@ describe('Section Components', () => { expect( new SectionBuilder() .addTextDisplayComponents((textDisplay) => textDisplay.setContent('test')) - .setAccessory(button) + .setButtonAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); expect( new SectionBuilder() .spliceTextDisplayComponents(0, 0, (textDisplay) => textDisplay.setContent('test')) - .setAccessory(button) + .setButtonAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); expect( new SectionBuilder() .addTextDisplayComponents([(textDisplay) => textDisplay.setContent('test')]) - .setAccessory(button) + .setButtonAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); expect( new SectionBuilder() .spliceTextDisplayComponents(0, 0, [(textDisplay) => textDisplay.setContent('test')]) - .setAccessory(button) + .setButtonAccessory(button) .toJSON(), ).toEqual(sectionWithButtonData); }); diff --git a/packages/builders/src/components/v2/Section.ts b/packages/builders/src/components/v2/Section.ts index ac59cba22700..432d9ba2d482 100644 --- a/packages/builders/src/components/v2/Section.ts +++ b/packages/builders/src/components/v2/Section.ts @@ -1,8 +1,13 @@ /* eslint-disable jsdoc/check-param-names */ -import type { APISectionComponent, APITextDisplayComponent } from 'discord-api-types/v10'; +import type { + APIButtonComponent, + APISectionComponent, + APITextDisplayComponent, + APIThumbnailComponent, +} from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; -import type { ButtonBuilder, ThumbnailBuilder } from '../../index.js'; +import { ButtonBuilder, ThumbnailBuilder } from '../../index.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { ComponentBuilder } from '../Component.js'; import { createComponentBuilder, resolveBuilder } from '../Components.js'; @@ -65,12 +70,26 @@ export class SectionBuilder extends ComponentBuilder { } /** - * Sets the accessory of this section. + * Sets the accessory of this section to a button. * * @param accessory - The accessory to use */ - public setAccessory(accessory: ButtonBuilder | ThumbnailBuilder): this { - Reflect.set(this, 'accessory', accessoryPredicate.parse(accessory)); + public setButtonAccessory( + accessory: APIButtonComponent | ButtonBuilder | ((builder: ButtonBuilder) => ButtonBuilder), + ): this { + Reflect.set(this, 'accessory', accessoryPredicate.parse(resolveBuilder(accessory, ButtonBuilder))); + return this; + } + + /** + * Sets the accessory of this section to a thumbnail. + * + * @param accessory - The accessory to use + */ + public setThumbnailAccessory( + accessory: APIThumbnailComponent | ThumbnailBuilder | ((builder: ThumbnailBuilder) => ThumbnailBuilder), + ): this { + Reflect.set(this, 'accessory', accessoryPredicate.parse(resolveBuilder(accessory, ThumbnailBuilder))); return this; } From 77efc97ee16c370461fb5d7625f17a36cfd5c2f1 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:19:11 +0100 Subject: [PATCH 15/22] chore: backport changes from v2 PR --- .../__tests__/components/v2/container.test.ts | 4 ++-- packages/builders/src/components/Assertions.ts | 1 - packages/builders/src/components/Component.ts | 9 ++++++++- packages/builders/src/components/v2/Assertions.ts | 6 ++---- packages/builders/src/components/v2/Container.ts | 12 ++++++++++-- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts index 01fe25a0d5dc..ae94e81a49f9 100644 --- a/packages/builders/__tests__/components/v2/container.test.ts +++ b/packages/builders/__tests__/components/v2/container.test.ts @@ -176,7 +176,7 @@ describe('Container Components', () => { ], }) .setAccentColor([255, 0, 255]) - .setAccentColor(null) + .clearAccentColor() .toJSON(), ).toEqual({ type: ComponentType.Container, @@ -187,7 +187,7 @@ describe('Container Components', () => { }, ], }); - expect(new ContainerBuilder(containerWithSeparatorData).setAccentColor(null).toJSON()).toEqual( + expect(new ContainerBuilder(containerWithSeparatorData).clearAccentColor().toJSON()).toEqual( containerWithSeparatorDataNoColor, ); }); diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 64a44e2f282e..df8d8a4099f4 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -8,7 +8,6 @@ export const idValidator = s .safeInt() .greaterThanOrEqual(1) .lessThan(4_294_967_296) - .optional() .setValidationEnabled(isValidationEnabled); export const customIdValidator = s diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index c88c2eddc560..7c06b2a6e4b4 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -53,8 +53,15 @@ export abstract class ComponentBuilder< * * @param id - The id for this component */ - public setId(id?: number | undefined) { + public setId(id: number) { this.data.id = idValidator.parse(id); return this; } + + /** + * Clears the id of this component, defaulting to a default incremented id. + */ + public clearId() { + this.data.id = undefined; + } } diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts index 85890108195b..184026da54f1 100644 --- a/packages/builders/src/components/v2/Assertions.ts +++ b/packages/builders/src/components/v2/Assertions.ts @@ -1,6 +1,5 @@ import { s } from '@sapphire/shapeshift'; import { SeparatorSpacingSize } from 'discord-api-types/v10'; -import { colorPredicate } from '../../messages/embed/Assertions'; import { isValidationEnabled } from '../../util/validation'; import { ComponentBuilder } from '../Component'; import { ButtonBuilder } from '../button/Button'; @@ -24,7 +23,6 @@ export const descriptionPredicate = s .string() .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(1_024) - .nullish() .setValidationEnabled(isValidationEnabled); export const filePredicate = s @@ -41,8 +39,6 @@ export const dividerPredicate = s.boolean(); export const spacingPredicate = s.nativeEnum(SeparatorSpacingSize); -export const containerColorPredicate = colorPredicate.nullish(); - export const textDisplayContentPredicate = s .string() .lengthGreaterThanOrEqual(1) @@ -70,3 +66,5 @@ export function validateComponentArray< .lengthLessThanOrEqual(max) .parse(input); } + +export { colorPredicate as containerColorPredicate } from '../../messages/embed/Assertions'; diff --git a/packages/builders/src/components/v2/Container.ts b/packages/builders/src/components/v2/Container.ts index a4658c09365b..2c7fb7f87bd3 100644 --- a/packages/builders/src/components/v2/Container.ts +++ b/packages/builders/src/components/v2/Container.ts @@ -73,7 +73,7 @@ export class ContainerBuilder extends ComponentBuilder { * * @param color - The color to use */ - public setAccentColor(color?: RGBTuple | number | null | undefined): this { + public setAccentColor(color?: RGBTuple | number): this { // Data assertions containerColorPredicate.parse(color); @@ -83,7 +83,15 @@ export class ContainerBuilder extends ComponentBuilder { return this; } - this.data.accent_color = color ?? undefined; + this.data.accent_color = color; + return this; + } + + /** + * Clears the accent color of this container. + */ + public clearAccentColor() { + this.data.accent_color = undefined; return this; } From 3800a61773effdd38ad948afe852b6152854f97c Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:45:50 +0100 Subject: [PATCH 16/22] fix: accent_color is nullish --- packages/builders/src/components/v2/Assertions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/builders/src/components/v2/Assertions.ts b/packages/builders/src/components/v2/Assertions.ts index 184026da54f1..6bcbff8c308d 100644 --- a/packages/builders/src/components/v2/Assertions.ts +++ b/packages/builders/src/components/v2/Assertions.ts @@ -1,5 +1,6 @@ import { s } from '@sapphire/shapeshift'; import { SeparatorSpacingSize } from 'discord-api-types/v10'; +import { colorPredicate } from '../../messages/embed/Assertions'; import { isValidationEnabled } from '../../util/validation'; import { ComponentBuilder } from '../Component'; import { ButtonBuilder } from '../button/Button'; @@ -50,6 +51,8 @@ export const accessoryPredicate = s .or(s.instance(ThumbnailBuilder)) .setValidationEnabled(isValidationEnabled); +export const containerColorPredicate = colorPredicate.nullish(); + export function assertReturnOfBuilder( input: unknown, ExpectedInstanceOf: new () => ReturnType, @@ -66,5 +69,3 @@ export function validateComponentArray< .lengthLessThanOrEqual(max) .parse(input); } - -export { colorPredicate as containerColorPredicate } from '../../messages/embed/Assertions'; From 3149f55b12ba476f7c431fb7cb8a62514fc5b169 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:46:41 +0100 Subject: [PATCH 17/22] fix: allow passing raw json to MediaGallery methods --- packages/builders/src/components/v2/MediaGallery.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/builders/src/components/v2/MediaGallery.ts b/packages/builders/src/components/v2/MediaGallery.ts index c71db6285312..cad2b5a22a5e 100644 --- a/packages/builders/src/components/v2/MediaGallery.ts +++ b/packages/builders/src/components/v2/MediaGallery.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/check-param-names */ -import type { APIMediaGalleryComponent } from 'discord-api-types/v10'; +import type { APIMediaGalleryComponent, APIMediaGalleryItem } from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { ComponentBuilder } from '../Component.js'; @@ -62,7 +62,9 @@ export class MediaGalleryBuilder extends ComponentBuilder MediaGalleryItemBuilder)> + ...items: RestOrArray< + APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder) + > ) { this.items.push( ...normalizeArray(items).map((input) => { @@ -85,7 +87,9 @@ export class MediaGalleryBuilder extends ComponentBuilder MediaGalleryItemBuilder)> + ...items: RestOrArray< + APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder) + > ) { this.items.splice( index, From 231f046bf91d788f9333e85fbbb6571210a4fb6b Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:56:28 +0100 Subject: [PATCH 18/22] fix: add test --- .../__tests__/components/v2/container.test.ts | 5 ++++- .../components/v2/mediagallery.test.ts | 18 +++++++++++++++++- packages/builders/src/components/Component.ts | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts index ae94e81a49f9..d64ee7f12ee6 100644 --- a/packages/builders/__tests__/components/v2/container.test.ts +++ b/packages/builders/__tests__/components/v2/container.test.ts @@ -194,7 +194,10 @@ describe('Container Components', () => { test('GIVEN valid method parameters THEN valid JSON is given', () => { expect( - new ContainerBuilder().addComponents(new TextDisplayBuilder().setContent('test')).setSpoiler().toJSON(), + new ContainerBuilder() + .addComponents(new TextDisplayBuilder().setId(3).clearId().setContent('test')) + .setSpoiler() + .toJSON(), ).toEqual({ type: ComponentType.Container, components: [ diff --git a/packages/builders/__tests__/components/v2/mediagallery.test.ts b/packages/builders/__tests__/components/v2/mediagallery.test.ts index 82a1ce827f67..965059dc0ff1 100644 --- a/packages/builders/__tests__/components/v2/mediagallery.test.ts +++ b/packages/builders/__tests__/components/v2/mediagallery.test.ts @@ -1,4 +1,4 @@ -import { type APIMediaGalleryComponent, ComponentType } from 'discord-api-types/v10'; +import { type APIMediaGalleryItem, type APIMediaGalleryComponent, ComponentType } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { createComponentBuilder } from '../../../src/components/Components.js'; import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js'; @@ -100,6 +100,22 @@ describe('Media Gallery Components', () => { expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData); }); + test('GIVEN valid JSON options THEN valid JSON output is given 2', () => { + const item1: APIMediaGalleryItem = { + description: 'test', + spoiler: false, + media: { url: 'https://discord.com/logo.png' }, + }; + const item2 = { + media: { url: 'attachment://file.png' }, + }; + + expect(new MediaGalleryBuilder().addItems(item1).toJSON()).toEqual(galleryHttpsDisplay); + expect(new MediaGalleryBuilder().addItems(item2).setId(123).toJSON()).toEqual(galleryAttachmentData); + expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay); + expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData); + }); + test('GIVEN valid builder callback THEN valid JSON output is given', () => { const item1 = new MediaGalleryItemBuilder() .setDescription('test') diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 7c06b2a6e4b4..bd6e9f1bb22a 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -63,5 +63,6 @@ export abstract class ComponentBuilder< */ public clearId() { this.data.id = undefined; + return this; } } From ea11041422287419cc44b10f9c9ebb5a03497b46 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 15 Mar 2025 10:55:14 +0100 Subject: [PATCH 19/22] chore: add Container#addXComponents --- .../__tests__/components/v2/container.test.ts | 34 ++++-- .../builders/src/components/v2/Container.ts | 112 ++++++++++++++++-- 2 files changed, 125 insertions(+), 21 deletions(-) diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts index d64ee7f12ee6..b01d2fd92a82 100644 --- a/packages/builders/__tests__/components/v2/container.test.ts +++ b/packages/builders/__tests__/components/v2/container.test.ts @@ -1,7 +1,12 @@ import { type APIContainerComponent, ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; +import { ButtonBuilder } from '../../../dist/index.mjs'; +import { ActionRowBuilder } from '../../../src/components/ActionRow.js'; import { createComponentBuilder } from '../../../src/components/Components.js'; import { ContainerBuilder } from '../../../src/components/v2/Container.js'; +import { FileBuilder } from '../../../src/components/v2/File.js'; +import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js'; +import { SectionBuilder } from '../../../src/components/v2/Section.js'; import { SeparatorBuilder } from '../../../src/components/v2/Separator.js'; import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js'; @@ -44,9 +49,18 @@ const containerWithSeparatorDataNoColor: APIContainerComponent = { describe('Container Components', () => { describe('Assertion Tests', () => { test('GIVEN valid components THEN do not throw', () => { - expect(() => new ContainerBuilder().addComponents(new SeparatorBuilder())).not.toThrowError(); + expect(() => + new ContainerBuilder().addActionRowComponents( + new ActionRowBuilder().addComponents(new ButtonBuilder()), + ), + ).not.toThrowError(); + expect(() => new ContainerBuilder().addFileComponents(new FileBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().addMediaGalleryComponents(new MediaGalleryBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().addSectionComponents(new SectionBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().addSeparatorComponents(new SeparatorBuilder())).not.toThrowError(); + expect(() => new ContainerBuilder().addTextDisplayComponents(new TextDisplayBuilder())).not.toThrowError(); expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder())).not.toThrowError(); - expect(() => new ContainerBuilder().addComponents([new SeparatorBuilder()])).not.toThrowError(); + expect(() => new ContainerBuilder().addSeparatorComponents([new SeparatorBuilder()])).not.toThrowError(); expect(() => new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]), ).not.toThrowError(); @@ -117,10 +131,14 @@ describe('Container Components', () => { const textDisplay = new TextDisplayBuilder().setContent('test').setId(123); const separator = new SeparatorBuilder().setId(1_234).setSpacing(SeparatorSpacingSize.Small).setDivider(false); - expect(new ContainerBuilder().addComponents(textDisplay).toJSON()).toEqual(containerWithTextDisplay); - expect(new ContainerBuilder().addComponents(separator).toJSON()).toEqual(containerWithSeparatorDataNoColor); - expect(new ContainerBuilder().addComponents([textDisplay]).toJSON()).toEqual(containerWithTextDisplay); - expect(new ContainerBuilder().addComponents([separator]).toJSON()).toEqual(containerWithSeparatorDataNoColor); + expect(new ContainerBuilder().addTextDisplayComponents(textDisplay).toJSON()).toEqual(containerWithTextDisplay); + expect(new ContainerBuilder().addSeparatorComponents(separator).toJSON()).toEqual( + containerWithSeparatorDataNoColor, + ); + expect(new ContainerBuilder().addTextDisplayComponents([textDisplay]).toJSON()).toEqual(containerWithTextDisplay); + expect(new ContainerBuilder().addSeparatorComponents([separator]).toJSON()).toEqual( + containerWithSeparatorDataNoColor, + ); }); test('GIVEN valid accent color THEN valid JSON output is given', () => { @@ -195,7 +213,7 @@ describe('Container Components', () => { test('GIVEN valid method parameters THEN valid JSON is given', () => { expect( new ContainerBuilder() - .addComponents(new TextDisplayBuilder().setId(3).clearId().setContent('test')) + .addTextDisplayComponents(new TextDisplayBuilder().setId(3).clearId().setContent('test')) .setSpoiler() .toJSON(), ).toEqual({ @@ -210,7 +228,7 @@ describe('Container Components', () => { }); expect( new ContainerBuilder() - .addComponents({ type: ComponentType.TextDisplay, content: 'test' }) + .addTextDisplayComponents({ type: ComponentType.TextDisplay, content: 'test' }) .setSpoiler(false) .setId(5) .toJSON(), diff --git a/packages/builders/src/components/v2/Container.ts b/packages/builders/src/components/v2/Container.ts index 2c7fb7f87bd3..a446f3db9a71 100644 --- a/packages/builders/src/components/v2/Container.ts +++ b/packages/builders/src/components/v2/Container.ts @@ -1,16 +1,28 @@ /* eslint-disable jsdoc/check-param-names */ -import type { APIComponentInContainer, APIContainerComponent } from 'discord-api-types/v10'; +import type { + APIActionRowComponent, + APIComponentInContainer, + APIComponentInMessageActionRow, + APIContainerComponent, + APIFileComponent, + APIMediaGalleryComponent, + APISectionComponent, + APISeparatorComponent, + APITextDisplayComponent, +} from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10'; -import type { MediaGalleryBuilder, RGBTuple, SectionBuilder } from '../../index.js'; +import type { RGBTuple } from '../../index.js'; +import { MediaGalleryBuilder, SectionBuilder } from '../../index.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; -import type { ActionRowBuilder, AnyComponentBuilder } from '../ActionRow.js'; +import type { AnyComponentBuilder, MessageActionRowComponentBuilder } from '../ActionRow.js'; +import { ActionRowBuilder } from '../ActionRow.js'; import { ComponentBuilder } from '../Component.js'; -import { createComponentBuilder } from '../Components.js'; +import { createComponentBuilder, resolveBuilder } from '../Components.js'; import { containerColorPredicate, spoilerPredicate, validateComponentArray } from './Assertions.js'; -import type { FileBuilder } from './File.js'; -import type { SeparatorBuilder } from './Separator.js'; -import type { TextDisplayBuilder } from './TextDisplay.js'; +import { FileBuilder } from './File.js'; +import { SeparatorBuilder } from './Separator.js'; +import { TextDisplayBuilder } from './TextDisplay.js'; /** * The builders that may be used within a container. @@ -96,15 +108,89 @@ export class ContainerBuilder extends ComponentBuilder { } /** - * Adds components to this container. + * Adds action row components to this container. * - * @param components - The components to add + * @param components - The action row components to add */ - public addComponents(...components: RestOrArray) { + public addActionRowComponents( + ...components: RestOrArray< + | ActionRowBuilder + | APIActionRowComponent + | ((builder: ActionRowBuilder) => ActionRowBuilder) + > + ) { this.components.push( - ...normalizeArray(components).map((component) => - component instanceof ComponentBuilder ? component : createComponentBuilder(component), - ), + ...normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder)), + ); + return this; + } + + /** + * Adds file components to this container. + * + * @param components - The file components to add + */ + public addFileComponents( + ...components: RestOrArray FileBuilder)> + ) { + this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, FileBuilder))); + return this; + } + + /** + * Adds media gallery components to this container. + * + * @param components - The media gallery components to add + */ + public addMediaGalleryComponents( + ...components: RestOrArray< + APIMediaGalleryComponent | MediaGalleryBuilder | ((builder: MediaGalleryBuilder) => MediaGalleryBuilder) + > + ) { + this.components.push( + ...normalizeArray(components).map((component) => resolveBuilder(component, MediaGalleryBuilder)), + ); + return this; + } + + /** + * Adds section components to this container. + * + * @param components - The section components to add + */ + public addSectionComponents( + ...components: RestOrArray SectionBuilder)> + ) { + this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, SectionBuilder))); + return this; + } + + /** + * Adds separator components to this container. + * + * @param components - The separator components to add + */ + public addSeparatorComponents( + ...components: RestOrArray< + APISeparatorComponent | SeparatorBuilder | ((builder: SeparatorBuilder) => SeparatorBuilder) + > + ) { + this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, SeparatorBuilder))); + return this; + } + + /** + * Adds text display components to this container. + * + * @param components - The text display components to add + */ + public addTextDisplayComponents( + ...components: RestOrArray< + APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder) + > + ) { + this.components.push( + ...normalizeArray(components).map((component) => resolveBuilder(component, TextDisplayBuilder)), ); return this; } From 14aca0c69d3f52ddd403f012af8676895b2cf023 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:03:30 +0200 Subject: [PATCH 20/22] fix: docs --- packages/builders/src/components/v2/Thumbnail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builders/src/components/v2/Thumbnail.ts b/packages/builders/src/components/v2/Thumbnail.ts index 58823bad623e..f10922a043d8 100644 --- a/packages/builders/src/components/v2/Thumbnail.ts +++ b/packages/builders/src/components/v2/Thumbnail.ts @@ -14,7 +14,7 @@ export class ThumbnailBuilder extends ComponentBuilder { * const thumbnaik = new ThumbnailBuilder({ * description: 'some text', * media: { - * url: 'https://cdn.discordapp.com/embed/assets/4.png', + * url: 'https://cdn.discordapp.com/embed/avatars/4.png', * }, * }); * ``` From 3550a1338afff9698cfb7341328598537ac5118d Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:42:36 +0200 Subject: [PATCH 21/22] chore: bump discord-api-types --- packages/builders/package.json | 2 +- packages/discord.js/package.json | 2 +- packages/rest/package.json | 2 +- packages/rest/src/lib/utils/constants.ts | 7 +- pnpm-lock.yaml | 137 ++++++++++++----------- 5 files changed, 76 insertions(+), 74 deletions(-) diff --git a/packages/builders/package.json b/packages/builders/package.json index 90268f1a8b8e..7e47b910f223 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -68,7 +68,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/util": "workspace:^", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.38.0-next-1740095508888", + "discord-api-types": "^0.38.1", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 5f642cf03c2a..412bac33fb17 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -72,7 +72,7 @@ "@discordjs/util": "workspace:^", "@discordjs/ws": "1.1.1", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.38.0-next-1740095508888", + "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", diff --git a/packages/rest/package.json b/packages/rest/package.json index 38b2e070118a..8a2802456813 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -88,7 +88,7 @@ "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.37.119", + "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.1" diff --git a/packages/rest/src/lib/utils/constants.ts b/packages/rest/src/lib/utils/constants.ts index 91375b5a6df5..eb7fc7dfbb64 100644 --- a/packages/rest/src/lib/utils/constants.ts +++ b/packages/rest/src/lib/utils/constants.ts @@ -1,5 +1,5 @@ import { getUserAgentAppendix } from '@discordjs/util'; -import { APIVersion } from 'discord-api-types/v10'; +import { APIVersion, type ImageSize } from 'discord-api-types/v10'; import { getDefaultStrategy } from '../../environment.js'; import type { RESTOptions, ResponseLike } from './types.js'; @@ -48,11 +48,10 @@ export enum RESTEvents { export const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const satisfies readonly string[]; export const ALLOWED_STICKER_EXTENSIONS = ['png', 'json', 'gif'] as const satisfies readonly string[]; -export const ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096] as const satisfies readonly number[]; +export const ALLOWED_SIZES: readonly number[] = [16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096] satisfies ImageSize[]; export type ImageExtension = (typeof ALLOWED_EXTENSIONS)[number]; export type StickerExtension = (typeof ALLOWED_STICKER_EXTENSIONS)[number]; -export type ImageSize = (typeof ALLOWED_SIZES)[number]; export const OverwrittenMimeTypes = { // https://github.com/discordjs/discord.js/issues/8557 @@ -67,3 +66,5 @@ export const BurstHandlerMajorIdKey = 'burst'; * @internal */ export const DEPRECATION_WARNING_PREFIX = 'DeprecationWarning' as const; + +export { type ImageSize } from 'discord-api-types/v10'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00657558e014..f83728fff4d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,8 +680,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 discord-api-types: - specifier: 0.38.0-next-1740095508888 - version: 0.38.0-next-1740095508888 + specifier: ^0.38.1 + version: 0.38.1 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -941,8 +941,8 @@ importers: specifier: 3.5.3 version: 3.5.3 discord-api-types: - specifier: 0.38.0-next-1740095508888 - version: 0.38.0-next-1740095508888 + specifier: ^0.38.1 + version: 0.38.1 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -1307,8 +1307,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: ^0.37.119 - version: 0.37.119 + specifier: ^0.38.1 + version: 0.38.1 magic-bytes.js: specifier: ^1.10.0 version: 1.10.0 @@ -2601,12 +2601,12 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@definitelytyped/header-parser@0.2.17': - resolution: {integrity: sha512-U0juKFkTOcbkSfO83WSzMEJHYDwoBFiq0tf/JszulL3+7UoSiqunpGmxXS54bm3eGqy7GWjV8AqPQHdeoEaWBQ==} + '@definitelytyped/header-parser@0.2.19': + resolution: {integrity: sha512-zu+RxQpUCgorYUQZoyyrRIn9CljL1CeM4qak3NDeMO1r7tjAkodfpAGnVzx/6JR2OUk0tAgwmZxNMSwd9LVgxw==} engines: {node: '>=18.18.0'} - '@definitelytyped/typescript-versions@0.1.7': - resolution: {integrity: sha512-sBzBi1SBn79OkSr8V0H+FzR7QumHk23syPyRxod/VRBrSkgN9rCliIe+nqLoWRAKN8EeKbp00ketnJNLZhucdA==} + '@definitelytyped/typescript-versions@0.1.8': + resolution: {integrity: sha512-iz6q9aTwWW7CzN2g8jFQfZ955D63LA+wdIAKz4+2pCc/7kokmEHie1/jVWSczqLFOlmH+69bWQxIurryBP/sig==} engines: {node: '>=18.18.0'} '@definitelytyped/utils@0.1.8': @@ -7638,8 +7638,8 @@ packages: discord-api-types@0.37.83: resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} - discord-api-types@0.38.0-next-1740095508888: - resolution: {integrity: sha512-J9ZpbCe8sO/TJIhjUdrfZmR+VPqD/x0WzkiAv4u6H/4Cazo0nE3lYU31wRFbaiXDKcw6+hUjkVVfp5dWLrGdNA==} + discord-api-types@0.38.1: + resolution: {integrity: sha512-vsjsqjAuxsPhiwbPjTBeGQaDPlizFmSkU0mTzFGMgRxqCDIRBR7iTY74HacpzrDV0QtERHRKQEk1tq7drZUtHg==} dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -8105,6 +8105,7 @@ packages: eslint-plugin-i@2.29.1: resolution: {integrity: sha512-ORizX37MelIWLbMyqI7hi8VJMf7A0CskMmYkB+lkCX3aF4pkGV7kwx5bSEb4qx7Yce2rAf9s34HqDRPjGRZPNQ==} engines: {node: '>=12'} + deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead peerDependencies: eslint: ^7.2.0 || ^8 @@ -13865,7 +13866,7 @@ snapshots: '@babel/helper-plugin-utils': 7.24.8 debug: 4.4.0 lodash.debounce: 4.0.8 - resolve: 1.22.8 + resolve: 1.22.10 transitivePeerDependencies: - supports-color @@ -14614,7 +14615,7 @@ snapshots: '@babel/parser': 7.25.4 '@babel/template': 7.25.0 '@babel/types': 7.25.4 - debug: 4.3.6 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -14842,7 +14843,7 @@ snapshots: '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.0.0)': dependencies: '@types/semver': 7.5.8 - semver: 7.6.3 + semver: 7.5.4 optionalDependencies: conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.0.0 @@ -14851,13 +14852,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@definitelytyped/header-parser@0.2.17': + '@definitelytyped/header-parser@0.2.19': dependencies: - '@definitelytyped/typescript-versions': 0.1.7 + '@definitelytyped/typescript-versions': 0.1.8 '@definitelytyped/utils': 0.1.8 semver: 7.6.3 - '@definitelytyped/typescript-versions@0.1.7': {} + '@definitelytyped/typescript-versions@0.1.8': {} '@definitelytyped/utils@0.1.8': dependencies: @@ -15314,7 +15315,7 @@ snapshots: '@antfu/install-pkg': 0.4.0 '@antfu/utils': 0.7.10 '@iconify/types': 2.0.0 - debug: 4.3.6 + debug: 4.4.0 kolorist: 1.8.0 local-pkg: 0.5.0 mlly: 1.7.1 @@ -15437,7 +15438,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -15517,7 +15518,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -15535,7 +15536,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -15557,7 +15558,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -15819,7 +15820,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@16.18.105) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -15838,7 +15839,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@18.17.9) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -15857,7 +15858,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@18.19.45) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -15875,7 +15876,7 @@ snapshots: '@rushstack/ts-command-line': 4.19.1(@types/node@20.16.1) lodash: 4.17.21 minimatch: 3.0.8 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 typescript: 5.4.2 @@ -18000,7 +18001,7 @@ snapshots: fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 z-schema: 5.0.5 optionalDependencies: @@ -18012,7 +18013,7 @@ snapshots: fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 z-schema: 5.0.5 optionalDependencies: @@ -18024,7 +18025,7 @@ snapshots: fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 z-schema: 5.0.5 optionalDependencies: @@ -18035,7 +18036,7 @@ snapshots: fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 - resolve: 1.22.8 + resolve: 1.22.10 semver: 7.5.4 z-schema: 5.0.5 optionalDependencies: @@ -18060,7 +18061,7 @@ snapshots: '@rushstack/rig-package@0.5.2': dependencies: - resolve: 1.22.8 + resolve: 1.22.10 strip-json-comments: 3.1.1 '@rushstack/terminal@0.10.0(@types/node@16.18.105)': @@ -18924,11 +18925,11 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/concat-stream@2.0.3': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/connect@3.4.38': dependencies: @@ -18936,13 +18937,13 @@ snapshots: '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/cookiejar@2.1.5': {} '@types/cross-spawn@6.0.6': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/debug@4.1.12': dependencies: @@ -18974,7 +18975,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -18991,11 +18992,11 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/hast@2.3.10': dependencies: @@ -19077,7 +19078,7 @@ snapshots: '@types/node-fetch@2.6.11': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 form-data: 4.0.0 '@types/node@16.18.105': {} @@ -19104,7 +19105,7 @@ snapshots: '@types/pg@8.11.6': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 pg-protocol: 1.6.1 pg-types: 4.0.2 @@ -19142,7 +19143,7 @@ snapshots: '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/send': 0.17.4 '@types/stack-utils@2.0.3': {} @@ -19163,7 +19164,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 '@types/tinycolor2@1.4.6': {} @@ -19283,7 +19284,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.11.0(typescript@5.5.4) '@typescript-eslint/utils': 7.11.0(eslint@8.57.0)(typescript@5.5.4) - debug: 4.3.6 + debug: 4.4.0 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: @@ -19295,7 +19296,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.5.4) - debug: 4.3.6 + debug: 4.4.0 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: @@ -19307,7 +19308,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) '@typescript-eslint/utils': 8.2.0(eslint@8.57.0)(typescript@5.5.4) - debug: 4.3.6 + debug: 4.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 @@ -19356,7 +19357,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -19371,7 +19372,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.2.0 '@typescript-eslint/visitor-keys': 8.2.0 - debug: 4.3.6 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -21514,7 +21515,7 @@ snapshots: discord-api-types@0.37.83: {} - discord-api-types@0.38.0-next-1740095508888: {} + discord-api-types@0.38.1: {} dlv@1.1.3: {} @@ -21565,7 +21566,7 @@ snapshots: dts-critic@3.3.11(typescript@5.5.4): dependencies: - '@definitelytyped/header-parser': 0.2.17 + '@definitelytyped/header-parser': 0.2.19 command-exists: 1.2.9 rimraf: 3.0.2 semver: 6.3.1 @@ -21575,8 +21576,8 @@ snapshots: dtslint@4.2.1(typescript@5.5.4): dependencies: - '@definitelytyped/header-parser': 0.2.17 - '@definitelytyped/typescript-versions': 0.1.7 + '@definitelytyped/header-parser': 0.2.19 + '@definitelytyped/typescript-versions': 0.1.8 '@definitelytyped/utils': 0.1.8 dts-critic: 3.3.11(typescript@5.5.4) fs-extra: 6.0.1 @@ -23920,7 +23921,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -24095,7 +24096,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -24105,7 +24106,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 18.19.45 + '@types/node': 18.19.74 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -24144,7 +24145,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -24168,7 +24169,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.8 + resolve: 1.22.10 resolve.exports: 2.0.2 slash: 3.0.0 @@ -24179,7 +24180,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -24207,7 +24208,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 @@ -24253,7 +24254,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -24272,7 +24273,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.45 + '@types/node': 18.19.74 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -24286,7 +24287,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 18.19.45 + '@types/node': 18.19.74 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -26072,7 +26073,7 @@ snapshots: normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.15.1 + is-core-module: 2.16.1 semver: 7.5.4 validate-npm-package-license: 3.0.4 @@ -26771,7 +26772,7 @@ snapshots: proxy-agent@6.4.0: dependencies: agent-base: 7.1.1 - debug: 4.3.6 + debug: 4.4.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 lru-cache: 7.18.3 @@ -26943,7 +26944,7 @@ snapshots: '@types/doctrine': 0.0.9 '@types/resolve': 1.20.6 doctrine: 3.0.0 - resolve: 1.22.8 + resolve: 1.22.10 strip-indent: 4.0.0 transitivePeerDependencies: - supports-color @@ -27442,7 +27443,7 @@ snapshots: resolve@2.0.0-next.5: dependencies: - is-core-module: 2.15.1 + is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -28095,7 +28096,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.6 + debug: 4.4.0 fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1 @@ -28839,7 +28840,7 @@ snapshots: '@types/node': 20.16.1 '@types/unist': 3.0.3 concat-stream: 2.0.0 - debug: 4.3.6 + debug: 4.4.0 extend: 3.0.2 glob: 10.4.5 ignore: 5.3.2 From d75c2dc4367828d5a37250b7a26ec8db6d5d7abe Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:12:53 +0200 Subject: [PATCH 22/22] Update packages/builders/src/components/Assertions.ts Co-authored-by: Denis-Adrian Cristea --- packages/builders/src/components/Assertions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index df8d8a4099f4..7165a5dd6850 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -7,7 +7,7 @@ export const idValidator = s .number() .safeInt() .greaterThanOrEqual(1) - .lessThan(4_294_967_296) + .lessThan(4_294_967_296) // 2^32 - 1 .setValidationEnabled(isValidationEnabled); export const customIdValidator = s