@lunibee/builders 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/embed.ts CHANGED
@@ -1,181 +1,193 @@
1
1
  /** Discord API embed field payload. */
2
2
  export interface EmbedField {
3
- /** Field name. */ name: string;
4
- /** Field value. */ value: string;
5
- /** Whether the field is displayed inline. */ inline?: boolean;
3
+ /** Field name. */ name: string;
4
+ /** Field value. */ value: string;
5
+ /** Whether the field is displayed inline. */ inline?: boolean;
6
6
  }
7
7
  /** Discord API embed footer payload. */
8
8
  export interface EmbedFooter {
9
- /** Footer text. */ text: string;
10
- /** Footer icon URL. */ icon_url?: string;
9
+ /** Footer text. */ text: string;
10
+ /** Footer icon URL. */ icon_url?: string;
11
11
  }
12
12
  /** Discord API embed author payload. */
13
13
  export interface EmbedAuthor {
14
- /** Author display name. */ name: string;
15
- /** Author URL. */ url?: string;
16
- /** Author icon URL. */ icon_url?: string;
14
+ /** Author display name. */ name: string;
15
+ /** Author URL. */ url?: string;
16
+ /** Author icon URL. */ icon_url?: string;
17
17
  }
18
18
  /** Strict Discord API embed payload. */
19
19
  export interface APIEmbed {
20
- /** Embed title. */ title?: string;
21
- /** Embed description. */ description?: string;
22
- /** Embed URL. */ url?: string;
23
- /** Embed color. */ color?: number;
24
- /** ISO-8601 timestamp. */ timestamp?: string;
25
- /** Embed footer. */ footer?: EmbedFooter;
26
- /** Embed author. */ author?: EmbedAuthor;
27
- /** Thumbnail payload. */ thumbnail?: { /** Thumbnail URL. */ url: string };
28
- /** Image payload. */ image?: { /** Image URL. */ url: string };
29
- /** Embed fields. */ fields?: EmbedField[];
20
+ /** Embed title. */ title?: string;
21
+ /** Embed description. */ description?: string;
22
+ /** Embed URL. */ url?: string;
23
+ /** Embed color. */ color?: number;
24
+ /** ISO-8601 timestamp. */ timestamp?: string;
25
+ /** Embed footer. */ footer?: EmbedFooter;
26
+ /** Embed author. */ author?: EmbedAuthor;
27
+ /** Thumbnail payload. */ thumbnail?: { /** Thumbnail URL. */ url: string };
28
+ /** Image payload. */ image?: { /** Image URL. */ url: string };
29
+ /** Embed fields. */ fields?: EmbedField[];
30
30
  }
31
31
 
32
32
  /** Builds compile-time-safe rich embed payloads for Discord API requests. */
33
33
  export class EmbedBuilder {
34
- readonly #data: APIEmbed = {};
35
- /** Sets the embed title. @param value Title text. @returns This builder. @throws {RangeError} If title exceeds Discord's limit. */ public setTitle(
36
- value: string,
37
- ): this {
38
- this.#data.title = validate(value, 256, "Embed title");
39
- return this;
40
- }
41
- /** Sets the embed description. @param value Description text. @returns This builder. @throws {RangeError} If description exceeds Discord's limit. */ public setDescription(
42
- value: string,
43
- ): this {
44
- this.#data.description = validate(value, 4096, "Embed description");
45
- return this;
46
- }
47
- /** Sets the embed URL. @param value Absolute URL. @returns This builder. @throws {TypeError} If URL is invalid. */ public setURL(
48
- value: string,
49
- ): this {
50
- this.#data.url = url(value, "Embed URL");
51
- return this;
52
- }
53
- /** Sets the embed color. @param value RGB integer. @returns This builder. @throws {RangeError} If color is outside 24-bit RGB range. */ public setColor(
54
- value: number,
55
- ): this {
56
- if (!Number.isInteger(value) || value < 0 || value > 0xffffff)
57
- throw new RangeError(
58
- "Embed color must be an integer between 0 and 16777215.",
59
- );
60
- this.#data.color = value;
61
- return this;
62
- }
63
- /** Sets the embed timestamp. @param value Date value. @returns This builder. @throws {RangeError} If date is invalid. */ public setTimestamp(
64
- value = new Date(),
65
- ): this {
66
- const date = value instanceof Date ? value : new Date(value);
67
- if (Number.isNaN(date.getTime()))
68
- throw new RangeError("Embed timestamp must be a valid date.");
69
- this.#data.timestamp = date.toISOString();
70
- return this;
71
- }
72
- /** Sets the embed footer. @param value Footer payload. @returns This builder. @throws {RangeError} If footer text exceeds Discord's limit. */ public setFooter(
73
- value: EmbedFooter,
74
- ): this {
75
- this.#data.footer = {
76
- text: validate(value.text, 2048, "Footer text"),
77
- ...(value.icon_url
78
- ? { icon_url: url(value.icon_url, "Footer icon URL") }
79
- : {}),
80
- };
81
- return this;
82
- }
83
- /** Sets the embed author. @param value Author payload. @returns This builder. @throws {RangeError} If author name exceeds Discord's limit. */ public setAuthor(
84
- value: EmbedAuthor,
85
- ): this {
86
- this.#data.author = {
87
- name: validate(value.name, 256, "Author name"),
88
- ...(value.url ? { url: url(value.url, "Author URL") } : {}),
89
- ...(value.icon_url
90
- ? { icon_url: url(value.icon_url, "Author icon URL") }
91
- : {}),
92
- };
93
- return this;
94
- }
95
- /** Sets the thumbnail URL. @param value Thumbnail URL or payload. @returns This builder. @throws {TypeError} If URL is invalid. */ public setThumbnail(
96
- value: { url: string } | string,
97
- ): this {
98
- const valueURL = typeof value === "string" ? value : value.url;
99
- this.#data.thumbnail = { url: url(valueURL, "Thumbnail URL") };
100
- return this;
101
- }
102
- /** Sets the image URL. @param value Image URL or payload. @returns This builder. @throws {TypeError} If URL is invalid. */ public setImage(
103
- value: { url: string } | string,
104
- ): this {
105
- const valueURL = typeof value === "string" ? value : value.url;
106
- this.#data.image = { url: url(valueURL, "Image URL") };
107
- return this;
108
- }
109
- /** Adds embed fields. @param fields Field payloads. @returns This builder. @throws {RangeError} If the 25-field limit is exceeded. */ public addFields(
110
- ...fields: EmbedField[]
111
- ): this {
112
- if (!fields.length)
113
- throw new TypeError("At least one embed field is required.");
114
- const current = this.#data.fields ?? [];
115
- if (current.length + fields.length > 25)
116
- throw new RangeError("An embed cannot contain more than 25 fields.");
117
- for (const field of fields) {
118
- validate(field.name, 256, "Embed field name");
119
- validate(field.value, 1024, "Embed field value");
120
- }
121
- this.#data.fields = [...current, ...fields.map((field) => ({ ...field }))];
122
- return this;
123
- }
124
- public setFields(fields: EmbedField[]): this {
125
- this.#data.fields = [];
126
- return this.addFields(...fields);
127
- }
128
- /** Replaces fields starting at an index. @param index Start index. @param deleteCount Number of fields to remove. @param fields Replacement fields. @returns This builder. @throws {RangeError} If arguments are invalid or the 25-field limit is exceeded. */ public spliceFields(
129
- index: number,
130
- deleteCount: number,
131
- ...fields: EmbedField[]
132
- ): this {
133
- const current = [...(this.#data.fields ?? [])];
134
- if (
135
- !Number.isInteger(index) ||
136
- !Number.isInteger(deleteCount) ||
137
- index < 0 ||
138
- deleteCount < 0
139
- )
140
- throw new RangeError(
141
- "Field index and delete count must be non-negative integers.",
142
- );
143
- current.splice(index, deleteCount, ...fields);
144
- if (current.length > 25)
145
- throw new RangeError("An embed cannot contain more than 25 fields.");
146
- this.#data.fields = current;
147
- return this;
148
- }
149
- /** Removes all embed fields. @returns This builder. */ public clearFields(): this {
150
- delete this.#data.fields;
151
- return this;
152
- }
153
- /** Removes the embed title. @returns This builder. */ public clearTitle(): this {
154
- delete this.#data.title;
155
- return this;
156
- }
157
- /** Removes the embed description. @returns This builder. */ public clearDescription(): this {
158
- delete this.#data.description;
159
- return this;
160
- }
161
- /** Returns a deep-cloned Discord API payload. @returns Strict embed payload. */ public get data(): APIEmbed {
162
- return this.toJSON();
163
- }
164
- public toJSON(): APIEmbed {
165
- return structuredClone(this.#data);
166
- }
34
+ readonly #data: APIEmbed = {};
35
+ /** Sets the embed title. @param value Title text. @returns This builder. @throws {RangeError} If title exceeds Discord's limit. */ public setTitle(
36
+ value: string,
37
+ ): this {
38
+ this.#data.title = validate(value, 256, "Embed title");
39
+ return this;
40
+ }
41
+ /** Sets the embed description. @param value Description text. @returns This builder. @throws {RangeError} If description exceeds Discord's limit. */ public setDescription(
42
+ value: string,
43
+ ): this {
44
+ this.#data.description = validate(value, 4096, "Embed description");
45
+ return this;
46
+ }
47
+ /** Sets the embed URL. @param value Absolute URL. @returns This builder. @throws {TypeError} If URL is invalid. */ public setURL(
48
+ value: string,
49
+ ): this {
50
+ this.#data.url = url(value, "Embed URL");
51
+ return this;
52
+ }
53
+ /** Sets the embed color. @param value RGB integer. @returns This builder. @throws {RangeError} If color is outside 24-bit RGB range. */ public setColor(
54
+ value: number,
55
+ ): this {
56
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffff)
57
+ throw new RangeError(
58
+ "Embed color must be an integer between 0 and 16777215.",
59
+ );
60
+ this.#data.color = value;
61
+ return this;
62
+ }
63
+ /** Sets the embed timestamp. @param value Date value. @returns This builder. @throws {RangeError} If date is invalid. */ public setTimestamp(
64
+ value = new Date(),
65
+ ): this {
66
+ const date = value instanceof Date ? value : new Date(value);
67
+ if (Number.isNaN(date.getTime()))
68
+ throw new RangeError("Embed timestamp must be a valid date.");
69
+ this.#data.timestamp = date.toISOString();
70
+ return this;
71
+ }
72
+ /** Sets the embed footer. @param value Footer payload. @returns This builder. @throws {RangeError} If footer text exceeds Discord's limit. */ public setFooter(
73
+ value: EmbedFooter,
74
+ ): this {
75
+ this.#data.footer = {
76
+ text: validate(value.text, 2048, "Footer text"),
77
+ ...(value.icon_url
78
+ ? { icon_url: url(value.icon_url, "Footer icon URL") }
79
+ : {}),
80
+ };
81
+ return this;
82
+ }
83
+ /** Sets the embed author. @param value Author payload. @returns This builder. @throws {RangeError} If author name exceeds Discord's limit. */ public setAuthor(
84
+ value: EmbedAuthor,
85
+ ): this {
86
+ this.#data.author = {
87
+ name: validate(value.name, 256, "Author name"),
88
+ ...(value.url ? { url: url(value.url, "Author URL") } : {}),
89
+ ...(value.icon_url
90
+ ? { icon_url: url(value.icon_url, "Author icon URL") }
91
+ : {}),
92
+ };
93
+ return this;
94
+ }
95
+ /** Sets the thumbnail URL. @param value Thumbnail URL or payload. @returns This builder. @throws {TypeError} If URL is invalid. */ public setThumbnail(
96
+ value: { url: string } | string,
97
+ ): this {
98
+ const valueURL = typeof value === "string" ? value : value.url;
99
+ this.#data.thumbnail = { url: url(valueURL, "Thumbnail URL") };
100
+ return this;
101
+ }
102
+ /** Sets the image URL. @param value Image URL or payload. @returns This builder. @throws {TypeError} If URL is invalid. */ public setImage(
103
+ value: { url: string } | string,
104
+ ): this {
105
+ const valueURL = typeof value === "string" ? value : value.url;
106
+ this.#data.image = { url: url(valueURL, "Image URL") };
107
+ return this;
108
+ }
109
+ /** Adds embed fields. Accepts both the spread form `addFields(a, b)` and the
110
+ * single-array form `addFields([a, b])` for discord.js `RestOrArray` parity.
111
+ * @param fields Field payloads, spread or as a single array. @returns This builder. @throws {RangeError} If the 25-field limit is exceeded. */ public addFields(
112
+ ...fields: EmbedField[] | [EmbedField[]]
113
+ ): this {
114
+ const resolved = (
115
+ fields.length === 1 && Array.isArray(fields[0]) ? fields[0] : fields
116
+ ) as EmbedField[];
117
+ if (!resolved.length)
118
+ throw new TypeError("At least one embed field is required.");
119
+ const current = this.#data.fields ?? [];
120
+ if (current.length + resolved.length > 25)
121
+ throw new RangeError(
122
+ "An embed cannot contain more than 25 fields.",
123
+ );
124
+ for (const field of resolved) {
125
+ validate(field.name, 256, "Embed field name");
126
+ validate(field.value, 1024, "Embed field value");
127
+ }
128
+ this.#data.fields = [
129
+ ...current,
130
+ ...resolved.map((field) => ({ ...field })),
131
+ ];
132
+ return this;
133
+ }
134
+ public setFields(fields: EmbedField[]): this {
135
+ this.#data.fields = [];
136
+ return this.addFields(...fields);
137
+ }
138
+ /** Replaces fields starting at an index. @param index Start index. @param deleteCount Number of fields to remove. @param fields Replacement fields. @returns This builder. @throws {RangeError} If arguments are invalid or the 25-field limit is exceeded. */ public spliceFields(
139
+ index: number,
140
+ deleteCount: number,
141
+ ...fields: EmbedField[]
142
+ ): this {
143
+ const current = [...(this.#data.fields ?? [])];
144
+ if (
145
+ !Number.isInteger(index) ||
146
+ !Number.isInteger(deleteCount) ||
147
+ index < 0 ||
148
+ deleteCount < 0
149
+ )
150
+ throw new RangeError(
151
+ "Field index and delete count must be non-negative integers.",
152
+ );
153
+ current.splice(index, deleteCount, ...fields);
154
+ if (current.length > 25)
155
+ throw new RangeError(
156
+ "An embed cannot contain more than 25 fields.",
157
+ );
158
+ this.#data.fields = current;
159
+ return this;
160
+ }
161
+ /** Removes all embed fields. @returns This builder. */ public clearFields(): this {
162
+ delete this.#data.fields;
163
+ return this;
164
+ }
165
+ /** Removes the embed title. @returns This builder. */ public clearTitle(): this {
166
+ delete this.#data.title;
167
+ return this;
168
+ }
169
+ /** Removes the embed description. @returns This builder. */ public clearDescription(): this {
170
+ delete this.#data.description;
171
+ return this;
172
+ }
173
+ /** Returns a deep-cloned Discord API payload. @returns Strict embed payload. */ public get data(): APIEmbed {
174
+ return this.toJSON();
175
+ }
176
+ public toJSON(): APIEmbed {
177
+ return structuredClone(this.#data);
178
+ }
167
179
  }
168
180
  /** Validates a bounded Discord string field. @param value Field value. @param max Maximum length. @param field Human-readable field name. @returns Validated value. @throws {RangeError} If length is invalid. */
169
181
  function validate(value: string, max: number, field: string): string {
170
- if (typeof value !== "string" || value.length === 0 || value.length > max)
171
- throw new RangeError(`${field} must contain 1-${max} characters.`);
172
- return value;
182
+ if (typeof value !== "string" || value.length === 0 || value.length > max)
183
+ throw new RangeError(`${field} must contain 1-${max} characters.`);
184
+ return value;
173
185
  }
174
186
  /** Validates and normalizes an absolute URL. @param value URL value. @param field Human-readable field name. @returns Normalized URL. @throws {TypeError} If URL is invalid. */
175
187
  function url(value: string, field: string): string {
176
- try {
177
- return new URL(value).toString();
178
- } catch (error) {
179
- throw new TypeError(`${field} must be a valid URL.`, { cause: error });
180
- }
188
+ try {
189
+ return new URL(value).toString();
190
+ } catch (error) {
191
+ throw new TypeError(`${field} must be a valid URL.`, { cause: error });
192
+ }
181
193
  }
package/src/index.ts CHANGED
@@ -1,37 +1,45 @@
1
- export { EmbedBuilder } from "./embed.js";
2
- export type { EmbedField } from "./embed.js";
3
- export {
4
- ButtonBuilder,
5
- ButtonStyle,
6
- ComponentType,
7
- ActionRowBuilder,
8
- StringSelectBuilder,
9
- EntitySelectBuilder,
10
- ModalBuilder,
11
- TextInputBuilder,
12
- TextInputStyle,
13
- ContainerBuilder,
14
- SectionBuilder,
15
- TextDisplayBuilder,
16
- MediaGalleryBuilder,
17
- FileComponentBuilder,
18
- SeparatorBuilder,
19
- ThumbnailBuilder,
20
- ContentInventoryEntryBuilder,
21
- } from "./components.js";
22
- export {
23
- SlashCommandBuilder,
24
- StringOptionBuilder,
25
- IntegerOptionBuilder,
26
- NumberOptionBuilder,
27
- BooleanOptionBuilder,
28
- UserOptionBuilder,
29
- ChannelOptionBuilder,
30
- RoleOptionBuilder,
31
- MentionableOptionBuilder,
32
- AttachmentOptionBuilder,
33
- SubcommandBuilder,
34
- SubcommandGroupBuilder,
35
- ApplicationCommandOptionType,
36
- } from "./commands.js";
37
- export { AttachmentBuilder, type AttachmentData } from "./attachment.js";
1
+ export { EmbedBuilder } from "./embed.js";
2
+ export type { EmbedField } from "./embed.js";
3
+ export {
4
+ ButtonBuilder,
5
+ ButtonStyle,
6
+ ComponentType,
7
+ ActionRowBuilder,
8
+ StringSelectBuilder,
9
+ StringSelectMenuBuilder,
10
+ EntitySelectBuilder,
11
+ UserSelectMenuBuilder,
12
+ RoleSelectMenuBuilder,
13
+ MentionableSelectMenuBuilder,
14
+ ChannelSelectMenuBuilder,
15
+ ModalBuilder,
16
+ TextInputBuilder,
17
+ TextInputStyle,
18
+ ContainerBuilder,
19
+ SectionBuilder,
20
+ TextDisplayBuilder,
21
+ MediaGalleryBuilder,
22
+ FileComponentBuilder,
23
+ SeparatorBuilder,
24
+ ThumbnailBuilder,
25
+ ContentInventoryEntryBuilder,
26
+ } from "./components.js";
27
+ export {
28
+ SlashCommandBuilder,
29
+ StringOptionBuilder,
30
+ IntegerOptionBuilder,
31
+ NumberOptionBuilder,
32
+ BooleanOptionBuilder,
33
+ UserOptionBuilder,
34
+ ChannelOptionBuilder,
35
+ RoleOptionBuilder,
36
+ MentionableOptionBuilder,
37
+ AttachmentOptionBuilder,
38
+ SubcommandBuilder,
39
+ SubcommandGroupBuilder,
40
+ ApplicationCommandOptionType,
41
+ UserCommandBuilder,
42
+ MessageCommandBuilder,
43
+ ContextMenuCommandBuilder,
44
+ } from "./commands.js";
45
+ export { AttachmentBuilder, type AttachmentData } from "./attachment.js";