Compare commits

..

41 Commits

Author SHA1 Message Date
Vlad Frangu
da8f08785f chore: tie the flow 2025-04-26 00:56:33 +03:00
Vlad Frangu
68282eeac4 chore: lock file 2025-04-26 00:54:44 +03:00
Vlad Frangu
cc5fbd8cc3 chore(ws): release @discordjs/ws@1.2.2 2025-04-26 00:53:36 +03:00
Vlad Frangu
25cc7282d0 chore(ws): release @discordjs/ws@1.2.1 2025-02-11 01:00:54 +02:00
Vlad Frangu
d8c10a7825 build: bump @discordjs/rest to 2.4.3 2025-02-11 00:59:53 +02:00
Jiralite
ea5471aa0a build: bump undici to 6.21.1 2025-02-08 15:40:17 +00:00
Jiralite
7a5e6ffc70 build: bump discord-api-types to 0.37.119 2025-02-08 15:32:36 +00:00
Jiralite
e1c4ee90b5 build: bump discord-api-types to 0.37.118 2025-01-29 09:40:48 +00:00
Qjuh
06c2b820c6 feat(website): include reexported members in docs (#10518)
* feat(website): add re-exported members to docs site

* refactor(scripts): rewrite sourceURL for externals

* feat(website): add external badge

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-10 04:05:10 +00:00
Vlad Frangu
f65ff060ae chore(ws): release @discordjs/ws@1.2.0 2025-01-02 00:02:20 +02:00
Vlad Frangu
835c4496ab chore(ws): release @discordjs/ws@1.2.0 2025-01-01 23:57:54 +02:00
didinele
a589c6d492 fix(SimpleIdentifyThrottler): don't sleep negative amounts 2025-01-01 23:55:47 +02:00
Jiralite
fdf0b8455c build: bump discord-api-types to 0.37.114 2025-01-01 23:55:47 +02:00
Jiralite
3f9c3dc497 chore: ignore unrelated code 2025-01-01 23:55:47 +02:00
Jiralite
437b2d459d build: bump discord-api-types to 0.37.113 2025-01-01 23:55:47 +02:00
DD
5b8a08ebb6 fix: retry for EAI_AGAIN I/O error (#10383) 2025-01-01 23:55:47 +02:00
Jiralite
22e013b3e9 refactor(GuildChannelManager): Remove redundant edit code (#10370)
refactor(GuildChannelManager): remove redundant edit code

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:47 +02:00
Almeida
e133aa9a1e feat(GuildAuditLogsEntry): onboarding events (#9726)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:46 +02:00
Almeida
c1b5242d2f test: complete collection coverage (#10380) 2025-01-01 23:55:46 +02:00
Jiralite
9779baea84 feat: Premium buttons (#10353)
* feat: premium buttons

* docs: deprecation string

* feat(InteractionResponses): add deprecation message

* feat(builders): add tests

* chore: remove @ts-expect-errors

* test: update method name

* refactor(formatters): stricter types

* docs: deprecate method in typings

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:46 +02:00
DD
a5437a41f3 feat(WebSocketShard): explicit time out network error handling (#10375)
* feat(WebSocketShard): explicit time out network error handling

* refactor: use constant
2025-01-01 23:55:43 +02:00
Jiralite
11dd1c0666 fix(GuildMemberManager): Fix data type check for add() method (#10338)
fix(GuildMemberManager): fix data type check

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:01 +02:00
TÆMBØ
95fae30606 feat: add user-installable apps support (#10348)
* feat(SlashCommandBuilder): `addContexts()` and `addIntegrationTypes()`

* Add methods to ContextMenuCommandbuilder

* Fix JSDoc

* Use `setX` over `addX`

* Fix tests

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2025-01-01 23:55:01 +02:00
Jiralite
93eeaeb56b types(ApplicationCommandManager): Snowflake fetch (#10366) 2025-01-01 23:55:00 +02:00
Jiralite
886a701251 chore: Remove "typings", "wip", and "workflow" scope (#10340)
* chore: remove "typings" commit lint

* chore: remove "workflow" too

* chore: also remove wip

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:00 +02:00
Jiralite
6dc0b7c18a build: Bump discord-api-types to 0.37.90 (#10354)
build: bump discord-api-types

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:00 +02:00
Adnan Khan
c4eba873ea ci: Reference title via environment variable (#10342)
Reference title via environment variable.

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:00 +02:00
DD
5c023bd64b fix: package gen script (#10352)
* fix: package gen script

* fix: files without extensions didn't have handlebars stripped

* chore: requested change
2025-01-01 23:55:00 +02:00
Qjuh
e7cc754fd3 fix(website): link tags to events named same as methods (#10351)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:55:00 +02:00
Qjuh
6e32ee565f fix(website): link tags with explicit URL showed undefined (#10350) 2025-01-01 23:54:59 +02:00
Jiralite
c6710e56cc fix: Consistent debug log spacing (#10349)
* fix: consistent debug log spacing

* refactor: simplify formatting

* refactor: more readable ternary

Co-Authored-By: Synbulat Biishev <contact@syjalo.dev>

* fix: modify parameters and types

---------

Co-authored-by: Synbulat Biishev <contact@syjalo.dev>
2025-01-01 23:54:59 +02:00
Qjuh
5761f2cbfd fix(website): remove merged interface from sitemap (#10343) 2025-01-01 23:54:22 +02:00
Jiralite
dd9ba1ad9d ci(pr-triage): Split job up (#10341)
ci: split job up
2025-01-01 23:54:22 +02:00
Danial Raza
0b0abfa283 feat(Message): add call (#10283)
* feat(Message): add `call`

* refactor: make `endedAt` a getter

* types: fix `endedAt` return type

* types(Message): add `call` property

* docs: requested changes

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2025-01-01 23:54:22 +02:00
Jiralite
419266d839 ci: Check pull request titles for the commit convention format (#10334)
ci: check pull request titles
2025-01-01 23:54:22 +02:00
Jiralite
5e7ba2a016 docs: Update rule trigger types (#9708)
docs: update rule trigger types
2025-01-01 23:54:22 +02:00
Jiralite
efd6b36227 fix: Correct base path for GIF stickers (#10330)
* fix: correct base path for GIF stickers

* test: add sticker GIF

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:54:21 +02:00
Amir Farzamnia
66774c1b53 docs(stageInstances): Correct reference for stage instance creation (#10333)
Update stageInstances.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-01 23:54:21 +02:00
Jiralite
728c2686bd fix: Update config file to address labeller file changes (#10332)
fix: update label script
2025-01-01 23:54:21 +02:00
ckohen
1bafba3fff ci: fix coverage upload (#10331) 2025-01-01 23:54:21 +02:00
Jiralite
a193147f96 build: Bump dependencies (#10322)
* build: bump dependencies

* build: update pnpm to 9.1.4
2025-01-01 23:54:21 +02:00
383 changed files with 21924 additions and 16211 deletions

View File

@@ -7,7 +7,6 @@
"always",
["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types"]
],
"scope-case": [0],
"subject-exclamation-mark": [0]
"scope-case": [0]
}
}

View File

@@ -2,7 +2,7 @@ version = 1
[merge]
require_automerge_label = false
blocking_labels = ['blocked', 'in review', 'semver:major']
blocking_labels = ['blocked', 'in review']
method = 'squash'
[merge.message]

View File

@@ -7,7 +7,7 @@
Messages must be matched by the following regex:
```js
/^(revert: )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|types)(\(.+\))?!?: .{1,72}/;
/^(revert: )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|types)(\(.+\))?: .{1,72}/;
```
#### Examples
@@ -55,7 +55,6 @@ A commit message consists of a **header**, **body** and **footer**. The header h
```
The **header** is mandatory and the **scope** of the header is optional.
If the commit contains **Breaking Changes**, a `!` can be added before the `:` as an indicator.
### Revert

View File

@@ -75,7 +75,7 @@ jobs:
- name: Apply tag to api-extractor config
if: ${{ env.REF_TYPE == 'tag' && !inputs.ref }}
run: sed -i 's!https://github.com/discordjs/discord.js/tree/main!https://github.com/discordjs/discord.js/tree/${{ github.ref_name }}!' "packages/${{ steps.extract-tag.outputs.package}}/api-extractor.json"
run: sed -i 's!https://github.com/discordjs/discord.js/tree/main!https://github.com/discordjs/discord.js/tree/${{ steps.extract-tag.outputs.semver }}!' "packages/${{ steps.extract-tag.outputs.package}}/api-extractor.json"
- name: Build docs
run: pnpm run docs

View File

@@ -26,7 +26,7 @@ jobs:
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
REGEX="^(revert: )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|types)(\\(.+\\))?!?: .{1,72}$"
REGEX="^(revert: )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|types)(\\(.+\\))?: .{1,72}$"
echo "Title: \"$TITLE\""

View File

@@ -72,7 +72,7 @@ jobs:
- name: Publish package
if: steps.release-check.outputs.release == '1'
run: |
pnpm --filter=${{ matrix.package }} run release --preid "dev.$(date +%s)-$(git rev-parse --short HEAD)" --skip-changelog
pnpm --filter=${{ matrix.package }} run release --preid "dev.$(date +%s)-$(git rev-parse --short HEAD)"
pnpm --filter=${{ matrix.package }} publish --provenance --no-git-checks --tag dev || true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@@ -35,7 +35,8 @@ jobs:
tag: ${{ github.ref_name }}
- name: Publish package
if: ${{ steps.extract-tag.outputs.package == 'ws' }}
run: |
pnpm --filter=${{ steps.extract-tag.outputs.subpackage == 'true' && '@discordjs/' || '' }}${{ steps.extract-tag.outputs.package }} publish --provenance --no-git-checks
pnpm --filter=${{ steps.extract-tag.outputs.subpackage == 'true' && '@discordjs/' || '' }}${{ steps.extract-tag.outputs.package }} publish --provenance --no-git-checks --tag v1-lts
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@@ -69,14 +69,14 @@
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.2",
"@types/html-escaper": "^3.0.2",
"@types/node": "^18.19.45",
"@types/node": "18.18.8",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@unocss/eslint-plugin": "^0.60.4",
"@unocss/postcss": "^0.60.4",
"@unocss/reset": "^0.60.4",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
@@ -86,12 +86,12 @@
"hastscript": "^8.0.0",
"html-escaper": "^3.0.3",
"postcss": "^8.4.38",
"prettier": "^3.3.3",
"turbo": "^2.0.14",
"typescript": "~5.5.4",
"prettier": "^3.3.0",
"turbo": "^1.13.3",
"typescript": "^5.4.5",
"unocss": "^0.60.4",
"vercel": "^37.0.0",
"vitest": "^2.0.5"
"vercel": "^34.2.4",
"vitest": "^1.6.0"
},
"engines": {
"node": ">=18"

View File

@@ -50,9 +50,10 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@react-icons/all-files": "^4.1.0",
"@vercel/analytics": "^1.3.1",
"@vercel/blob": "^0.23.3",
"@vercel/edge-config": "^1.1.1",
"@vercel/og": "^0.6.2",
"@vercel/postgres": "^0.9.0",
"@vercel/postgres": "^0.8.0",
"cmdk": "^1.0.0",
"geist": "^1.3.0",
"jotai": "^2.8.2",
@@ -75,11 +76,11 @@
"@tailwindcss/typography": "^0.5.13",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^18.19.45",
"@types/node": "18.18.8",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.19",
"babel-plugin-react-compiler": "0.0.0-experimental-592953e-20240517",
"cpy-cli": "^5.0.0",
@@ -89,16 +90,16 @@
"eslint-formatter-pretty": "^6.0.1",
"happy-dom": "^14.12.0",
"postcss": "^8.4.38",
"prettier": "^3.3.3",
"prettier": "^3.3.0",
"prettier-plugin-tailwindcss": "^0.5.14",
"remark-gfm": "^4.0.0",
"remark-rehype": "^11.1.0",
"shiki": "^1.6.2",
"tailwindcss": "^3.4.3",
"turbo": "^2.0.14",
"typescript": "~5.5.4",
"vercel": "^37.0.0",
"vitest": "^2.0.5"
"turbo": "^1.13.3",
"typescript": "^5.4.5",
"vercel": "^34.2.4",
"vitest": "^1.6.0"
},
"engines": {
"node": ">=18"

View File

@@ -18,8 +18,9 @@ export async function Badges({ node }: { readonly node: any }) {
const isAbstract = node.isAbstract;
const isReadonly = node.isReadonly;
const isOptional = node.isOptional;
const isExternal = node.isExternal;
const isAny = isDeprecated || isProtected || isStatic || isAbstract || isReadonly || isOptional;
const isAny = isDeprecated || isProtected || isStatic || isAbstract || isReadonly || isOptional || isExternal;
return isAny ? (
<div className="mb-1 flex gap-3">
@@ -33,6 +34,7 @@ export async function Badges({ node }: { readonly node: any }) {
{isAbstract ? <Badge className="bg-cyan-500/20 text-cyan-500">abstract</Badge> : null}
{isReadonly ? <Badge className="bg-purple-500/20 text-purple-500">readonly</Badge> : null}
{isOptional ? <Badge className="bg-cyan-500/20 text-cyan-500">optional</Badge> : null}
{isExternal ? <Badge className="bg-purple-500/20 text-purple-500">external</Badge> : null}
</div>
) : null;
}

View File

@@ -4,7 +4,7 @@ import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
export async function ExcerptNode({ node, version }: { readonly node?: any; readonly version: string }) {
const createExcerpt = (excerpts: any) => {
const excerpt = Array.isArray(excerpts) ? excerpts : (excerpts.excerpts ?? [excerpts]);
const excerpt = Array.isArray(excerpts) ? excerpts : excerpts.excerpts ?? [excerpts];
return (
<span

View File

@@ -29,7 +29,6 @@ export async function ParameterNode({
{description ? <Badges node={parameter} /> : null}
{parameter.name}
{parameter.isOptional ? '?' : ''}: <ExcerptNode node={parameter.typeExcerpt} version={version} />
{parameter.defaultValue ? ` = ${parameter.defaultValue}` : ''}
</span>
{description && parameter.description?.length ? (
<div className="mt-4 pl-4">

View File

@@ -51,13 +51,7 @@ export async function PropertyNode({
<LinkIcon aria-hidden size={16} />
</Link>
{property.displayName}
{property.isOptional ? '?' : ''} : <ExcerptNode node={property.typeExcerpt} version={version} />{' '}
{property.summary?.defaultValueBlock.length
? `= ${property.summary.defaultValueBlock.reduce(
(acc: string, def: { kind: string; text: string }) => `${acc}${def.text}`,
'',
)}`
: ''}
{property.isOptional ? '?' : ''} : <ExcerptNode node={property.typeExcerpt} version={version} />
</span>
</h3>

View File

@@ -20,7 +20,7 @@ export async function fetchDependencies({
return Object.entries<string>(parsedDependencies)
.filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor'))
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${sanitizeVersion(value)}`);
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`);
} catch {
return [];
}
@@ -36,12 +36,8 @@ export async function fetchDependencies({
return Object.entries<string>(parsedDependencies)
.filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor'))
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${sanitizeVersion(value)}`);
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`);
} catch {
return [];
}
}
function sanitizeVersion(version: string) {
return version.replaceAll('.', '-').replace(/^[\^~]/, '');
}

View File

@@ -28,7 +28,7 @@
"contributors": [
"Crawl <icrawltogo@gmail.com>",
"Amish Shah <amishshah.2k@gmail.com>",
"Vlad Frangu <me@vladfrangu.dev>",
"Vlad Frangu <kingdgrizzle@gmail.com>",
"SpaceEEC <spaceeec@yahoo.com>",
"Aura Román <kyradiscord@gmail.com>"
],
@@ -50,28 +50,28 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"devDependencies": {
"@commitlint/cli": "^19.4.0",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-angular": "^19.3.0",
"@favware/cliff-jumper": "^4.1.0",
"@favware/cliff-jumper": "^3.0.3",
"@favware/npm-deprecate": "^1.0.7",
"@types/lodash.merge": "^4.6.9",
"@unocss/eslint-plugin": "^0.59.4",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/coverage-v8": "^1.6.0",
"conventional-changelog-cli": "^4.1.0",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"husky": "^9.1.5",
"husky": "^9.0.11",
"is-ci": "^3.0.1",
"lint-staged": "^15.2.9",
"lint-staged": "^15.2.5",
"lodash.merge": "^4.6.2",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "~5.5.4",
"typescript-eslint": "^8.2.0",
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3",
"typescript": "^5.4.5",
"typescript-eslint": "^7.11.0",
"unocss": "^0.60.4",
"vercel": "^37.0.0",
"vitest": "^2.0.5"
"vercel": "^34.2.4",
"vitest": "^1.6.0"
},
"pnpm": {
"peerDependencyRules": {
@@ -97,5 +97,5 @@
"engines": {
"node": ">=18"
},
"packageManager": "pnpm@9.8.0"
"packageManager": "pnpm@9.1.4"
}

View File

@@ -42,27 +42,27 @@
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/glob": "^0.5.0",
"@actions/glob": "^0.4.0",
"@discordjs/scripts": "workspace:^",
"@vercel/blob": "^0.23.4",
"@vercel/postgres": "^0.9.0",
"@vercel/blob": "^0.22.3",
"@vercel/postgres": "^0.8.0",
"meilisearch": "^0.38.0",
"p-limit": "^6.1.0",
"tslib": "^2.6.3",
"undici": "6.19.8"
"p-limit": "^5.0.0",
"tslib": "^2.6.2",
"undici": "6.21.1"
},
"devDependencies": {
"@types/node": "^18.19.45",
"@vitest/coverage-v8": "^2.0.5",
"@types/node": "18.18.8",
"@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "~5.5.4",
"vitest": "^2.0.5"
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
},
"engines": {
"node": ">=18"

View File

@@ -4,7 +4,7 @@ export function formatTag(tag: string) {
if (parsed?.groups) {
const isSubpackage = typeof parsed.groups.package === 'string';
const pkg = isSubpackage ? parsed.groups.package : (parsedPackage?.groups?.package ?? 'discord.js');
const pkg = isSubpackage ? parsed.groups.package : parsedPackage?.groups?.package ?? 'discord.js';
const semver = parsed.groups.semver;
return {

View File

@@ -8,7 +8,6 @@ runs:
using: 'composite'
steps:
- name: Upload Guide Coverage
if: ${{ hashFiles('apps/guide/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./apps/guide/coverage/cobertura-coverage.xml
@@ -17,7 +16,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Website Coverage
if: ${{ hashFiles('apps/website/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./apps/website/coverage/cobertura-coverage.xml
@@ -26,7 +24,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Brokers Coverage
if: ${{ hashFiles('packages/brokers/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/brokers/coverage/cobertura-coverage.xml
@@ -35,7 +32,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Builders Coverage
if: ${{ hashFiles('packages/builders/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/builders/coverage/cobertura-coverage.xml
@@ -44,7 +40,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Collection Coverage
if: ${{ hashFiles('packages/collection/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/collection/coverage/cobertura-coverage.xml
@@ -53,7 +48,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Discord.js Coverage
if: ${{ hashFiles('packages/discord.js/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/discord.js/coverage/cobertura-coverage.xml
@@ -62,7 +56,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Formatters Coverage
if: ${{ hashFiles('packages/formatters/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/formatters/coverage/cobertura-coverage.xml
@@ -71,7 +64,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Next Coverage
if: ${{ hashFiles('packages/next/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/next/coverage/cobertura-coverage.xml
@@ -80,7 +72,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Proxy Coverage
if: ${{ hashFiles('packages/proxy/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/proxy/coverage/cobertura-coverage.xml
@@ -89,7 +80,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Rest Coverage
if: ${{ hashFiles('packages/rest/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/rest/coverage/cobertura-coverage.xml
@@ -98,7 +88,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Voice Coverage
if: ${{ hashFiles('packages/voice/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/voice/coverage/cobertura-coverage.xml
@@ -107,7 +96,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload WS Coverage
if: ${{ hashFiles('packages/ws/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/ws/coverage/cobertura-coverage.xml
@@ -116,7 +104,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Util Coverage
if: ${{ hashFiles('packages/util/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/util/coverage/cobertura-coverage.xml
@@ -125,7 +112,6 @@ runs:
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Utilities Coverage
if: ${{ hashFiles('packages/actions/coverage/cobertura-coverage.xml') != '' || hashFiles('packages/scripts/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/actions/coverage/cobertura-coverage.xml, ./packages/scripts/coverage/cobertura-coverage.xml

View File

@@ -53,6 +53,7 @@ try {
console.log('Uploading indices...');
try {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
promises = indices.map(async (index) =>
limit(async () => {
console.log(`Uploading ${index.index}...`);

View File

@@ -37,15 +37,14 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^18.19.45",
"@types/node": "^18.19.33",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "~5.5.4"
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3"
}
}

View File

@@ -2,15 +2,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import {
type DocNode,
type DocPlainText,
DocDeclarationReference,
DocNodeKind,
TSDocConfiguration,
DocMemberReference,
DocMemberIdentifier,
} from '@microsoft/tsdoc';
import { TSDocConfiguration } from '@microsoft/tsdoc';
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { InternalError } from '@rushstack/node-core-library';
import type { IExcerptToken, IExcerptTokenRange } from '../index.js';
@@ -50,11 +42,6 @@ export interface IApiItemContainerJson extends IApiItemJson {
preserveMemberOrder?: boolean;
}
interface Mixin {
declarationReference: DocDeclarationReference;
typeParameters: IExcerptTokenRange[];
}
interface ExcerptTokenRangeInDeclaredItem {
item: ApiDeclaredItem;
range: IExcerptTokenRange;
@@ -406,52 +393,14 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
}
}
const findPlainTextNode = (node: DocNode): DocPlainText | undefined => {
switch (node.kind) {
case DocNodeKind.PlainText:
return node as DocPlainText;
default:
for (const child of node.getChildNodes()) {
const result = findPlainTextNode(child);
if (result) return result;
}
}
return undefined;
};
// Interfaces can extend multiple interfaces, so iterate through all of them.
// Also Classes can have multiple mixins
const extendedItems: IMappedTypeParameters[] = [];
let extendsTypes: readonly (HeritageType | Mixin)[] | undefined;
let extendsTypes: readonly HeritageType[] | undefined;
switch (next.item.kind) {
case ApiItemKind.Class: {
const apiClass: ApiClass = next.item as ApiClass;
const configuration = apiClass.tsdocComment?.configuration ?? new TSDocConfiguration();
const mixins =
apiClass.tsdocComment?.customBlocks
.filter(
(block) => block.blockTag.tagName === '@mixes', // &&
// block.getChildNodes().some((node) => node.kind === DocNodeKind.PlainText),
)
.map(findPlainTextNode)
.filter((block) => block !== undefined)
.map((block) => ({
declarationReference: new DocDeclarationReference({
configuration,
memberReferences: block.text.split('.').map(
(part, index) =>
new DocMemberReference({
configuration,
hasDot: index > 0,
memberIdentifier: new DocMemberIdentifier({ configuration, identifier: part }),
}),
),
}),
typeParameters: [] as IExcerptTokenRange[],
})) ?? [];
extendsTypes = apiClass.extendsType ? [apiClass.extendsType, ...mixins] : [...mixins];
extendsTypes = apiClass.extendsType ? [apiClass.extendsType] : [];
break;
}
@@ -476,38 +425,30 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
}
for (const extendsType of extendsTypes) {
let canonicalReference: DeclarationReference | DocDeclarationReference;
if ('excerpt' in extendsType) {
// We want to find the reference token associated with the actual inherited declaration.
// In every case we support, this is the first reference token. For example:
//
// ```
// export class A extends B {}
// ^
// export class A extends B<C> {}
// ^
// export class A extends B.C {}
// ^^^
// ```
const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find(
(token: ExcerptToken) => {
return token.kind === ExcerptTokenKind.Reference && token.canonicalReference;
},
);
// We want to find the reference token associated with the actual inherited declaration.
// In every case we support, this is the first reference token. For example:
//
// ```
// export class A extends B {}
// ^
// export class A extends B<C> {}
// ^
// export class A extends B.C {}
// ^^^
// ```
const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find(
(token: ExcerptToken) => {
return token.kind === ExcerptTokenKind.Reference && token.canonicalReference;
},
);
if (!firstReferenceToken) {
messages.push({
messageId: FindApiItemsMessageId.ExtendsClauseMissingReference,
text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`,
});
maybeIncompleteResult = true;
continue;
}
canonicalReference = firstReferenceToken.canonicalReference!;
} else {
// extendsType is a Mixin
canonicalReference = extendsType.declarationReference;
if (!firstReferenceToken) {
messages.push({
messageId: FindApiItemsMessageId.ExtendsClauseMissingReference,
text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`,
});
maybeIncompleteResult = true;
continue;
}
const apiModel: ApiModel | undefined = this.getAssociatedModel();
@@ -520,9 +461,10 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
continue;
}
const canonicalReference: DeclarationReference = firstReferenceToken.canonicalReference!;
const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference(
canonicalReference,
this,
undefined,
);
const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem;

View File

@@ -14,7 +14,6 @@ import type { IExcerptTokenRange } from './Excerpt.js';
* @public
*/
export interface IApiParameterOptions {
defaultValue: string | undefined;
isOptional: boolean;
isRest: boolean;
parameterName: string;
@@ -125,7 +124,6 @@ export function ApiParameterListMixin<TBaseClass extends IApiItemConstructor>(
isOptional: Boolean(parameterOptions.isOptional),
isRest: Boolean(parameterOptions.isRest),
parent: this,
defaultValue: parameterOptions.defaultValue,
});
this[_parameters].push(parameter);
@@ -173,7 +171,6 @@ export function ApiParameterListMixin<TBaseClass extends IApiItemConstructor>(
parameterTypeTokenRange: parameter.parameterTypeExcerpt.tokenRange,
isOptional: parameter.isOptional,
isRest: parameter.isRest,
defaultValue: parameter.defaultValue,
});
}

View File

@@ -41,7 +41,6 @@ const MinifyJSONMapping = {
constraintTokenRange: 'ctr',
dependencies: 'dp',
defaultTypeTokenRange: 'dtr',
defaultValue: 'dv',
docComment: 'd',
endIndex: 'en',
excerptTokens: 'ex',
@@ -294,11 +293,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();
if (
versionToDeserialize >= ApiJsonSchemaVersion.V_1004 &&
'tsdocConfig' in jsonObject.metadata &&
'$schema' in jsonObject.metadata.tsdocConfig
) {
if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfiguration' in jsonObject) {
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig);
if (tsdocConfigFile.hasErrors) {
throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary());
@@ -385,7 +380,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
toolPackage: ioptions.toolPackage ?? packageJson.name,
// In test mode, we don't write the real version, since that would cause spurious diffs whenever
// the version is bumped. Instead we write a placeholder string.
toolVersion: ioptions.testMode ? '[test mode]' : (ioptions.toolVersion ?? packageJson.version),
toolVersion: ioptions.testMode ? '[test mode]' : ioptions.toolVersion ?? packageJson.version,
schemaVersion: ApiJsonSchemaVersion.LATEST,
oldestForwardsCompatibleVersion: ApiJsonSchemaVersion.OLDEST_FORWARDS_COMPATIBLE,
tsdocConfig,
@@ -425,8 +420,6 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
for (const key of Object.keys(item)) {
if (key === 'dependencies') {
result[MinifyJSONMapping.dependencies] = item.dependencies;
} else if (key === 'tsdocConfig') {
result[MinifyJSONMapping.tsdocConfig] = item.tsdocConfig;
} else
result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] =
typeof item[key] === 'object' ? mapper(item[key]) : item[key];
@@ -447,8 +440,6 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
for (const key of Object.keys(item)) {
if (key === MinifyJSONMapping.dependencies) {
result.dependencies = item[MinifyJSONMapping.dependencies];
} else if (key === MinifyJSONMapping.tsdocConfig) {
result.tsdocConfig = item[MinifyJSONMapping.tsdocConfig];
} else
result[
Object.keys(MinifyJSONMapping).find(

View File

@@ -182,9 +182,7 @@ export interface DocgenJson {
}
function formatVarType(type: DocgenVarTypeJson): string {
return (Array.isArray(type) ? type : (type.types ?? []))
.map((t1) => t1.map((t2) => t2.join('')).join(''))
.join(' | ');
return (Array.isArray(type) ? type : type.types ?? []).map((t1) => t1.map((t2) => t2.join('')).join('')).join(' | ');
}
function getFirstType(type: DocgenVarTypeJson): string {
@@ -194,7 +192,7 @@ function getFirstType(type: DocgenVarTypeJson): string {
// function mapEvent(_event: DocgenEventJson, _package: string, _parent: DocgenClassJson): void {}
function mapVarType(type: DocgenVarTypeJson, _package: string): IExcerptToken[] {
const mapper = Array.isArray(type) ? type : (type.types ?? []);
const mapper = Array.isArray(type) ? type : type.types ?? [];
return mapper.flatMap((typ) =>
typ.reduce<IExcerptToken[]>(
(arr, [_class, symbol]) => [
@@ -262,7 +260,6 @@ function mapParam(
startIndex: 1 + index + paramTokens.slice(0, index).reduce((akk, num) => akk + num, 0),
endIndex: 1 + index + paramTokens.slice(0, index + 1).reduce((akk, num) => akk + num, 0),
},
defaultValue: param.default,
};
}

View File

@@ -12,7 +12,6 @@ import type { Excerpt } from '../mixins/Excerpt.js';
* @public
*/
export interface IParameterOptions {
defaultValue: string | undefined;
isOptional: boolean;
isRest: boolean;
name: string;
@@ -57,11 +56,6 @@ export class Parameter {
*/
public isRest: boolean;
/**
* The default value for this parameter if optional
*/
public defaultValue: string | undefined;
private readonly _parent: ApiParameterListMixin;
public constructor(options: IParameterOptions) {
@@ -70,7 +64,6 @@ export class Parameter {
this.isOptional = options.isOptional;
this.isRest = options.isRest;
this._parent = options.parent;
this.defaultValue = options.defaultValue;
}
/**

View File

@@ -50,15 +50,15 @@
"@microsoft/tsdoc": "0.14.2"
},
"devDependencies": {
"@types/node": "^18.19.45",
"@types/node": "18.18.8",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "~5.5.4"
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3",
"typescript": "^5.4.5"
},
"engines": {
"node": ">=18"

View File

@@ -199,7 +199,7 @@ export function genToken(model: ApiModel, token: ExcerptToken, version: string)
}
const item = token.canonicalReference
? (model.resolveDeclarationReference(token.canonicalReference, undefined).resolvedApiItem ?? null)
? model.resolveDeclarationReference(token.canonicalReference, undefined).resolvedApiItem ?? null
: null;
return {

View File

@@ -32,10 +32,6 @@
{
"tagName": "@preapproved",
"syntaxKind": "modifier"
},
{
"tagName": "@mixes",
"syntaxKind": "block"
}
],

View File

@@ -61,12 +61,12 @@
"resolve": "~1.22.1",
"semver": "~7.5.4",
"source-map": "0.6.1",
"typescript": "~5.5.4"
"typescript": "^5.4.5"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.4",
"@types/node": "^18.19.45",
"@types/node": "^18.19.33",
"@types/resolve": "^1.20.6",
"@types/semver": "^7.5.8",
"cpy-cli": "^5.0.0",
@@ -75,8 +75,8 @@
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14"
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3"
}
}

View File

@@ -114,7 +114,7 @@ interface DocgenEventJson {
}
interface DocgenParamJson {
default?: boolean | number | string;
default?: string;
description: string;
name: string;
nullable?: boolean;
@@ -155,7 +155,7 @@ interface DocgenMethodJson {
interface DocgenPropertyJson {
abstract?: boolean;
access?: DocgenAccess;
default?: boolean | number | string;
default?: string;
deprecated?: DocgenDeprecated;
description: string;
meta: DocgenMetaJson;
@@ -843,7 +843,6 @@ export class ApiModelGenerator {
const parameters: IApiParameterOptions[] = this._captureParameters(
nodesToCapture,
functionDeclaration.parameters,
jsDoc?.params,
);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
@@ -1044,7 +1043,6 @@ export class ApiModelGenerator {
const parameters: IApiParameterOptions[] = this._captureParameters(
nodesToCapture,
methodDeclaration.parameters,
jsDoc?.params,
);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
@@ -1139,11 +1137,7 @@ export class ApiModelGenerator {
methodSignature.typeParameters,
);
const parameters: IApiParameterOptions[] = this._captureParameters(
nodesToCapture,
methodSignature.parameters,
jsDoc?.params,
);
const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, methodSignature.parameters);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
@@ -1270,7 +1264,7 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._tsDocParser.parseString(
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}${jsDoc.default ? ` (default: ${this._escapeSpecialChars(jsDoc.default)})` : ''}\n${
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}\n${
'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''
}${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${
'deprecated' in jsDoc && jsDoc.deprecated
@@ -1348,7 +1342,7 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._tsDocParser.parseString(
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}${jsDoc.default ? `\n * @defaultValue ${this._escapeSpecialChars(jsDoc.default)}` : ''}\n${
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}\n${
'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''
}${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${
'deprecated' in jsDoc && jsDoc.deprecated
@@ -1429,9 +1423,7 @@ export class ApiModelGenerator {
}${
'returns' in jsDoc
? jsDoc.returns
.map(
(ret) => ` * @returns ${Array.isArray(ret) ? '' : (this._fixLinkTags(ret.description) ?? '')}\n`,
)
.map((ret) => ` * @returns ${Array.isArray(ret) ? '' : this._fixLinkTags(ret.description) ?? ''}\n`)
.join('')
: ''
} */`,
@@ -1516,17 +1508,15 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = [
{
kind: ExcerptTokenKind.Content,
text: `public on(eventName: '${name}', listener: (${
jsDoc.params?.length
? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.optional ? '?' : ''}: `
: ') => void): this;'
text: `on('${name}', (${
jsDoc.params?.length ? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.nullable ? '?' : ''}: ` : ') => {})'
}`,
},
];
const parameters: IApiParameterOptions[] = [];
for (let index = 0; index < (jsDoc.params?.length ?? 0) - 1; index++) {
const parameter = jsDoc.params![index]!;
const newTokens = this._mapVarType(parameter.type, parameter.nullable);
const newTokens = this._mapVarType(parameter.type);
parameters.push({
parameterName: parameter.name,
parameterTypeTokenRange: {
@@ -1535,7 +1525,6 @@ export class ApiModelGenerator {
},
isOptional: Boolean(parameter.optional),
isRest: parameter.name.startsWith('...'),
defaultValue: parameter.default?.toString(),
});
excerptTokens.push(...newTokens);
excerptTokens.push({
@@ -1546,7 +1535,7 @@ export class ApiModelGenerator {
if (jsDoc.params?.length) {
const parameter = jsDoc.params![jsDoc.params.length - 1]!;
const newTokens = this._mapVarType(parameter.type, parameter.nullable);
const newTokens = this._mapVarType(parameter.type);
parameters.push({
parameterName: parameter.name,
parameterTypeTokenRange: {
@@ -1555,12 +1544,11 @@ export class ApiModelGenerator {
},
isOptional: Boolean(parameter.optional),
isRest: parameter.name.startsWith('...'),
defaultValue: parameter.default?.toString(),
});
excerptTokens.push(...newTokens);
excerptTokens.push({
kind: ExcerptTokenKind.Content,
text: `) => void): this;`,
text: `) => {})`,
});
}
@@ -1648,7 +1636,6 @@ export class ApiModelGenerator {
private _captureParameters(
nodesToCapture: IExcerptBuilderNodeToCapture[],
parameterNodes: ts.NodeArray<ts.ParameterDeclaration>,
jsDoc?: DocgenParamJson[] | undefined,
): IApiParameterOptions[] {
const parameters: IApiParameterOptions[] = [];
for (const parameter of parameterNodes) {
@@ -1659,9 +1646,6 @@ export class ApiModelGenerator {
parameterTypeTokenRange,
isOptional: this._collector.typeChecker.isOptionalParameter(parameter),
isRest: Boolean(parameter.dotDotDotToken),
defaultValue:
parameter.initializer?.getText() ??
jsDoc?.find((param) => param.name === parameter.name.getText().trim())?.default?.toString(),
});
}
@@ -1760,14 +1744,6 @@ export class ApiModelGenerator {
return sourceLocation;
}
private _escapeSpecialChars(input: boolean | number | string) {
if (typeof input !== 'string') {
return input;
}
return input.replaceAll(/(?<char>[@{}])/g, '\\$<char>');
}
private _fixLinkTags(input?: string): string | undefined {
return input
?.replaceAll(linkRegEx, (_match, _p1, _p2, _p3, _p4, _p5, _offset, _string, groups) => {
@@ -1787,8 +1763,8 @@ export class ApiModelGenerator {
.replaceAll('* ', '\n * * ');
}
private _mapVarType(typey: DocgenVarTypeJson, nullable?: boolean): IExcerptToken[] {
const mapper = Array.isArray(typey) ? typey : (typey.types ?? []);
private _mapVarType(typey: DocgenVarTypeJson): IExcerptToken[] {
const mapper = Array.isArray(typey) ? typey : typey.types ?? [];
const lookup: { [K in ts.SyntaxKind]?: string } = {
[ts.SyntaxKind.ClassDeclaration]: 'class',
[ts.SyntaxKind.EnumDeclaration]: 'enum',
@@ -1812,40 +1788,23 @@ export class ApiModelGenerator {
{
kind: type?.includes("'") ? ExcerptTokenKind.Content : ExcerptTokenKind.Reference,
text: fixPrimitiveTypes(type ?? 'unknown', symbol),
canonicalReference:
type?.includes("'") || !astEntity
? undefined
: DeclarationReference.package(pkg)
.addNavigationStep(
Navigation.Members as any,
DeclarationReference.parseComponent(type ?? 'unknown'),
)
.withMeaning(
(lookup[
astSymbol?.astDeclarations.at(-1)?.declaration.kind ?? ts.SyntaxKind.ClassDeclaration
] ?? 'class') as Meaning,
)
.toString(),
canonicalReference: type?.includes("'")
? undefined
: DeclarationReference.package(pkg)
.addNavigationStep(
Navigation.Members as any,
DeclarationReference.parseComponent(type ?? 'unknown'),
)
.withMeaning(
(lookup[astSymbol?.astDeclarations.at(-1)?.declaration.kind ?? ts.SyntaxKind.ClassDeclaration] ??
'class') as Meaning,
)
.toString(),
},
{ kind: ExcerptTokenKind.Content, text: symbol ?? '' },
];
}, []);
return index === 0
? mapper.length === 1 && (nullable || ('nullable' in typey && typey.nullable))
? [
...result,
{ kind: ExcerptTokenKind.Content, text: ' | ' },
{ kind: ExcerptTokenKind.Reference, text: 'null' },
]
: result
: index === mapper.length - 1 && (nullable || ('nullable' in typey && typey.nullable))
? [
{ kind: ExcerptTokenKind.Content, text: ' | ' },
...result,
{ kind: ExcerptTokenKind.Content, text: ' | ' },
{ kind: ExcerptTokenKind.Reference, text: 'null' },
]
: [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result];
return index === 0 ? result : [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result];
})
.filter((excerpt) => excerpt.text.length);
}
@@ -1860,7 +1819,7 @@ export class ApiModelGenerator {
isOptional: Boolean(prop.nullable),
isReadonly: Boolean(prop.readonly),
docComment: this._tsDocParser.parseString(
`/**\n * ${this._fixLinkTags(prop.description) ?? ''}\n${prop.default ? ` * @defaultValue ${this._escapeSpecialChars(prop.default)}\n` : ''}${
`/**\n * ${this._fixLinkTags(prop.description) ?? ''}\n${
prop.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''
}${prop.readonly ? ' * @readonly\n' : ''} */`,
).docComment,
@@ -1872,7 +1831,7 @@ export class ApiModelGenerator {
}${prop.name} :`,
},
...mappedVarType,
{ kind: ExcerptTokenKind.Content, text: `${prop.default ? ` = ${prop.default}` : ''};` },
{ kind: ExcerptTokenKind.Content, text: ';' },
],
propertyTypeTokenRange: { startIndex: 1, endIndex: 1 + mappedVarType.length },
releaseTag: prop.access === 'private' ? ReleaseTag.Internal : ReleaseTag.Public,
@@ -1895,7 +1854,6 @@ export class ApiModelGenerator {
startIndex: 1 + index + paramTokens.slice(0, index).reduce((akk, num) => akk + num, 0),
endIndex: 1 + index + paramTokens.slice(0, index + 1).reduce((akk, num) => akk + num, 0),
},
defaultValue: param.default?.toString(),
};
}
@@ -1909,7 +1867,7 @@ export class ApiModelGenerator {
: `${method.access ? `${method.access} ` : ''}${method.scope === 'static' ? 'static ' : ''}${method.name}(`
}${
method.params?.length
? `${method.params[0]!.name}${method.params[0]!.nullable || method.params[0]!.optional ? '?' : ''}: `
? `${method.params[0]!.name}${method.params[0]!.nullable || method.params[0]!.optional ? '?' : ''}`
: '): '
}`,
});
@@ -1920,7 +1878,7 @@ export class ApiModelGenerator {
excerptTokens.push(...newTokens);
excerptTokens.push({
kind: ExcerptTokenKind.Content,
text: `${method.params![index]!.default ? ` = ${method.params![index]!.default}` : ''}, ${method.params![index + 1]!.name}${
text: `, ${method.params![index + 1]!.name}${
method.params![index + 1]!.nullable || method.params![index + 1]!.optional ? '?' : ''
}: `,
});
@@ -1930,10 +1888,7 @@ export class ApiModelGenerator {
const newTokens = this._mapVarType(method.params[method.params.length - 1]!.type);
paramTokens.push(newTokens.length);
excerptTokens.push(...newTokens);
excerptTokens.push({
kind: ExcerptTokenKind.Content,
text: `${method.params![method.params.length - 1]!.default ? ` = ${method.params![method.params.length - 1]!.default}` : ''}): `,
});
excerptTokens.push({ kind: ExcerptTokenKind.Content, text: `): ` });
}
const returnTokens = this._mapVarType(method.returns?.[0] ?? []);

View File

@@ -2,14 +2,6 @@
All notable changes to this project will be documented in this file.
# [@discordjs/brokers@1.0.0](https://github.com/discordjs/discord.js/compare/@discordjs/brokers@0.3.0...@discordjs/brokers@1.0.0) - (2024-09-01)
## Refactor
- **brokers:** Re-design API to make groups a constructor option (#10297) ([38a37b5](https://github.com/discordjs/discord.js/commit/38a37b5caf06913131c6dc2dc5cc258aecfe2266))
- **brokers:** Make option props more correct (#10242) ([393ded4](https://github.com/discordjs/discord.js/commit/393ded4ea14e73b2bb42226f57896130329f88ca))
- **BREAKING CHANGE:** Classes now take redis client as standalone parameter, various props from the base option interface moved to redis options
# [@discordjs/brokers@0.3.0](https://github.com/discordjs/discord.js/compare/@discordjs/brokers@0.2.3...@discordjs/brokers@0.3.0) - (2024-05-04)
## Bug Fixes

View File

@@ -23,7 +23,7 @@
## Installation
**Node.js 20 or newer is required.**
**Node.js 18 or newer is required.**
```sh
npm install @discordjs/brokers

View File

@@ -5,16 +5,13 @@ header = """
All notable changes to this project will be documented in this file.\n
"""
body = """
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
{% if version %}\
# [{{ version | trim_start_matches(pat="v") }}]\
{% if previous %}\
{% if previous.version %}\
({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
(https://github.com/discordjs/discord.js/compare/{{ previous.version }}...{{ version }})\
{% else %}\
({{ self::remote_url() }}/tree/{{ version }})\
(https://github.com/discordjs/discord.js/tree/{{ version }})\
{% endif %}\
{% endif %} \
- ({{ timestamp | date(format="%Y-%m-%d") }})
@@ -27,23 +24,14 @@ body = """
- {% if commit.scope %}\
**{{commit.scope}}:** \
{% endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/discordjs/discord.js/commit/{{ commit.id }}))\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}
{% endfor %}\
{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
\n### New Contributors\n
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
* @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
{% endfor %}\
{% endif %}\n
{% endfor %}\n
"""
trim = true
footer = ""
@@ -71,9 +59,5 @@ commit_parsers = [
filter_commits = true
tag_pattern = "@discordjs/brokers@[0-9]*"
ignore_tags = ""
topo_order = false
topo_order = true
sort_commits = "newest"
[remote.github]
owner = "discordjs"
repo = "discord.js"

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@discordjs/brokers",
"version": "1.0.0",
"version": "0.3.0",
"description": "Powerful set of message brokers",
"scripts": {
"test": "vitest run",
@@ -42,7 +42,7 @@
"Crawl <icrawltogo@gmail.com>",
"Amish Shah <amishshah.2k@gmail.com>",
"SpaceEEC <spaceeec@yahoo.com>",
"Vlad Frangu <me@vladfrangu.dev>",
"Vlad Frangu <kingdgrizzle@gmail.com>",
"Aura Roman <kyradiscord@gmail.com>",
"DD <didinele.dev@gmail.com>"
],
@@ -68,28 +68,28 @@
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2",
"@vladfrangu/async_event_emitter": "^2.4.6",
"@vladfrangu/async_event_emitter": "^2.2.4",
"ioredis": "^5.4.1"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
"@favware/cliff-jumper": "^4.1.0",
"@types/node": "^18.19.45",
"@vitest/coverage-v8": "^2.0.5",
"@favware/cliff-jumper": "^3.0.3",
"@types/node": "18.18.8",
"@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"esbuild-plugin-version-injector": "^1.2.1",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "~5.5.4",
"vitest": "^2.0.5"
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
},
"engines": {
"node": ">=20"
"node": ">=18"
},
"publishConfig": {
"access": "public",

View File

@@ -30,17 +30,17 @@ export const DefaultBrokerOptions = {
} as const satisfies Required<BaseBrokerOptions>;
export type ToEventMap<
TRecord extends Record<string, any[]>,
TRecord extends Record<string, any>,
TResponses extends Record<keyof TRecord, any> | undefined = undefined,
> = {
[TKey in keyof TRecord]: [
event: TResponses extends Record<keyof TRecord, any>
? { ack(): Promise<void>; reply(data: TResponses[TKey]): Promise<void> }
: { ack(): Promise<void>; data: TRecord[TKey] },
: { ack(): Promise<void> } & { data: TRecord[TKey] },
];
};
} & { [K: string]: any };
export interface IBaseBroker<TEvents extends {}> {
export interface IBaseBroker<TEvents extends Record<string, any>> {
/**
* Subscribes to the given events
*/
@@ -51,7 +51,7 @@ export interface IBaseBroker<TEvents extends {}> {
unsubscribe(events: (keyof TEvents)[]): Promise<void>;
}
export interface IPubSubBroker<TEvents extends {}>
export interface IPubSubBroker<TEvents extends Record<string, any>>
extends IBaseBroker<TEvents>,
AsyncEventEmitter<ToEventMap<TEvents>> {
/**
@@ -60,7 +60,7 @@ export interface IPubSubBroker<TEvents extends {}>
publish<Event extends keyof TEvents>(event: Event, data: TEvents[Event]): Promise<void>;
}
export interface IRPCBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>
export interface IRPCBroker<TEvents extends Record<string, any>, TResponses extends Record<keyof TEvents, any>>
extends IBaseBroker<TEvents>,
AsyncEventEmitter<ToEventMap<TEvents, TResponses>> {
/**

View File

@@ -57,11 +57,8 @@ export const DefaultRedisBrokerOptions = {
/**
* Helper class with shared Redis logic
*/
export abstract class BaseRedisBroker<
TEvents extends Record<string, any[]>,
TResponses extends Record<keyof TEvents, any> | undefined = undefined,
>
extends AsyncEventEmitter<ToEventMap<TEvents, TResponses>>
export abstract class BaseRedisBroker<TEvents extends Record<string, any>>
extends AsyncEventEmitter<ToEventMap<TEvents>>
implements IBaseBroker<TEvents>
{
/**
@@ -185,7 +182,6 @@ export abstract class BaseRedisBroker<
}
}
} catch (error) {
// @ts-expect-error: Intended
this.emit('error', error);
break;
}

View File

@@ -48,7 +48,6 @@ export class PubSubRedisBroker<TEvents extends Record<string, any>>
},
};
// @ts-expect-error: Intended
this.emit(event, payload);
}
}

View File

@@ -54,8 +54,8 @@ export const DefaultRPCRedisBrokerOptions = {
* await broker.subscribe('responders', ['testcall']);
* ```
*/
export class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses extends Record<keyof TEvents, any>>
extends BaseRedisBroker<TEvents, TResponses>
export class RPCRedisBroker<TEvents extends Record<string, any>, TResponses extends Record<keyof TEvents, any>>
extends BaseRedisBroker<TEvents>
implements IRPCBroker<TEvents, TResponses>
{
/**
@@ -125,7 +125,6 @@ export class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses ex
},
};
// @ts-expect-error: Intended
this.emit(event, payload);
}
}

View File

@@ -2,16 +2,6 @@
All notable changes to this project will be documented in this file.
# [@discordjs/builders@1.9.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.8.2...@discordjs/builders@1.9.0) - (2024-09-01)
## Features
- User-installable apps (#10227) ([fc0b6f7](https://github.com/discordjs/discord.js/commit/fc0b6f7f8ebd94a4a05fac0c76e49b23752a8e65))
- **builders:** Update to @sapphire/shapeshift v4 (#10291) ([2d5531f](https://github.com/discordjs/discord.js/commit/2d5531f35c6b4d70f83e46b99c284030108dcf5c))
- **SlashCommandBuilder:** Add explicit command type when building (#10395) ([b2970bb](https://github.com/discordjs/discord.js/commit/b2970bb2dddf70d2d918fda825059315f35d23f3))
- Premium buttons (#10353) ([4f59b74](https://github.com/discordjs/discord.js/commit/4f59b740d01b9ff2213949708a36e17da32b89c3))
- Add user-installable apps support (#10348) ([9c76bbe](https://github.com/discordjs/discord.js/commit/9c76bbea172d49320f7fdac19ec1a43a49d05116))
# [@discordjs/builders@1.8.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.8.1...@discordjs/builders@1.8.2) - (2024-06-02)
## Bug Fixes
@@ -280,6 +270,261 @@ All notable changes to this project will be documented in this file.
- Cleanup tests and tsup configs ([6b8ef20](https://github.com/discordjs/discord.js/commit/6b8ef20cb3af5b5cfd176dd0aa0a1a1e98551629))
# [@discordjs/builders@1.6.5](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.4...@discordjs/builders@1.6.5) - (2023-08-17)
## Documentation
- Update Node.js requirement to 16.11.0 (#9764) ([188877c](https://github.com/discordjs/discord.js/commit/188877c50af70f0d5cffb246620fa277435c6ce6))
# [@discordjs/builders@1.6.3](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.2...@discordjs/builders@1.6.3) - (2023-05-01)
## Refactor
- Remove `@discordjs/util` re-export (#9488) ([54ceedf](https://github.com/discordjs/discord.js/commit/54ceedf6c535d4641643d4106b6286cbef09de4a))
# [@discordjs/builders@1.6.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.1...@discordjs/builders@1.6.2) - (2023-05-01)
## Bug Fixes
- **BaseSelectMenuBuilder:** Modify class to be `abstract` (#9358) ([ca4de2d](https://github.com/discordjs/discord.js/commit/ca4de2d9c6bc204e85d1b7eae7eabd23dbeb4475))
- Correct `@link` tags that involve parents (#9351) ([fbbce3e](https://github.com/discordjs/discord.js/commit/fbbce3eb4ba20bc0c4806ca2259d1f86001594be))
- Fix external links (#9313) ([a7425c2](https://github.com/discordjs/discord.js/commit/a7425c29c4f23f1b31f4c6a463107ca9eb7fd7e2))
## Documentation
- Reference package names properly (#9426) ([d6bca9b](https://github.com/discordjs/discord.js/commit/d6bca9bb4d976dc069a5039250db7d5b3e9142ef))
- Generate static imports for types with api-extractor ([98a76db](https://github.com/discordjs/discord.js/commit/98a76db482879f79d6bb2fb2e5fc65ac2c34e2d9))
- **builders:** Add some basic documentation (#9359) ([8073561](https://github.com/discordjs/discord.js/commit/8073561824f911d1a18d0b4f1de39f452bc69fa9))
- Use `@link` in `@see` (#9348) ([d66d113](https://github.com/discordjs/discord.js/commit/d66d1133331b81563588db4500c63a18c3c3dfae))
# [@discordjs/builders@1.6.3](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.2...@discordjs/builders@1.6.3) - (2023-05-01)
## Refactor
- Remove `@discordjs/util` re-export (#9488) ([54ceedf](https://github.com/discordjs/discord.js/commit/54ceedf6c535d4641643d4106b6286cbef09de4a))
# [@discordjs/builders@1.6.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.1...@discordjs/builders@1.6.2) - (2023-05-01)
## Bug Fixes
- **BaseSelectMenuBuilder:** Modify class to be `abstract` (#9358) ([ca4de2d](https://github.com/discordjs/discord.js/commit/ca4de2d9c6bc204e85d1b7eae7eabd23dbeb4475))
- Correct `@link` tags that involve parents (#9351) ([fbbce3e](https://github.com/discordjs/discord.js/commit/fbbce3eb4ba20bc0c4806ca2259d1f86001594be))
- Fix external links (#9313) ([a7425c2](https://github.com/discordjs/discord.js/commit/a7425c29c4f23f1b31f4c6a463107ca9eb7fd7e2))
## Documentation
- Reference package names properly (#9426) ([d6bca9b](https://github.com/discordjs/discord.js/commit/d6bca9bb4d976dc069a5039250db7d5b3e9142ef))
- Generate static imports for types with api-extractor ([98a76db](https://github.com/discordjs/discord.js/commit/98a76db482879f79d6bb2fb2e5fc65ac2c34e2d9))
- **builders:** Add some basic documentation (#9359) ([8073561](https://github.com/discordjs/discord.js/commit/8073561824f911d1a18d0b4f1de39f452bc69fa9))
- Use `@link` in `@see` (#9348) ([d66d113](https://github.com/discordjs/discord.js/commit/d66d1133331b81563588db4500c63a18c3c3dfae))
# [@discordjs/builders@1.6.3](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.2...@discordjs/builders@1.6.3) - (2023-05-01)
## Refactor
- Remove `@discordjs/util` re-export (#9488) ([54ceedf](https://github.com/discordjs/discord.js/commit/54ceedf6c535d4641643d4106b6286cbef09de4a))
# [@discordjs/builders@1.6.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.1...@discordjs/builders@1.6.2) - (2023-05-01)
## Bug Fixes
- **BaseSelectMenuBuilder:** Modify class to be `abstract` (#9358) ([ca4de2d](https://github.com/discordjs/discord.js/commit/ca4de2d9c6bc204e85d1b7eae7eabd23dbeb4475))
- Correct `@link` tags that involve parents (#9351) ([fbbce3e](https://github.com/discordjs/discord.js/commit/fbbce3eb4ba20bc0c4806ca2259d1f86001594be))
- Fix external links (#9313) ([a7425c2](https://github.com/discordjs/discord.js/commit/a7425c29c4f23f1b31f4c6a463107ca9eb7fd7e2))
## Documentation
- Reference package names properly (#9426) ([d6bca9b](https://github.com/discordjs/discord.js/commit/d6bca9bb4d976dc069a5039250db7d5b3e9142ef))
- Generate static imports for types with api-extractor ([98a76db](https://github.com/discordjs/discord.js/commit/98a76db482879f79d6bb2fb2e5fc65ac2c34e2d9))
- **builders:** Add some basic documentation (#9359) ([8073561](https://github.com/discordjs/discord.js/commit/8073561824f911d1a18d0b4f1de39f452bc69fa9))
- Use `@link` in `@see` (#9348) ([d66d113](https://github.com/discordjs/discord.js/commit/d66d1133331b81563588db4500c63a18c3c3dfae))
# [@discordjs/builders@1.6.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.6.1...@discordjs/builders@1.6.2) - (2023-05-01)
## Bug Fixes
- **BaseSelectMenuBuilder:** Modify class to be `abstract` (#9358) ([ca4de2d](https://github.com/discordjs/discord.js/commit/ca4de2d9c6bc204e85d1b7eae7eabd23dbeb4475))
- Correct `@link` tags that involve parents (#9351) ([fbbce3e](https://github.com/discordjs/discord.js/commit/fbbce3eb4ba20bc0c4806ca2259d1f86001594be))
- Fix external links (#9313) ([a7425c2](https://github.com/discordjs/discord.js/commit/a7425c29c4f23f1b31f4c6a463107ca9eb7fd7e2))
## Documentation
- Reference package names properly (#9426) ([d6bca9b](https://github.com/discordjs/discord.js/commit/d6bca9bb4d976dc069a5039250db7d5b3e9142ef))
- Generate static imports for types with api-extractor ([98a76db](https://github.com/discordjs/discord.js/commit/98a76db482879f79d6bb2fb2e5fc65ac2c34e2d9))
- **builders:** Add some basic documentation (#9359) ([8073561](https://github.com/discordjs/discord.js/commit/8073561824f911d1a18d0b4f1de39f452bc69fa9))
- Use `@link` in `@see` (#9348) ([d66d113](https://github.com/discordjs/discord.js/commit/d66d1133331b81563588db4500c63a18c3c3dfae))
# [@discordjs/builders@1.6.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.5.0...@discordjs/builders@1.6.0) - (2023-04-01)
## Bug Fixes
- **scripts:** Accessing tsComment ([d8d5f31](https://github.com/discordjs/discord.js/commit/d8d5f31d3927fd1de62f1fa3a1a6e454243ad87b))
## Features
- **website:** Render syntax and mdx on the server (#9086) ([ee5169e](https://github.com/discordjs/discord.js/commit/ee5169e0aadd7bbfcd752aae614ec0f69602b68b))
# [@discordjs/builders@1.5.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.4.0...@discordjs/builders@1.5.0) - (2023-03-12)
## Documentation
- **EmbedBuilder#spliceFields:** Fix a typo (#9159) ([4367ab9](https://github.com/discordjs/discord.js/commit/4367ab930227048868db3ed8437f6c4507ff32e1))
- Fix version export (#9049) ([8b70f49](https://github.com/discordjs/discord.js/commit/8b70f497a1207e30edebdecd12b926c981c13d28))
## Features
- **website:** Add support for source file links (#9048) ([f6506e9](https://github.com/discordjs/discord.js/commit/f6506e99c496683ee0ab67db0726b105b929af38))
- **StringSelectMenu:** Add `spliceOptions()` (#8937) ([a6941d5](https://github.com/discordjs/discord.js/commit/a6941d536ce24ed2b5446a154cbc886b2b97c63a))
- Add support for nsfw commands (#7976) ([7a51344](https://github.com/discordjs/discord.js/commit/7a5134459c5f06864bf74631d83b96d9c21b72d8))
- Add `@discordjs/formatters` (#8889) ([3fca638](https://github.com/discordjs/discord.js/commit/3fca638a8470dcea2f79ddb9f18526dbc0017c88))
## Styling
- Run prettier (#9041) ([2798ba1](https://github.com/discordjs/discord.js/commit/2798ba1eb3d734f0cf2eeccd2e16cfba6804873b))
# [@discordjs/builders@1.4.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.3.0...@discordjs/builders@1.4.0) - (2022-11-28)
## Bug Fixes
- Pin @types/node version ([9d8179c](https://github.com/discordjs/discord.js/commit/9d8179c6a78e1c7f9976f852804055964d5385d4))
## Features
- New select menus (#8793) ([5152abf](https://github.com/discordjs/discord.js/commit/5152abf7285581abf7689e9050fdc56c4abb1e2b))
- Allow punctuation characters in context menus (#8783) ([b521366](https://github.com/discordjs/discord.js/commit/b5213664fa66746daab1673ebe2adf2db3d1522c))
## Typings
- **Formatters:** Allow boolean in `formatEmoji` (#8823) ([ec37f13](https://github.com/discordjs/discord.js/commit/ec37f137fd4fca0fdbdb8a5c83abf32362a8f285))
# [@discordjs/builders@1.3.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.2.0...@discordjs/builders@1.3.0) - (2022-10-08)
## Bug Fixes
- Allow adding forums to `channelTypes` (#8658) ([b1e190c](https://github.com/discordjs/discord.js/commit/b1e190c4f0773a1a739625f5b41026f593515370))
- **SlashCommandBuilder:** Missing methods in subcommand builder (#8583) ([1c5b78f](https://github.com/discordjs/discord.js/commit/1c5b78fd2130f09c951459cf4c2d637f46c3c2c9))
- Footer / sidebar / deprecation alert ([ba3e0ed](https://github.com/discordjs/discord.js/commit/ba3e0ed348258fe8e51eefb4aa7379a1230616a9))
## Documentation
- **builders/components:** Document constructors (#8636) ([8444576](https://github.com/discordjs/discord.js/commit/8444576f45da5fdddbf8ba2d91b4cb31a3b51c04))
- Change name (#8604) ([dd5a089](https://github.com/discordjs/discord.js/commit/dd5a08944c258a847fc4377f1d5e953264ab47d0))
- Use remarks instead of `Note` in descriptions (#8597) ([f3ce4a7](https://github.com/discordjs/discord.js/commit/f3ce4a75d0c4eafc89a1f0ce9f4964bcbcdae6da))
## Features
- Web-components (#8715) ([0ac3e76](https://github.com/discordjs/discord.js/commit/0ac3e766bd9dbdeb106483fa4bb085d74de346a2))
- Add `@discordjs/util` (#8591) ([b2ec865](https://github.com/discordjs/discord.js/commit/b2ec865765bf94181473864a627fb63ea8173fd3))
- Add `chatInputApplicationCommandMention` formatter (#8546) ([d08a57c](https://github.com/discordjs/discord.js/commit/d08a57cadd9d69a734077cc1902d931ab10336db))
## Refactor
- Replace usage of deprecated `ChannelType`s (#8625) ([669c3cd](https://github.com/discordjs/discord.js/commit/669c3cd2566eac68ef38ab522dd6378ba761e8b3))
- Website components (#8600) ([c334157](https://github.com/discordjs/discord.js/commit/c3341570d983aea9ecc419979d5a01de658c9d67))
- Use `eslint-config-neon` for packages. (#8579) ([edadb9f](https://github.com/discordjs/discord.js/commit/edadb9fe5dfd9ff51a3cfc9b25cb242d3f9f5241))
## Testing
- Rename incorrect test (#8596) ([ce991dd](https://github.com/discordjs/discord.js/commit/ce991dd1d883f6785b5f4b4b3ac80ef21cb304e7))
## Typings
- **interactions:** Fix `{Slash,ContextMenu}CommandBuilder#toJSON` (#8568) ([b7eb96d](https://github.com/discordjs/discord.js/commit/b7eb96d45670616521fbcca28a657793d91605c7))
# [@discordjs/builders@1.2.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.1.0...@discordjs/builders@1.2.0) - (2022-08-22)
## Features
- **website:** Show `constructor` information (#8540) ([e42fd16](https://github.com/discordjs/discord.js/commit/e42fd1636973b10dd7ed6fb4280ee1a4a8f82007))
- **website:** Show descriptions for `@typeParam` blocks (#8523) ([e475b63](https://github.com/discordjs/discord.js/commit/e475b63f257f6261d73cb89fee9ecbcdd84e2a6b))
- **website:** Show parameter descriptions (#8519) ([7f415a2](https://github.com/discordjs/discord.js/commit/7f415a2502bf7ce2025dbcfed9017b0635a19966))
- **WebSocketShard:** Support new resume url (#8480) ([bc06cc6](https://github.com/discordjs/discord.js/commit/bc06cc638d2f57ab5c600e8cdb6afc8eb2180166))
## Refactor
- Docs design (#8487) ([4ab1d09](https://github.com/discordjs/discord.js/commit/4ab1d09997a18879a9eb9bda39df6f15aa22557e))
# [@discordjs/builders@1.1.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.16.0...@discordjs/builders@1.1.0) - (2022-07-29)
## Bug Fixes
- Use proper format for `@link` text (#8384) ([2655639](https://github.com/discordjs/discord.js/commit/26556390a3800e954974a00c1328ff47d3e67e9a))
- **Formatters:** Add newline in `codeBlock` (#8369) ([5d8bd03](https://github.com/discordjs/discord.js/commit/5d8bd030d60ef364de3ef5f9963da8bda5c4efd4))
- **selectMenu:** Allow json to be used for select menu options (#8322) ([6a2d0d8](https://github.com/discordjs/discord.js/commit/6a2d0d8e96d157d5b85cee7f17bffdfff4240074))
## Documentation
- Use link tags (#8382) ([5494791](https://github.com/discordjs/discord.js/commit/549479131318c659f86f0eb18578d597e22522d3))
## Features
- Add channel & message URL formatters (#8371) ([a7deb8f](https://github.com/discordjs/discord.js/commit/a7deb8f89830ead6185c5fb46a49688b6d209ed1))
## Testing
- **builders:** Improve coverage (#8274) ([b7e6238](https://github.com/discordjs/discord.js/commit/b7e62380f2e6b9324d6bba9b9eaa5315080bf66a))
# [@discordjs/builders@0.16.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.15.0...@discordjs/builders@0.16.0) - (2022-07-17)
## Bug Fixes
- Slash command name regex (#8265) ([32f9056](https://github.com/discordjs/discord.js/commit/32f9056b15edede3bab07de96afb4b56d3a9ecca))
- **TextInputBuilder:** Parse `custom_id`, `label`, and `style` (#8216) ([2d9dfa3](https://github.com/discordjs/discord.js/commit/2d9dfa3c6ea4bb972da2f7e088d148b798c866d9))
## Documentation
- Add codecov coverage badge to readmes (#8226) ([f6db285](https://github.com/discordjs/discord.js/commit/f6db285c073898a749fe4591cbd4463d1896daf5))
## Features
- **builder:** Add max min length in string option (#8214) ([96c8d21](https://github.com/discordjs/discord.js/commit/96c8d21f95eb366c46ae23505ba9054f44821b25))
- Codecov (#8219) ([f10f4cd](https://github.com/discordjs/discord.js/commit/f10f4cdcd88ca6be7ec735ed3a415ba13da83db0))
- **docgen:** Update typedoc ([b3346f4](https://github.com/discordjs/discord.js/commit/b3346f4b9b3d4f96443506643d4631dc1c6d7b21))
- Website (#8043) ([127931d](https://github.com/discordjs/discord.js/commit/127931d1df7a2a5c27923c2f2151dbf3824e50cc))
- **docgen:** Typescript support ([3279b40](https://github.com/discordjs/discord.js/commit/3279b40912e6aa61507bedb7db15a2b8668de44b))
- Docgen package (#8029) ([8b979c0](https://github.com/discordjs/discord.js/commit/8b979c0245c42fd824d8e98745ee869f5360fc86))
## Refactor
- **builder:** Remove `unsafe*Builder`s (#8074) ([a4d1862](https://github.com/discordjs/discord.js/commit/a4d18629828234f43f03d1bd4851d4b727c6903b))
- Remove @sindresorhus/is as it's now esm only (#8133) ([c6f285b](https://github.com/discordjs/discord.js/commit/c6f285b7b089b004776fbeb444fe973a68d158d8))
- Move all the config files to root (#8033) ([769ea0b](https://github.com/discordjs/discord.js/commit/769ea0bfe78c4f1d413c6b397c604ffe91e39c6a))
## Typings
- Remove expect error (#8242) ([7e6dbaa](https://github.com/discordjs/discord.js/commit/7e6dbaaed900c07d1a04e23bbbf9cd0d1b0501c5))
- **builder:** Remove casting (#8241) ([8198da5](https://github.com/discordjs/discord.js/commit/8198da5cd0898e06954615a2287853321e7ebbd4))
# [@discordjs/builders@0.15.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.14.0...@discordjs/builders@0.15.0) - (2022-06-06)
## Features
- Allow builders to accept rest params and arrays (#7874) ([ad75be9](https://github.com/discordjs/discord.js/commit/ad75be9a9cf90c8624495df99b75177e6c24022f))
- Use vitest instead of jest for more speed ([8d8e6c0](https://github.com/discordjs/discord.js/commit/8d8e6c03decd7352a2aa180f6e5bc1a13602539b))
- Add scripts package for locally used scripts ([f2ae1f9](https://github.com/discordjs/discord.js/commit/f2ae1f9348bfd893332a9060f71a8a5f272a1b8b))
# [@discordjs/builders@0.14.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.13.0...@discordjs/builders@0.14.0) - (2022-06-04)
## Bug Fixes
- **builders:** Leftover invalid null type ([8a7cd10](https://github.com/discordjs/discord.js/commit/8a7cd10554a2a71cd2fe7f6a177b5f4f43464348))
- **SlashCommandBuilder:** Import `Permissions` correctly (#7921) ([7ce641d](https://github.com/discordjs/discord.js/commit/7ce641d33a4af6586d5e7beffbe7d38619dcf1a2))
- Add localizations for subcommand builders and option choices (#7862) ([c1b5e73](https://github.com/discordjs/discord.js/commit/c1b5e731daa9cbbfca03a046e47cb1221ee1ed7c))
## Features
- Export types from `interactions/slashCommands/mixins` (#7942) ([68d5169](https://github.com/discordjs/discord.js/commit/68d5169f66c96f8fe5be17a1c01cdd5155607ab2))
- **builders:** Add new command permissions v2 (#7861) ([de3f157](https://github.com/discordjs/discord.js/commit/de3f1573f07dda294cc0fbb1ca4b659eb2388a12))
- **builders:** Improve embed errors and predicates (#7795) ([ec8d87f](https://github.com/discordjs/discord.js/commit/ec8d87f93272cc9987f9613735c0361680c4ed1e))
## Refactor
- Use arrays instead of rest parameters for builders (#7759) ([29293d7](https://github.com/discordjs/discord.js/commit/29293d7bbb5ed463e52e5a5853817e5a09cf265b))
## Styling
- Cleanup tests and tsup configs ([6b8ef20](https://github.com/discordjs/discord.js/commit/6b8ef20cb3af5b5cfd176dd0aa0a1a1e98551629))
# [@discordjs/builders@0.13.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.12.0...@discordjs/builders@0.13.0) - (2022-04-17)
## Bug Fixes

View File

@@ -23,7 +23,7 @@
## Installation
**Node.js 18 or newer is required.**
**Node.js 16.11.0 or newer is required.**
```sh
npm install @discordjs/builders

View File

@@ -7,8 +7,8 @@ import {
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
PrimaryButtonBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
} from '../../src/index.js';
@@ -41,14 +41,21 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
value: 'two',
},
],
max_values: 2,
min_values: 2,
max_values: 10,
min_values: 12,
},
],
};
describe('Action Row Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().addComponents([new ButtonBuilder()])).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents([new ButtonBuilder()])).not.toThrowError();
});
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
@@ -65,10 +72,22 @@ describe('Action Row Components', () => {
style: ButtonStyle.Link,
url: 'https://google.com',
},
{
type: ComponentType.StringSelect,
placeholder: 'test',
custom_id: 'test',
options: [
{
label: 'option',
value: 'option',
},
],
},
],
};
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
@@ -101,23 +120,24 @@ describe('Action Row Components', () => {
value: 'two',
},
],
max_values: 1,
min_values: 1,
max_values: 10,
min_values: 12,
},
],
};
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123');
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setMaxValues(2)
.setMinValues(2)
.setMaxValues(10)
.setMinValues(12)
.setOptions(
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
@@ -127,39 +147,10 @@ describe('Action Row Components', () => {
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
]);
expect(new ActionRowBuilder().addPrimaryButtonComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().addPrimaryButtonComponents([button]).toJSON()).toEqual(rowWithButtonData);
});
test('GIVEN 2 select menus THEN it throws', () => {
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setOptions(
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
);
expect(() =>
new ActionRowBuilder()
.addStringSelectMenuComponent(selectMenu)
.addStringSelectMenuComponent(selectMenu)
.toJSON(),
).toThrowError();
});
test('GIVEN a button and a select menu THEN it throws', () => {
const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123');
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setOptions(
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
);
expect(() =>
new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).addPrimaryButtonComponents(button).toJSON(),
).toThrowError();
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().addComponents([button]).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents([selectMenu]).toJSON()).toEqual(rowWithSelectMenuData);
});
});
});

View File

@@ -5,20 +5,45 @@ import {
type APIButtonComponentWithURL,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { PrimaryButtonBuilder, PremiumButtonBuilder, LinkButtonBuilder } from '../../src/index.js';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions.js';
import { ButtonBuilder } from '../../src/components/button/Button.js';
const buttonComponent = () => new ButtonBuilder();
const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
describe('Button Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid label THEN validator does not throw', () => {
expect(() => buttonLabelValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid label THEN validator does throw', () => {
expect(() => buttonLabelValidator.parse(null)).toThrowError();
expect(() => buttonLabelValidator.parse('')).toThrowError();
expect(() => buttonLabelValidator.parse(longStr)).toThrowError();
});
test('GIVEN valid style THEN validator does not throw', () => {
expect(() => buttonStyleValidator.parse(3)).not.toThrowError();
expect(() => buttonStyleValidator.parse(ButtonStyle.Secondary)).not.toThrowError();
});
test('GIVEN invalid style THEN validator does throw', () => {
expect(() => buttonStyleValidator.parse(7)).toThrowError();
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => new PrimaryButtonBuilder().setCustomId('custom').setLabel('test')).not.toThrowError();
expect(() =>
buttonComponent().setCustomId('custom').setStyle(ButtonStyle.Primary).setLabel('test'),
).not.toThrowError();
expect(() => {
const button = new PrimaryButtonBuilder()
const button = buttonComponent()
.setCustomId('custom')
.setLabel('test')
.setStyle(ButtonStyle.Primary)
.setDisabled(true)
.setEmoji({ name: 'test' });
@@ -26,41 +51,111 @@ describe('Button Components', () => {
}).not.toThrowError();
expect(() => {
const button = new PremiumButtonBuilder().setSKUId('123456789012345678');
const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium);
button.toJSON();
}).not.toThrowError();
expect(() => new LinkButtonBuilder().setURL('https://google.com')).not.toThrowError();
expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError();
});
test('GIVEN invalid fields THEN build does throw', () => {
expect(() => {
new PrimaryButtonBuilder().setCustomId(longStr).toJSON();
buttonComponent().setCustomId(longStr);
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setCustomId('custom')
.setStyle(ButtonStyle.Primary)
.setDisabled(true)
.setLabel('test')
.setURL('https://google.com')
.setEmoji({ name: 'test' });
button.toJSON();
}).toThrowError();
expect(() => {
// @ts-expect-error: Invalid emoji
const button = new PrimaryButtonBuilder().setEmoji('test');
const button = buttonComponent().setEmoji('test');
button.toJSON();
}).toThrowError();
expect(() => {
const button = new PrimaryButtonBuilder();
const button = buttonComponent().setStyle(ButtonStyle.Primary);
button.toJSON();
}).toThrowError();
expect(() => {
const button = new PrimaryButtonBuilder().setCustomId('test');
const button = buttonComponent().setStyle(ButtonStyle.Primary).setCustomId('test');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Link);
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary).setLabel('test').setURL('https://google.com');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent().setStyle(ButtonStyle.Primary).setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Secondary)
.setLabel('button')
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Success)
.setEmoji({ name: '😇' })
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Danger)
.setCustomId('test')
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
expect(() => {
const button = buttonComponent()
.setStyle(ButtonStyle.Link)
.setURL('https://google.com')
.setSKUId('123456789012345678');
button.toJSON();
}).toThrowError();
// @ts-expect-error: Invalid style
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setStyle(24).toJSON()).toThrowError();
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setLabel(longStr).toJSON()).toThrowError();
expect(() => buttonComponent().setStyle(24)).toThrowError();
expect(() => buttonComponent().setLabel(longStr)).toThrowError();
// @ts-expect-error: Invalid parameter for disabled
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setDisabled(0).toJSON()).toThrowError();
expect(() => buttonComponent().setDisabled(0)).toThrowError();
// @ts-expect-error: Invalid emoji
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setEmoji('foo').toJSON()).toThrowError();
expect(() => buttonComponent().setEmoji('foo')).toThrowError();
expect(() => buttonComponent().setURL('foobar')).toThrowError();
});
test('GiVEN valid input THEN valid JSON outputs are given', () => {
@@ -72,12 +167,13 @@ describe('Button Components', () => {
disabled: true,
};
expect(new PrimaryButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
expect(
new PrimaryButtonBuilder()
buttonComponent()
.setCustomId(interactionData.custom_id)
.setLabel(interactionData.label!)
.setStyle(interactionData.style)
.setDisabled(interactionData.disabled)
.toJSON(),
).toEqual(interactionData);
@@ -90,7 +186,9 @@ describe('Button Components', () => {
url: 'https://google.com',
};
expect(new LinkButtonBuilder(linkData).toJSON()).toEqual(linkData);
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
expect(buttonComponent().setLabel(linkData.label!).setDisabled(true).setURL(linkData.url));
});
});
});

View File

@@ -11,14 +11,14 @@ import {
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
CustomIdButtonBuilder,
StringSelectMenuBuilder,
TextInputBuilder,
} from '../../src/index.js';
describe('createComponentBuilder', () => {
test.each([StringSelectMenuBuilder, TextInputBuilder])(
test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])(
'passing an instance of %j should return itself',
(Builder) => {
const builder = new Builder();
@@ -42,7 +42,7 @@ describe('createComponentBuilder', () => {
type: ComponentType.Button,
};
expect(createComponentBuilder(button)).toBeInstanceOf(CustomIdButtonBuilder);
expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder);
});
test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => {

View File

@@ -3,7 +3,6 @@ import { describe, test, expect } from 'vitest';
import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js';
const selectMenu = () => new StringSelectMenuBuilder();
const selectMenuWithId = () => new StringSelectMenuBuilder({ custom_id: 'hi' });
const selectMenuOption = () => new StringSelectMenuOptionBuilder();
const longStr = 'a'.repeat(256);
@@ -17,10 +16,10 @@ const selectMenuOptionData: APISelectMenuOption = {
};
const selectMenuDataWithoutOptions = {
type: ComponentType.StringSelect,
type: ComponentType.SelectMenu,
custom_id: 'test',
max_values: 1,
min_values: 1,
max_values: 10,
min_values: 3,
disabled: true,
placeholder: 'test',
} as const;
@@ -110,87 +109,49 @@ describe('Select Menu Components', () => {
});
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
expect(() => selectMenu().setCustomId(longStr).toJSON()).toThrowError();
expect(() => selectMenuWithId().setMaxValues(30).toJSON()).toThrowError();
expect(() => selectMenuWithId().setMinValues(-20).toJSON()).toThrowError();
expect(() => selectMenu().setCustomId(longStr)).toThrowError();
expect(() => selectMenu().setMaxValues(30)).toThrowError();
expect(() => selectMenu().setMinValues(-20)).toThrowError();
// @ts-expect-error: Invalid disabled value
expect(() => selectMenuWithId().setDisabled(0).toJSON()).toThrowError();
expect(() => selectMenuWithId().setPlaceholder(longStr).toJSON()).toThrowError();
expect(() => selectMenu().setDisabled(0)).toThrowError();
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenuWithId().addOptions({ label: 'test' }).toJSON()).toThrowError();
expect(() => selectMenuWithId().addOptions({ label: longStr, value: 'test' }).toJSON()).toThrowError();
expect(() => selectMenuWithId().addOptions({ value: longStr, label: 'test' }).toJSON()).toThrowError();
expect(() =>
selectMenuWithId().addOptions({ label: 'test', value: 'test', description: longStr }).toJSON(),
).toThrowError();
expect(() =>
// @ts-expect-error: Invalid option
selectMenuWithId().addOptions({ label: 'test', value: 'test', default: 100 }).toJSON(),
).toThrowError();
expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenuWithId().addOptions({ value: 'test' }).toJSON()).toThrowError();
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenuWithId().addOptions({ default: true }).toJSON()).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ label: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
.addOptions([{ label: longStr, value: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
.addOptions([{ value: longStr, label: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
.addOptions([{ label: 'test', value: 'test', description: longStr }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ label: 'test', value: 'test', default: 100 }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ value: 'test' }])
.toJSON(),
).toThrowError();
expect(() =>
selectMenuWithId()
// @ts-expect-error: Invalid option
.addOptions([{ default: true }])
.toJSON(),
).toThrowError();
expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions({ default: true })).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', description: longStr }])).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', default: 100 }])).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ value: 'test' }])).toThrowError();
// @ts-expect-error: Invalid option
expect(() => selectMenu().addOptions([{ default: true }])).toThrowError();
const tooManyOptions = Array.from<APISelectMenuOption>({ length: 26 }).fill({ label: 'test', value: 'test' });
expect(() =>
selectMenu()
.setOptions(...tooManyOptions)
.toJSON(),
).toThrowError();
expect(() => selectMenu().setOptions(tooManyOptions).toJSON()).toThrowError();
expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError();
expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...tooManyOptions)
.toJSON(),
.addOptions(...tooManyOptions),
).toThrowError();
expect(() =>
selectMenu()
.addOptions([{ label: 'test', value: 'test' }])
.addOptions(tooManyOptions)
.toJSON(),
.addOptions(tooManyOptions),
).toThrowError();
expect(() => {
@@ -201,8 +162,7 @@ describe('Select Menu Components', () => {
.setDefault(-1)
// @ts-expect-error: Invalid emoji
.setEmoji({ name: 1 })
.setDescription(longStr)
.toJSON();
.setDescription(longStr);
}).toThrowError();
});
@@ -252,16 +212,17 @@ describe('Select Menu Components', () => {
).toStrictEqual([selectMenuOptionData]);
expect(() =>
makeStringSelectMenuWithOptions()
.spliceOptions(0, 0, ...Array.from({ length: 26 }, () => selectMenuOptionData))
.toJSON(),
makeStringSelectMenuWithOptions().spliceOptions(
0,
0,
...Array.from({ length: 26 }, () => selectMenuOptionData),
),
).toThrowError();
expect(() =>
makeStringSelectMenuWithOptions()
.setOptions(Array.from({ length: 25 }, () => selectMenuOptionData))
.spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData)
.toJSON(),
.spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData),
).toThrowError();
});
});

View File

@@ -1,5 +1,13 @@
import { ComponentType, TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
labelValidator,
maxLengthValidator,
minLengthValidator,
placeholderValidator,
valueValidator,
textInputStyleValidator,
} from '../../src/components/textInput/Assertions.js';
import { TextInputBuilder } from '../../src/components/textInput/TextInput.js';
const superLongStr = 'a'.repeat(5_000);
@@ -8,6 +16,56 @@ const textInputComponent = () => new TextInputBuilder();
describe('Text Input Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid label THEN validator does not throw', () => {
expect(() => labelValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid label THEN validator does throw', () => {
expect(() => labelValidator.parse(24)).toThrowError();
expect(() => labelValidator.parse(undefined)).toThrowError();
});
test('GIVEN valid style THEN validator does not throw', () => {
expect(() => textInputStyleValidator.parse(TextInputStyle.Paragraph)).not.toThrowError();
expect(() => textInputStyleValidator.parse(TextInputStyle.Short)).not.toThrowError();
});
test('GIVEN invalid style THEN validator does throw', () => {
expect(() => textInputStyleValidator.parse(24)).toThrowError();
});
test('GIVEN valid min length THEN validator does not throw', () => {
expect(() => minLengthValidator.parse(10)).not.toThrowError();
});
test('GIVEN invalid min length THEN validator does throw', () => {
expect(() => minLengthValidator.parse(-1)).toThrowError();
});
test('GIVEN valid max length THEN validator does not throw', () => {
expect(() => maxLengthValidator.parse(10)).not.toThrowError();
});
test('GIVEN invalid min length THEN validator does throw 2', () => {
expect(() => maxLengthValidator.parse(4_001)).toThrowError();
});
test('GIVEN valid value THEN validator does not throw', () => {
expect(() => valueValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid value THEN validator does throw', () => {
expect(() => valueValidator.parse(superLongStr)).toThrowError();
});
test('GIVEN valid placeholder THEN validator does not throw', () => {
expect(() => placeholderValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid value THEN validator does throw 2', () => {
expect(() => placeholderValidator.parse(superLongStr)).toThrowError();
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => {
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
@@ -26,7 +84,9 @@ describe('Text Input Components', () => {
}).not.toThrowError();
expect(() => {
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON();
// Issue #8107
// @ts-expect-error: Shapeshift maps the enum key to the value when parsing
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle('Short').toJSON();
}).not.toThrowError();
});
});

View File

@@ -1,516 +0,0 @@
import {
ApplicationCommandType,
ApplicationIntegrationType,
ChannelType,
InteractionContextType,
PermissionFlagsBits,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
ChatInputCommandBooleanOption,
ChatInputCommandBuilder,
ChatInputCommandChannelOption,
ChatInputCommandIntegerOption,
ChatInputCommandMentionableOption,
ChatInputCommandNumberOption,
ChatInputCommandRoleOption,
ChatInputCommandAttachmentOption,
ChatInputCommandStringOption,
ChatInputCommandSubcommandBuilder,
ChatInputCommandSubcommandGroupBuilder,
ChatInputCommandUserOption,
} from '../../../src/index.js';
const getBuilder = () => new ChatInputCommandBuilder();
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123');
const getIntegerOption = () => new ChatInputCommandIntegerOption().setName('owo').setDescription('Testing 123');
const getNumberOption = () => new ChatInputCommandNumberOption().setName('owo').setDescription('Testing 123');
const getBooleanOption = () => new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123');
const getUserOption = () => new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123');
const getChannelOption = () => new ChatInputCommandChannelOption().setName('owo').setDescription('Testing 123');
const getRoleOption = () => new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123');
const getAttachmentOption = () => new ChatInputCommandAttachmentOption().setName('owo').setDescription('Testing 123');
const getMentionableOption = () => new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123');
const getSubcommandGroup = () =>
new ChatInputCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
class Collection {
public readonly [Symbol.toStringTag] = 'Map';
}
describe('ChatInput Commands', () => {
describe('ChatInputCommandBuilder', () => {
describe('Builder with no options', () => {
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
expect(() => getBuilder().toJSON()).toThrowError();
});
test('GIVEN valid builder THEN does not throw error', () => {
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
});
});
describe('Builder with simple options', () => {
test('GIVEN valid builder THEN returns type included', () => {
expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput });
});
test('GIVEN valid builder with options THEN does not throw error', () => {
expect(() =>
getBuilder()
.setName('example')
.setDescription('Example command')
.addBooleanOptions((boolean) =>
boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true),
)
.addChannelOptions((channel) => channel.setName('iscool').setDescription('Are we cool or what?'))
.addMentionableOptions((mentionable) =>
mentionable.setName('iscool').setDescription('Are we cool or what?'),
)
.addRoleOptions((role) => role.setName('iscool').setDescription('Are we cool or what?'))
.addUserOptions((user) => user.setName('iscool').setDescription('Are we cool or what?'))
.addIntegerOptions((integer) =>
integer
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices({ name: 'Very cool', value: 1_000 })
.addChoices([{ name: 'Even cooler', value: 2_000 }]),
)
.addNumberOptions((number) =>
number
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices({ name: 'Very cool', value: 1.5 })
.addChoices([{ name: 'Even cooler', value: 2.5 }]),
)
.addStringOptions((string) =>
string
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' })
.addChoices([{ name: 'The Whole shebang', value: 'all' }]),
)
.addIntegerOptions((integer) =>
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addNumberOptions((number) =>
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addStringOptions((string) =>
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => {
expect(() =>
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
getNamedBuilder().addStringOptions(getStringOption().setAutocomplete('not a boolean')).toJSON(),
).toThrowError();
});
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
expect(() =>
getNamedBuilder()
.addStringOptions(
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
)
.toJSON(),
).toThrowError();
expect(() =>
getNamedBuilder()
.addStringOptions(
getStringOption()
.setAutocomplete(true)
.addChoices(
{ name: 'Fancy Pants', value: 'fp_1' },
{ name: 'Fancy Shoes', value: 'fs_1' },
{ name: 'The Whole shebang', value: 'all' },
),
)
.toJSON(),
).toThrowError();
expect(() =>
getNamedBuilder()
.addStringOptions(
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
)
.toJSON(),
).toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
expect(() =>
getNamedBuilder()
.addChannelOptions(
getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]),
)
.toJSON(),
).not.toThrowError();
expect(() => {
getNamedBuilder()
.addChannelOptions(getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText))
.toJSON();
}).not.toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
expect(() =>
// @ts-expect-error: Invalid channel type
getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100)).toJSON(),
).toThrowError();
expect(() =>
// @ts-expect-error: Invalid channel types
getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100, 200)).toJSON(),
).toThrowError();
});
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
// @ts-expect-error: Invalid max value
expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue('test')).toJSON()).toThrowError();
expect(() =>
// @ts-expect-error: Invalid max value
getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue('test')).toJSON(),
).toThrowError();
// @ts-expect-error: Invalid min value
expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMinValue('test')).toJSON()).toThrowError();
expect(() =>
// @ts-expect-error: Invalid min value
getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue('test')).toJSON(),
).toThrowError();
expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1.5)).toJSON()).toThrowError();
});
test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => {
expect(() =>
getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1)).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().addNumberOptions(getNumberOption().setMinValue(1.5)).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue(1)).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue(1.5)).toJSON(),
).not.toThrowError();
});
test('GIVEN an already built builder THEN does not throw an error', () => {
expect(() => getNamedBuilder().addStringOptions(getStringOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addNumberOptions(getNumberOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addUserOptions(getUserOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addChannelOptions(getChannelOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addRoleOptions(getRoleOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addAttachmentOptions(getAttachmentOption()).toJSON()).not.toThrowError();
expect(() => getNamedBuilder().addMentionableOptions(getMentionableOption()).toJSON()).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('TEST_COMMAND').setDescription(':3').toJSON()).toThrowError();
expect(() => getBuilder().setName('ĂĂĂĂĂĂ').setDescription(':3').toJSON()).toThrowError();
});
test('GIVEN valid names THEN does not throw error', () => {
expect(() => getBuilder().setName('hi_there').setDescription(':3')).not.toThrowError();
expect(() => getBuilder().setName('o_comandă').setDescription(':3')).not.toThrowError();
expect(() => getBuilder().setName('どうも').setDescription(':3')).not.toThrowError();
});
test('GIVEN invalid returns for builder THEN throw error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getNamedBuilder().addBooleanOptions(true).toJSON()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getNamedBuilder().addBooleanOptions(null).toJSON()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getNamedBuilder().addBooleanOptions(undefined).toJSON()).toThrowError();
expect(() =>
getNamedBuilder()
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
.addBooleanOptions(() => ChatInputCommandStringOption)
.toJSON(),
).toThrowError();
expect(() =>
getNamedBuilder()
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
.addBooleanOptions(() => new Collection())
.toJSON(),
).toThrowError();
});
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
expect(() =>
getNamedBuilder().addStringOptions(getStringOption().setAutocomplete(true).setChoices()).toJSON(),
).not.toThrowError();
});
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
expect(() =>
getNamedBuilder()
.addStringOptions(getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }))
.toJSON(),
).toThrowError();
});
test('GIVEN an option, THEN setting choices should not throw an error', () => {
expect(() =>
getNamedBuilder()
.addStringOptions(getStringOption().setChoices({ name: 'owo', value: 'uwu' }))
.toJSON(),
).not.toThrowError();
});
test('GIVEN valid builder with NSFW, THEN does not throw error', () => {
expect(() => getNamedBuilder().setName('foo').setDescription('foo').setNSFW(true).toJSON()).not.toThrowError();
});
});
describe('Builder with subcommand (group) options', () => {
test('GIVEN builder with subcommand group THEN does not throw error', () => {
expect(() =>
getNamedBuilder()
.addSubcommandGroups((group) =>
group.setName('group').setDescription('Group us together!').addSubcommands(getSubcommand()),
)
.toJSON(),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN does not throw error', () => {
expect(() =>
getNamedBuilder()
.addSubcommands((subcommand) => subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'))
.toJSON(),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN has regular ChatInput command fields', () => {
expect(() =>
getBuilder()
.setName('name')
.setDescription('description')
.addSubcommands((option) => option.setName('ye').setDescription('ye'))
.addSubcommands((option) => option.setName('no').setDescription('no'))
.setDefaultMemberPermissions(1n)
.toJSON(),
).not.toThrowError();
});
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand())).toJSON(),
).not.toThrowError();
});
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
expect(() => getNamedBuilder().addSubcommands(getSubcommand()).toJSON()).not.toThrowError();
});
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommands(getSubcommand().addBooleanOptions(getBooleanOption())).toJSON(),
).not.toThrowError();
});
test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => {
expect(() =>
// @ts-expect-error: Checking if check works JS-side too
getNamedBuilder().addSubcommands(getSubcommand()).addIntegerOptions(getInteger()).toJSON(),
).toThrowError();
});
test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getNamedBuilder().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError();
});
});
describe('Subcommand group builder', () => {
test('GIVEN no valid subcommand THEN throw error', () => {
expect(() => getSubcommandGroup().addSubcommands().toJSON()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getSubcommandGroup().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError();
});
test('GIVEN a valid subcommand THEN does not throw an error', () => {
expect(() =>
getSubcommandGroup()
.addSubcommands((sub) => sub.setName('sub').setDescription('Testing 123'))
.toJSON(),
).not.toThrowError();
});
});
describe('Subcommand builder', () => {
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
expect(() => getSubcommand().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError();
});
});
describe('ChatInput command localizations', () => {
const expectedSingleLocale = { 'en-US': 'foobar' };
const expectedMultipleLocales = {
...expectedSingleLocale,
bg: 'test',
};
test('GIVEN valid name localizations THEN does not throw error', () => {
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization
expect(() => getNamedBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError();
// @ts-expect-error: Invalid localization
expect(() => getNamedBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
});
test('GIVEN valid name localizations THEN valid data is stored', () => {
expect(getNamedBuilder().setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual(
expectedSingleLocale,
);
expect(
getNamedBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON().name_localizations,
).toEqual(expectedMultipleLocales);
expect(getNamedBuilder().clearNameLocalizations().toJSON().name_localizations).toBeUndefined();
expect(getNamedBuilder().clearNameLocalization('en-US').toJSON().name_localizations).toEqual({
'en-US': undefined,
});
});
test('GIVEN valid description localizations THEN does not throw error', () => {
expect(() => getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON()).not.toThrowError();
expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' }).toJSON()).not.toThrowError();
});
test('GIVEN invalid description localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization description
expect(() => getNamedBuilder().setDescriptionLocalization('en-U', 'foobar').toJSON()).toThrowError();
// @ts-expect-error: Invalid localization description
expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
});
test('GIVEN valid description localizations THEN valid data is stored', () => {
expect(
getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON(false).description_localizations,
).toEqual(expectedSingleLocale);
expect(
getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON(false)
.description_localizations,
).toEqual(expectedMultipleLocales);
expect(
getNamedBuilder().clearDescriptionLocalizations().toJSON(false).description_localizations,
).toBeUndefined();
expect(getNamedBuilder().clearDescriptionLocalization('en-US').toJSON(false).description_localizations).toEqual(
{
'en-US': undefined,
},
);
});
});
describe('permissions', () => {
test('GIVEN valid permission string THEN does not throw error', () => {
expect(() => getNamedBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
});
test('GIVEN valid permission bitfield THEN does not throw error', () => {
expect(() =>
getNamedBuilder().setDefaultMemberPermissions(
PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles,
),
).not.toThrowError();
});
test('GIVEN null permissions THEN does not throw error', () => {
expect(() => getNamedBuilder().clearDefaultMemberPermissions()).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getNamedBuilder().setDefaultMemberPermissions('1.1').toJSON()).toThrowError();
expect(() => getNamedBuilder().setDefaultMemberPermissions(1.1).toJSON()).toThrowError();
});
test('GIVEN valid permission with options THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addBooleanOptions(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(),
).not.toThrowError();
expect(() => getNamedBuilder().addChannelOptions(getChannelOption())).not.toThrowError();
});
});
describe('contexts', () => {
test('GIVEN a builder with valid contexts THEN does not throw an error', () => {
expect(() =>
getNamedBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]).toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM).toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
// @ts-expect-error: Invalid contexts
expect(() => getNamedBuilder().setContexts(999).toJSON()).toThrowError();
// @ts-expect-error: Invalid contexts
expect(() => getNamedBuilder().setContexts([999, 998]).toJSON()).toThrowError();
});
});
describe('integration types', () => {
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
expect(() =>
getNamedBuilder()
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall])
.toJSON(),
).not.toThrowError();
expect(() =>
getNamedBuilder()
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
.toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
// @ts-expect-error: Invalid integration types
expect(() => getNamedBuilder().setIntegrationTypes(999).toJSON()).toThrowError();
// @ts-expect-error: Invalid integration types
expect(() => getNamedBuilder().setIntegrationTypes([999, 998]).toJSON()).toThrowError();
});
});
});
});

View File

@@ -1,31 +1,74 @@
import {
ApplicationCommandType,
ApplicationIntegrationType,
InteractionContextType,
PermissionFlagsBits,
} from 'discord-api-types/v10';
import { ApplicationIntegrationType, InteractionContextType, PermissionFlagsBits } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { MessageContextCommandBuilder } from '../../src/index.js';
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index.js';
const getBuilder = () => new MessageContextCommandBuilder();
const getBuilder = () => new ContextMenuCommandBuilder();
describe('Context Menu Commands', () => {
describe('Assertions tests', () => {
test('GIVEN valid name THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateName('ping')).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => ContextMenuCommandAssertions.validateName(null)).toThrowError();
// Too short of a name
expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError();
// Invalid characters used
expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError();
// Too long of a name
expect(() =>
ContextMenuCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
).toThrowError();
});
test('GIVEN valid type THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateType(3)).not.toThrowError();
});
test('GIVEN invalid type THEN throw error', () => {
expect(() => ContextMenuCommandAssertions.validateType(null)).toThrowError();
// Out of range
expect(() => ContextMenuCommandAssertions.validateType(1)).toThrowError();
});
test('GIVEN valid required parameters THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateRequiredParameters('owo', 2)).not.toThrowError();
});
test('GIVEN valid default_permission THEN does not throw error', () => {
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
});
test('GIVEN invalid default_permission THEN throw error', () => {
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(null)).toThrowError();
});
});
describe('ContextMenuCommandBuilder', () => {
describe('Builder tests', () => {
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
expect(() => getBuilder().toJSON()).toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('$$$').toJSON()).toThrowError();
test('GIVEN valid builder THEN does not throw error', () => {
expect(() => getBuilder().setName('example').setType(3).toJSON()).not.toThrowError();
});
expect(() => getBuilder().setName(' ').toJSON()).toThrowError();
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('$$$')).toThrowError();
expect(() => getBuilder().setName(' ')).toThrowError();
});
test('GIVEN valid names THEN does not throw error', () => {
expect(() => getBuilder().setName('hi_there').toJSON()).not.toThrowError();
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
expect(() => getBuilder().setName('A COMMAND').toJSON()).not.toThrowError();
expect(() => getBuilder().setName('A COMMAND')).not.toThrowError();
// Translation: a_command
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
@@ -33,6 +76,20 @@ describe('Context Menu Commands', () => {
// Translation: thx (according to GTranslate)
expect(() => getBuilder().setName('どうも')).not.toThrowError();
});
test('GIVEN valid types THEN does not throw error', () => {
expect(() => getBuilder().setType(2)).not.toThrowError();
expect(() => getBuilder().setType(3)).not.toThrowError();
});
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
});
test('GIVEN valid builder with dmPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDMPermission(false)).not.toThrowError();
});
});
describe('Context menu command localizations', () => {
@@ -49,22 +106,19 @@ describe('Context Menu Commands', () => {
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError();
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
});
test('GIVEN valid name localizations THEN valid data is stored', () => {
expect(getBuilder().setName('hi').setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual(
expectedSingleLocale,
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
expectedMultipleLocales,
);
expect(
getBuilder().setName('hi').setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON()
.name_localizations,
).toEqual(expectedMultipleLocales);
expect(getBuilder().setName('hi').clearNameLocalizations().toJSON().name_localizations).toBeUndefined();
expect(getBuilder().setName('hi').clearNameLocalization('en-US').toJSON().name_localizations).toEqual({
'en-US': undefined,
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
'en-US': null,
});
});
});
@@ -80,10 +134,14 @@ describe('Context Menu Commands', () => {
).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getBuilder().setName('hi').setDefaultMemberPermissions('1.1').toJSON()).toThrowError();
test('GIVEN null permissions THEN does not throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
});
expect(() => getBuilder().setName('hi').setDefaultMemberPermissions(1.1).toJSON()).toThrowError();
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
});
});
@@ -100,10 +158,10 @@ describe('Context Menu Commands', () => {
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setName('hi').setContexts(999).toJSON()).toThrowError();
expect(() => getBuilder().setContexts(999)).toThrowError();
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setName('hi').setContexts([999, 998]).toJSON()).toThrowError();
expect(() => getBuilder().setContexts([999, 998])).toThrowError();
});
});
@@ -126,10 +184,10 @@ describe('Context Menu Commands', () => {
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setName('hi').setIntegrationTypes(999).toJSON()).toThrowError();
expect(() => getBuilder().setIntegrationTypes(999)).toThrowError();
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setName('hi').setIntegrationTypes([999, 998]).toJSON()).toThrowError();
expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError();
});
});
});

View File

@@ -13,32 +13,32 @@ import {
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
ChatInputCommandAttachmentOption,
ChatInputCommandBooleanOption,
ChatInputCommandChannelOption,
ChatInputCommandIntegerOption,
ChatInputCommandMentionableOption,
ChatInputCommandNumberOption,
ChatInputCommandRoleOption,
ChatInputCommandStringOption,
ChatInputCommandUserOption,
SlashCommandAttachmentOption,
SlashCommandBooleanOption,
SlashCommandChannelOption,
SlashCommandIntegerOption,
SlashCommandMentionableOption,
SlashCommandNumberOption,
SlashCommandRoleOption,
SlashCommandStringOption,
SlashCommandUserOption,
} from '../../../src/index.js';
const getBooleanOption = () =>
new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getChannelOption = () =>
new ChatInputCommandChannelOption()
new SlashCommandChannelOption()
.setName('owo')
.setDescription('Testing 123')
.setRequired(true)
.addChannelTypes(ChannelType.GuildText);
const getStringOption = () =>
new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getIntegerOption = () =>
new ChatInputCommandIntegerOption()
new SlashCommandIntegerOption()
.setName('owo')
.setDescription('Testing 123')
.setRequired(true)
@@ -46,24 +46,22 @@ const getIntegerOption = () =>
.setMaxValue(10);
const getNumberOption = () =>
new ChatInputCommandNumberOption()
new SlashCommandNumberOption()
.setName('owo')
.setDescription('Testing 123')
.setRequired(true)
.setMinValue(-1.23)
.setMaxValue(10);
const getUserOption = () =>
new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getRoleOption = () =>
new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getMentionableOption = () =>
new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
const getAttachmentOption = () =>
new ChatInputCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true);
new SlashCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true);
describe('Application Command toJSON() results', () => {
test('GIVEN a boolean option THEN calling toJSON should return a valid JSON', () => {
@@ -103,6 +101,7 @@ describe('Application Command toJSON() results', () => {
max_value: 10,
min_value: -1,
autocomplete: true,
// TODO
choices: [],
});

View File

@@ -0,0 +1,588 @@
import {
ApplicationIntegrationType,
ChannelType,
InteractionContextType,
PermissionFlagsBits,
type APIApplicationCommandOptionChoice,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
SlashCommandAssertions,
SlashCommandBooleanOption,
SlashCommandBuilder,
SlashCommandChannelOption,
SlashCommandIntegerOption,
SlashCommandMentionableOption,
SlashCommandNumberOption,
SlashCommandRoleOption,
SlashCommandAttachmentOption,
SlashCommandStringOption,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
SlashCommandUserOption,
} from '../../../src/index.js';
const largeArray = Array.from({ length: 26 }, () => 1 as unknown as APIApplicationCommandOptionChoice);
const getBuilder = () => new SlashCommandBuilder();
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
const getIntegerOption = () => new SlashCommandIntegerOption().setName('owo').setDescription('Testing 123');
const getNumberOption = () => new SlashCommandNumberOption().setName('owo').setDescription('Testing 123');
const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123');
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123');
const getChannelOption = () => new SlashCommandChannelOption().setName('owo').setDescription('Testing 123');
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123');
const getAttachmentOption = () => new SlashCommandAttachmentOption().setName('owo').setDescription('Testing 123');
const getMentionableOption = () => new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123');
const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
class Collection {
public readonly [Symbol.toStringTag] = 'Map';
}
describe('Slash Commands', () => {
describe('Assertions tests', () => {
test('GIVEN valid name THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateName('ping')).not.toThrowError();
expect(() => SlashCommandAssertions.validateName('hello-world_command')).not.toThrowError();
expect(() => SlashCommandAssertions.validateName('aˇ㐆1٢〣²अก')).not.toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => SlashCommandAssertions.validateName(null)).toThrowError();
// Too short of a name
expect(() => SlashCommandAssertions.validateName('')).toThrowError();
// Invalid characters used
expect(() => SlashCommandAssertions.validateName('ABC')).toThrowError();
expect(() => SlashCommandAssertions.validateName('ABC123$%^&')).toThrowError();
expect(() => SlashCommandAssertions.validateName('help ping')).toThrowError();
expect(() => SlashCommandAssertions.validateName('🦦')).toThrowError();
// Too long of a name
expect(() =>
SlashCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
).toThrowError();
});
test('GIVEN valid description THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateDescription('This is an OwO moment fur sure!~')).not.toThrowError();
});
test('GIVEN invalid description THEN throw error', () => {
expect(() => SlashCommandAssertions.validateDescription(null)).toThrowError();
// Too short of a description
expect(() => SlashCommandAssertions.validateDescription('')).toThrowError();
// Too long of a description
expect(() =>
SlashCommandAssertions.validateDescription(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magnam autem libero expedita vitae accusamus nostrum ipsam tempore repudiandae deserunt ipsum facilis, velit fugiat facere accusantium, explicabo corporis aliquam non quos.',
),
).toThrowError();
});
test('GIVEN valid default_permission THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
});
test('GIVEN invalid default_permission THEN throw error', () => {
expect(() => SlashCommandAssertions.validateDefaultPermission(null)).toThrowError();
});
test('GIVEN valid array of options or choices THEN does not throw error', () => {
expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError();
expect(() => SlashCommandAssertions.validateChoicesLength(25)).not.toThrowError();
expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError();
});
test('GIVEN invalid options or choices THEN throw error', () => {
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
// Given an array that's too big
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError();
});
test('GIVEN valid required parameters THEN does not throw error', () => {
expect(() =>
SlashCommandAssertions.validateRequiredParameters(
'owo',
'My fancy command that totally exists, to test assertions',
[],
),
).not.toThrowError();
});
});
describe('SlashCommandBuilder', () => {
describe('Builder with no options', () => {
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
expect(() => getBuilder().toJSON()).toThrowError();
});
test('GIVEN valid builder THEN does not throw error', () => {
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
});
});
describe('Builder with simple options', () => {
test('GIVEN valid builder with options THEN does not throw error', () => {
expect(() =>
getBuilder()
.setName('example')
.setDescription('Example command')
.setDMPermission(false)
.addBooleanOption((boolean) =>
boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true),
)
.addChannelOption((channel) => channel.setName('iscool').setDescription('Are we cool or what?'))
.addMentionableOption((mentionable) => mentionable.setName('iscool').setDescription('Are we cool or what?'))
.addRoleOption((role) => role.setName('iscool').setDescription('Are we cool or what?'))
.addUserOption((user) => user.setName('iscool').setDescription('Are we cool or what?'))
.addIntegerOption((integer) =>
integer
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices({ name: 'Very cool', value: 1_000 })
.addChoices([{ name: 'Even cooler', value: 2_000 }]),
)
.addNumberOption((number) =>
number
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices({ name: 'Very cool', value: 1.5 })
.addChoices([{ name: 'Even cooler', value: 2.5 }]),
)
.addStringOption((string) =>
string
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' })
.addChoices([{ name: 'The Whole shebang', value: 'all' }]),
)
.addIntegerOption((integer) =>
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addNumberOption((number) =>
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.addStringOption((string) =>
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
)
.toJSON(),
).not.toThrowError();
});
test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addStringOption(getStringOption().setAutocomplete('not a boolean'))).toThrowError();
});
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
expect(() =>
getBuilder().addStringOption(
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
),
).toThrowError();
expect(() =>
getBuilder().addStringOption(
getStringOption()
.setAutocomplete(true)
.addChoices(
{ name: 'Fancy Pants', value: 'fp_1' },
{ name: 'Fancy Shoes', value: 'fs_1' },
{ name: 'The Whole shebang', value: 'all' },
),
),
).toThrowError();
expect(() =>
getBuilder().addStringOption(
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
),
).toThrowError();
expect(() => {
const option = getStringOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
expect(() => {
const option = getNumberOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
expect(() => {
const option = getIntegerOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
expect(() =>
getBuilder().addChannelOption(
getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]),
),
).not.toThrowError();
expect(() => {
getBuilder().addChannelOption(
getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText),
);
}).not.toThrowError();
});
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
// @ts-expect-error: Invalid channel type
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100))).toThrowError();
// @ts-expect-error: Invalid channel types
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError();
});
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
// @ts-expect-error: Invalid max value
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue('test'))).toThrowError();
// @ts-expect-error: Invalid max value
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue('test'))).toThrowError();
// @ts-expect-error: Invalid min value
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue('test'))).toThrowError();
// @ts-expect-error: Invalid min value
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue('test'))).toThrowError();
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5))).toThrowError();
});
test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => {
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1))).not.toThrowError();
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue(1.5))).not.toThrowError();
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue(1))).not.toThrowError();
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue(1.5))).not.toThrowError();
});
test('GIVEN an already built builder THEN does not throw an error', () => {
expect(() => getBuilder().addStringOption(getStringOption())).not.toThrowError();
expect(() => getBuilder().addIntegerOption(getIntegerOption())).not.toThrowError();
expect(() => getBuilder().addNumberOption(getNumberOption())).not.toThrowError();
expect(() => getBuilder().addBooleanOption(getBooleanOption())).not.toThrowError();
expect(() => getBuilder().addUserOption(getUserOption())).not.toThrowError();
expect(() => getBuilder().addChannelOption(getChannelOption())).not.toThrowError();
expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError();
expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError();
expect(() => getBuilder().addMentionableOption(getMentionableOption())).not.toThrowError();
});
test('GIVEN no valid return for an addOption method THEN throw error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(getRoleOption())).toThrowError();
});
test('GIVEN invalid name THEN throw error', () => {
expect(() => getBuilder().setName('TEST_COMMAND')).toThrowError();
expect(() => getBuilder().setName('ĂĂĂĂĂĂ')).toThrowError();
});
test('GIVEN valid names THEN does not throw error', () => {
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
// Translation: a_command
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
// Translation: thx (according to GTranslate)
expect(() => getBuilder().setName('どうも')).not.toThrowError();
});
test('GIVEN invalid returns for builder THEN throw error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(true)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(null)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(undefined)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(() => SlashCommandStringOption)).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(() => new Collection())).toThrowError();
});
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
});
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
expect(() =>
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()),
).not.toThrowError();
});
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
expect(() =>
getBuilder().addStringOption(
getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }),
),
).toThrowError();
});
test('GIVEN an option, THEN setting choices should not throw an error', () => {
expect(() =>
getBuilder().addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })),
).not.toThrowError();
});
test('GIVEN valid builder with NSFW, THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDescription('foo').setNSFW(true)).not.toThrowError();
});
});
describe('Builder with subcommand (group) options', () => {
test('GIVEN builder with subcommand group THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommandGroup((group) => group.setName('group').setDescription('Group us together!')),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommand((subcommand) =>
subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'),
),
).not.toThrowError();
});
test('GIVEN builder with subcommand THEN has regular slash command fields', () => {
expect(() =>
getBuilder()
.setName('name')
.setDescription('description')
.addSubcommand((option) => option.setName('ye').setDescription('ye'))
.addSubcommand((option) => option.setName('no').setDescription('no'))
.setDMPermission(false)
.setDefaultMemberPermissions(1n),
).not.toThrowError();
});
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
expect(() => getNamedBuilder().addSubcommandGroup(getSubcommandGroup())).not.toThrowError();
});
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
expect(() => getNamedBuilder().addSubcommand(getSubcommand())).not.toThrowError();
});
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
expect(() =>
getNamedBuilder().addSubcommand(getSubcommand().addBooleanOption(getBooleanOption())),
).not.toThrowError();
});
test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => {
expect(() =>
// @ts-expect-error: Checking if check works JS-side too
getNamedBuilder().addSubcommand(getSubcommand()).addInteger(getInteger()),
).toThrowError();
});
test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addSubcommandGroup()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addSubcommand()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addSubcommand(getSubcommandGroup())).toThrowError();
});
});
describe('Subcommand group builder', () => {
test('GIVEN no valid subcommand THEN throw error', () => {
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getSubcommandGroup().addSubcommand()).toThrowError();
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
expect(() => getSubcommandGroup().addSubcommand(getSubcommandGroup())).toThrowError();
});
test('GIVEN a valid subcommand THEN does not throw an error', () => {
expect(() =>
getSubcommandGroup()
.addSubcommand((sub) => sub.setName('sub').setDescription('Testing 123'))
.toJSON(),
).not.toThrowError();
});
});
describe('Subcommand builder', () => {
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError();
});
});
describe('Slash command localizations', () => {
const expectedSingleLocale = { 'en-US': 'foobar' };
const expectedMultipleLocales = {
...expectedSingleLocale,
bg: 'test',
};
test('GIVEN valid name localizations THEN does not throw error', () => {
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error: Invalid localization
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
});
test('GIVEN valid name localizations THEN valid data is stored', () => {
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
expectedMultipleLocales,
);
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
'en-US': null,
});
});
test('GIVEN valid description localizations THEN does not throw error', () => {
expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError();
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN invalid description localizations THEN does throw error', () => {
// @ts-expect-error: Invalid localization description
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error: Invalid localization description
expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError();
});
test('GIVEN valid description localizations THEN valid data is stored', () => {
expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual(
expectedSingleLocale,
);
expect(
getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations,
).toEqual(expectedMultipleLocales);
expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull();
expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({
'en-US': null,
});
});
});
describe('permissions', () => {
test('GIVEN valid permission string THEN does not throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
});
test('GIVEN valid permission bitfield THEN does not throw error', () => {
expect(() =>
getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles),
).not.toThrowError();
});
test('GIVEN null permissions THEN does not throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
});
test('GIVEN valid permission with options THEN does not throw error', () => {
expect(() =>
getBuilder().addBooleanOption(getBooleanOption()).setDefaultMemberPermissions('1'),
).not.toThrowError();
expect(() => getBuilder().addChannelOption(getChannelOption()).setDMPermission(false)).not.toThrowError();
});
});
describe('contexts', () => {
test('GIVEN a builder with valid contexts THEN does not throw an error', () => {
expect(() =>
getBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]),
).not.toThrowError();
expect(() =>
getBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM),
).not.toThrowError();
});
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setContexts(999)).toThrowError();
// @ts-expect-error: Invalid contexts
expect(() => getBuilder().setContexts([999, 998])).toThrowError();
});
});
describe('integration types', () => {
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
expect(() =>
getBuilder().setIntegrationTypes([
ApplicationIntegrationType.GuildInstall,
ApplicationIntegrationType.UserInstall,
]),
).not.toThrowError();
expect(() =>
getBuilder().setIntegrationTypes(
ApplicationIntegrationType.GuildInstall,
ApplicationIntegrationType.UserInstall,
),
).not.toThrowError();
});
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setIntegrationTypes(999)).toThrowError();
// @ts-expect-error: Invalid integration types
expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError();
});
});
});
});

View File

@@ -1,21 +1,71 @@
import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
import {
ComponentType,
TextInputStyle,
type APIModalInteractionResponseCallbackData,
type APITextInputComponent,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js';
import {
ActionRowBuilder,
ButtonBuilder,
ModalBuilder,
TextInputBuilder,
type ModalActionRowComponentBuilder,
} from '../../src/index.js';
import {
componentsValidator,
titleValidator,
validateRequiredParameters,
} from '../../src/interactions/modals/Assertions.js';
const modal = () => new ModalBuilder();
const textInput = () =>
new ActionRowBuilder().addTextInputComponent(
new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short),
);
describe('Modals', () => {
describe('Assertion Tests', () => {
test('GIVEN valid title THEN validator does not throw', () => {
expect(() => titleValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid title THEN validator does throw', () => {
expect(() => titleValidator.parse(42)).toThrowError();
});
test('GIVEN valid components THEN validator does not throw', () => {
expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError();
});
test('GIVEN invalid components THEN validator does throw', () => {
expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError();
});
test('GIVEN valid required parameters THEN validator does not throw', () => {
expect(() =>
validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]),
).not.toThrowError();
});
test('GIVEN invalid required parameters THEN validator does throw', () => {
expect(() =>
// @ts-expect-error: Missing required parameter
validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]),
).toThrowError();
});
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError();
expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError();
expect(() =>
modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()),
).not.toThrowError();
expect(() =>
// @ts-expect-error: You can pass a TextInputBuilder and it will add it to an action row
modal().setTitle('test').setCustomId('foobar').addComponents(new TextInputBuilder()),
).not.toThrowError();
});
test('GIVEN invalid fields THEN builder does throw', () => {
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
// @ts-expect-error: CustomId is invalid
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
});
@@ -56,17 +106,68 @@ describe('Modals', () => {
modal()
.setTitle(modalData.title)
.setCustomId('custom id')
.setActionRows(
new ActionRowBuilder().addTextInputComponent(
.setComponents(
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
)
.addActionRows([
new ActionRowBuilder().addTextInputComponent(
.addComponents([
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
])
.toJSON(),
).toEqual(modalData);
});
describe('equals()', () => {
const textInput1 = new TextInputBuilder()
.setCustomId('custom id')
.setLabel('label')
.setStyle(TextInputStyle.Paragraph);
const textInput2: APITextInputComponent = {
type: ComponentType.TextInput,
custom_id: 'custom id',
label: 'label',
style: TextInputStyle.Paragraph,
};
test('GIVEN equal builders THEN returns true', () => {
const equalTextInput = new TextInputBuilder()
.setCustomId('custom id')
.setLabel('label')
.setStyle(TextInputStyle.Paragraph);
expect(textInput1.equals(equalTextInput)).toBeTruthy();
});
test('GIVEN the same builder THEN returns true', () => {
expect(textInput1.equals(textInput1)).toBeTruthy();
});
test('GIVEN equal builder and data THEN returns true', () => {
expect(textInput1.equals(textInput2)).toBeTruthy();
});
test('GIVEN different builders THEN returns false', () => {
const diffTextInput = new TextInputBuilder()
.setCustomId('custom id')
.setLabel('label 2')
.setStyle(TextInputStyle.Paragraph);
expect(textInput1.equals(diffTextInput)).toBeFalsy();
});
test('GIVEN different text input builder and data THEN returns false', () => {
const diffTextInputData: APITextInputComponent = {
type: ComponentType.TextInput,
custom_id: 'custom id',
label: 'label 2',
style: TextInputStyle.Short,
};
expect(textInput1.equals(diffTextInputData)).toBeFalsy();
});
});
});

View File

@@ -3,16 +3,6 @@ import { EmbedBuilder, embedLength } from '../../src/index.js';
const alpha = 'abcdefghijklmnopqrstuvwxyz';
const dummy = {
title: 'ooooo aaaaa uuuuuu aaaa',
};
const base = {
author: undefined,
fields: [],
footer: undefined,
};
describe('Embed', () => {
describe('Embed getters', () => {
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
@@ -24,136 +14,127 @@ describe('Embed', () => {
footer: { text: alpha },
});
expect(embedLength(embed.toJSON())).toEqual(alpha.length * 6);
expect(embedLength(embed.data)).toEqual(alpha.length * 6);
});
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
const embed = new EmbedBuilder();
expect(embedLength(embed.toJSON(false))).toEqual(0);
expect(embedLength(embed.data)).toEqual(0);
});
});
describe('Embed title', () => {
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
});
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setTitle('foo');
expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ title: 'foo', description: ':3' });
embed.clearTitle();
const embed = new EmbedBuilder({ title: 'foo' });
embed.setTitle(null);
expect(embed.toJSON()).toStrictEqual({ ...base, description: ':3', title: undefined });
expect(embed.toJSON()).toStrictEqual({ title: undefined });
});
test('GIVEN an embed with an invalid title THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setTitle('a'.repeat(257));
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
});
});
describe('Embed description', () => {
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
});
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setDescription('foo');
expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
});
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ description: 'foo', ...dummy });
embed.clearDescription();
const embed = new EmbedBuilder({ description: 'foo' });
embed.setDescription(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, description: undefined });
expect(embed.toJSON()).toStrictEqual({ description: undefined });
});
test('GIVEN an embed with an invalid description THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setDescription('a'.repeat(4_097));
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setDescription('a'.repeat(4_097))).toThrowError();
});
});
describe('Embed URL', () => {
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ url: 'https://discord.js.org/', ...dummy });
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
expect(embed.toJSON()).toStrictEqual({
...base,
...dummy,
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder(dummy);
const embed = new EmbedBuilder();
embed.setURL('https://discord.js.org/');
expect(embed.toJSON()).toStrictEqual({
...base,
...dummy,
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ url: 'https://discord.js.org', ...dummy });
embed.clearURL();
const embed = new EmbedBuilder({ url: 'https://discord.js.org' });
embed.setURL(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, url: undefined });
expect(embed.toJSON()).toStrictEqual({ url: undefined });
});
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
const embed = new EmbedBuilder();
embed.setURL(input);
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setURL(input)).toThrowError();
});
});
describe('Embed Color', () => {
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ color: 0xff0000, ...dummy });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 });
const embed = new EmbedBuilder({ color: 0xff0000 });
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
});
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
expect(new EmbedBuilder(dummy).setColor(0xff0000).toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 });
expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
});
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ ...dummy, color: 0xff0000 });
embed.clearColor();
const embed = new EmbedBuilder({ color: 0xff0000 });
embed.setColor(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: undefined });
expect(embed.toJSON()).toStrictEqual({ color: undefined });
});
test('GIVEN an embed with an invalid color THEN throws error', () => {
const embed = new EmbedBuilder();
// @ts-expect-error: Invalid color
embed.setColor('RED');
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setColor('RED')).toThrowError();
// @ts-expect-error: Invalid color
embed.setColor([42, 36]);
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setColor([42, 36])).toThrowError();
expect(() => embed.setColor([42, 36, 1_000])).toThrowError();
});
});
@@ -161,92 +142,98 @@ describe('Embed', () => {
const now = new Date();
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy });
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder(dummy);
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setTimestamp(now);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder(dummy);
const embed = new EmbedBuilder();
embed.setTimestamp(now.getTime());
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder(dummy);
const embed = new EmbedBuilder();
embed.setTimestamp();
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: embed.toJSON().timestamp });
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp });
});
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy });
embed.clearTimestamp();
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
embed.setTimestamp(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: undefined });
expect(embed.toJSON()).toStrictEqual({ timestamp: undefined });
});
});
describe('Embed Thumbnail', () => {
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setThumbnail('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, ...dummy });
embed.clearThumbnail();
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
embed.setThumbnail(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, thumbnail: undefined });
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
});
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setThumbnail('owo');
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setThumbnail('owo')).toThrowError();
});
});
describe('Embed Image', () => {
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
image: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setImage('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
image: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' }, ...dummy });
embed.clearImage();
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
embed.setImage(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, image: undefined });
expect(embed.toJSON()).toStrictEqual({ image: undefined });
});
test('GIVEN an embed with an invalid image THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setImage('owo');
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setImage('owo')).toThrowError();
});
});
@@ -256,19 +243,19 @@ describe('Embed', () => {
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
expect(embed.toJSON()).toStrictEqual({
...base,
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setAuthor((author) =>
author.setName('Wumpus').setIconURL('https://discord.js.org/static/logo.svg').setURL('https://discord.js.org'),
);
embed.setAuthor({
name: 'Wumpus',
iconURL: 'https://discord.js.org/static/logo.svg',
url: 'https://discord.js.org',
});
expect(embed.toJSON()).toStrictEqual({
...base,
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
});
@@ -276,18 +263,16 @@ describe('Embed', () => {
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
...dummy,
});
embed.clearAuthor();
embed.setAuthor(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, author: undefined });
expect(embed.toJSON()).toStrictEqual({ author: undefined });
});
test('GIVEN an embed with an invalid author name THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setAuthor({ name: 'a'.repeat(257) });
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
});
});
@@ -297,36 +282,32 @@ describe('Embed', () => {
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({
...base,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setFooter({ text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' });
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
expect(embed.toJSON()).toStrictEqual({
...base,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
const embed = new EmbedBuilder({
...dummy,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
embed.clearFooter();
embed.setFooter(null);
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, footer: undefined });
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
});
test('GIVEN an embed with invalid footer text THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setFooter({ text: 'a'.repeat(2_049) });
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.setFooter({ text: 'a'.repeat(2_049) })).toThrowError();
});
});
@@ -335,7 +316,9 @@ describe('Embed', () => {
const embed = new EmbedBuilder({
fields: [{ name: 'foo', value: 'bar' }],
});
expect(embed.toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'bar' }] });
expect(embed.toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'bar' }],
});
});
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
@@ -344,7 +327,6 @@ describe('Embed', () => {
embed.addFields([{ name: 'foo', value: 'bar' }]);
expect(embed.toJSON()).toStrictEqual({
...base,
fields: [
{ name: 'foo', value: 'bar' },
{ name: 'foo', value: 'bar' },
@@ -356,51 +338,56 @@ describe('Embed', () => {
const embed = new EmbedBuilder();
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'baz' }] });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'baz' }],
});
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => {
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).not.toThrowError();
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
});
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
});
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).not.toThrowError();
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).not.toThrowError();
expect(() =>
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
expect(() =>
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
});
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
const embed = new EmbedBuilder();
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
expect(() =>
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
});
describe('GIVEN invalid field amount THEN throws error', () => {
test('1', () => {
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
expect(() => embed.toJSON()).toThrowError();
expect(() =>
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
});
});
@@ -408,8 +395,7 @@ describe('Embed', () => {
test('2', () => {
const embed = new EmbedBuilder();
embed.addFields({ name: '', value: 'bar' });
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
});
});
@@ -417,8 +403,7 @@ describe('Embed', () => {
test('3', () => {
const embed = new EmbedBuilder();
embed.addFields({ name: 'a'.repeat(257), value: 'bar' });
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
});
});
@@ -426,8 +411,7 @@ describe('Embed', () => {
test('4', () => {
const embed = new EmbedBuilder();
embed.addFields({ name: '', value: 'a'.repeat(1_025) });
expect(() => embed.toJSON()).toThrowError();
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError();
});
});
});

View File

@@ -1,21 +1,17 @@
import { expectTypeOf } from 'vitest';
import {
ChatInputCommandBuilder,
ChatInputCommandStringOption,
ChatInputCommandSubcommandBuilder,
} from '../src/index.js';
import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandSubcommandBuilder } from '../src/index.js';
const getBuilder = () => new ChatInputCommandBuilder();
const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
const getBuilder = () => new SlashCommandBuilder();
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
type BuilderPropsOnly<Type = ChatInputCommandBuilder> = Pick<
type BuilderPropsOnly<Type = SlashCommandBuilder> = Pick<
Type,
keyof {
[Key in keyof Type as Type[Key] extends (...args: any) => any ? never : Key]: any;
}
>;
expectTypeOf(getBuilder().addStringOptions(getStringOption())).toMatchTypeOf<BuilderPropsOnly>();
expectTypeOf(getBuilder().addStringOption(getStringOption())).toMatchTypeOf<BuilderPropsOnly>();
expectTypeOf(getBuilder().addSubcommands(getSubcommand())).toMatchTypeOf<BuilderPropsOnly>();
expectTypeOf(getBuilder().addSubcommand(getSubcommand())).toMatchTypeOf<BuilderPropsOnly>();

View File

@@ -5,16 +5,13 @@ header = """
All notable changes to this project will be documented in this file.\n
"""
body = """
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
{% if version %}\
# [{{ version | trim_start_matches(pat="v") }}]\
{% if previous %}\
{% if previous.version %}\
({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
(https://github.com/discordjs/discord.js/compare/{{ previous.version }}...{{ version }})\
{% else %}\
({{ self::remote_url() }}/tree/{{ version }})\
(https://github.com/discordjs/discord.js/tree/{{ version }})\
{% endif %}\
{% endif %} \
- ({{ timestamp | date(format="%Y-%m-%d") }})
@@ -27,23 +24,14 @@ body = """
- {% if commit.scope %}\
**{{commit.scope}}:** \
{% endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/discordjs/discord.js/commit/{{ commit.id }}))\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}
{% endfor %}\
{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
\n### New Contributors\n
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
* @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
{% endfor %}\
{% endif %}\n
{% endfor %}\n
"""
trim = true
footer = ""
@@ -71,9 +59,5 @@ commit_parsers = [
filter_commits = true
tag_pattern = "@discordjs/builders@[0-9]*"
ignore_tags = ""
topo_order = false
topo_order = true
sort_commits = "newest"
[remote.github]
owner = "discordjs"
repo = "discord.js"

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@discordjs/builders",
"version": "1.9.0",
"version": "1.8.2",
"description": "A set of builders that you can use when creating your bot",
"scripts": {
"test": "vitest run",
@@ -38,7 +38,7 @@
"dist"
],
"contributors": [
"Vlad Frangu <me@vladfrangu.dev>",
"Vlad Frangu <kingdgrizzle@gmail.com>",
"Crawl <icrawltogo@gmail.com>",
"Amish Shah <amishshah.2k@gmail.com>",
"SpaceEEC <spaceeec@yahoo.com>",
@@ -65,32 +65,33 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@discordjs/formatters": "workspace:^",
"@discordjs/util": "workspace:^",
"discord-api-types": "^0.37.103",
"@sapphire/shapeshift": "^3.9.7",
"discord-api-types": "^0.37.119",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3",
"zod": "^3.23.8",
"zod-validation-error": "^3.4.0"
"tslib": "^2.6.2"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
"@favware/cliff-jumper": "^4.1.0",
"@types/node": "^18.19.44",
"@vitest/coverage-v8": "^2.0.5",
"@favware/cliff-jumper": "^3.0.3",
"@types/node": "16.18.60",
"@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"esbuild-plugin-version-injector": "^1.2.1",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.62",
"eslint-formatter-pretty": "^6.0.1",
"prettier": "^3.3.3",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "~5.5.4",
"vitest": "^2.0.5"
"prettier": "^3.3.0",
"tsup": "^8.1.0",
"turbo": "^1.13.3",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
},
"engines": {
"node": ">=18"
"node": ">=16.11.0"
},
"publishConfig": {
"access": "public",

View File

@@ -1,20 +0,0 @@
import { Locale } from 'discord-api-types/v10';
import { z } from 'zod';
export const customIdPredicate = z.string().min(1).max(100);
export const memberPermissionsPredicate = z.coerce.bigint();
export const localeMapPredicate = z
.object(
Object.fromEntries(Object.values(Locale).map((loc) => [loc, z.string().optional()])) as Record<
Locale,
z.ZodOptional<z.ZodString>
>,
)
.strict();
export const refineURLPredicate = (allowedProtocols: string[]) => (value: string) => {
const url = new URL(value);
return allowedProtocols.includes(url.protocol);
};

View File

@@ -1,60 +1,68 @@
/* eslint-disable jsdoc/check-param-names */
import type {
APITextInputComponent,
APIActionRowComponent,
APIActionRowComponentTypes,
APIChannelSelectComponent,
APIMentionableSelectComponent,
APIRoleSelectComponent,
APIStringSelectComponent,
APIUserSelectComponent,
APIButtonComponentWithCustomId,
APIButtonComponentWithSKUId,
APIButtonComponentWithURL,
} from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
import { resolveBuilder } from '../util/resolveBuilder.js';
import { validate } from '../util/validation.js';
import { actionRowPredicate } from './Assertions.js';
import { ComponentBuilder } from './Component.js';
import type { AnyActionRowComponentBuilder } from './Components.js';
import { createComponentBuilder } from './Components.js';
import {
DangerButtonBuilder,
PrimaryButtonBuilder,
SecondaryButtonBuilder,
SuccessButtonBuilder,
} from './button/CustomIdButton.js';
import { LinkButtonBuilder } from './button/LinkButton.js';
import { PremiumButtonBuilder } from './button/PremiumButton.js';
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import { TextInputBuilder } from './textInput/TextInput.js';
type APIActionRowComponent,
ComponentType,
type APIMessageActionRowComponent,
type APIModalActionRowComponent,
type APIActionRowComponentTypes,
} from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
import { ComponentBuilder } from './Component.js';
import { createComponentBuilder } from './Components.js';
import type { ButtonBuilder } from './button/Button.js';
import type { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
import type { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
import type { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import type { TextInputBuilder } from './textInput/TextInput.js';
export interface ActionRowBuilderData
extends Partial<Omit<APIActionRowComponent<APIActionRowComponentTypes>, 'components'>> {
components: AnyActionRowComponentBuilder[];
}
/**
* The builders that may be used for messages.
*/
export type MessageComponentBuilder =
| ActionRowBuilder<MessageActionRowComponentBuilder>
| MessageActionRowComponentBuilder;
/**
* The builders that may be used for modals.
*/
export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder;
/**
* The builders that may be used within an action row for messages.
*/
export type MessageActionRowComponentBuilder =
| ButtonBuilder
| ChannelSelectMenuBuilder
| MentionableSelectMenuBuilder
| RoleSelectMenuBuilder
| StringSelectMenuBuilder
| UserSelectMenuBuilder;
/**
* The builders that may be used within an action row for modals.
*/
export type ModalActionRowComponentBuilder = TextInputBuilder;
/**
* Any builder.
*/
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
/**
* A builder that creates API-compatible JSON data for action rows.
*
* @typeParam ComponentType - The types of components this action row holds
*/
export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIActionRowComponentTypes>> {
private readonly data: ActionRowBuilderData;
export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends ComponentBuilder<
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
> {
/**
* The components within this action row.
*/
public get components(): readonly AnyActionRowComponentBuilder[] {
return this.data.components;
}
public readonly components: ComponentType[];
/**
* Creates a new action row from API data.
@@ -90,254 +98,38 @@ export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<API
* .addComponents(button2, button3);
* ```
*/
public constructor({ components = [], ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
super();
this.data = {
...structuredClone(data),
type: ComponentType.ActionRow,
components: components.map((component) => createComponentBuilder(component)),
};
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
super({ type: ComponentType.ActionRow, ...data });
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[];
}
/**
* Adds primary button components to this action row.
* Adds components to this action row.
*
* @param input - The buttons to add
* @param components - The components to add
*/
public addPrimaryButtonComponents(
...input: RestOrArray<
APIButtonComponentWithCustomId | PrimaryButtonBuilder | ((builder: PrimaryButtonBuilder) => PrimaryButtonBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, PrimaryButtonBuilder));
this.data.components.push(...resolved);
public addComponents(...components: RestOrArray<ComponentType>) {
this.components.push(...normalizeArray(components));
return this;
}
/**
* Adds secondary button components to this action row.
* Sets components for this action row.
*
* @param input - The buttons to add
* @param components - The components to set
*/
public addSecondaryButtonComponents(
...input: RestOrArray<
| APIButtonComponentWithCustomId
| SecondaryButtonBuilder
| ((builder: SecondaryButtonBuilder) => SecondaryButtonBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, SecondaryButtonBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds success button components to this action row.
*
* @param input - The buttons to add
*/
public addSuccessButtonComponents(
...input: RestOrArray<
APIButtonComponentWithCustomId | SuccessButtonBuilder | ((builder: SuccessButtonBuilder) => SuccessButtonBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, SuccessButtonBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds danger button components to this action row.
*/
public addDangerButtonComponents(
...input: RestOrArray<
APIButtonComponentWithCustomId | DangerButtonBuilder | ((builder: DangerButtonBuilder) => DangerButtonBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, DangerButtonBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Generically add any type of component to this action row, only takes in an instance of a component builder.
*/
public addComponents(...input: RestOrArray<AnyActionRowComponentBuilder>): this {
const normalized = normalizeArray(input);
this.data.components.push(...normalized);
return this;
}
/**
* Adds SKU id button components to this action row.
*
* @param input - The buttons to add
*/
public addPremiumButtonComponents(
...input: RestOrArray<
APIButtonComponentWithSKUId | PremiumButtonBuilder | ((builder: PremiumButtonBuilder) => PremiumButtonBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, PremiumButtonBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds URL button components to this action row.
*
* @param input - The buttons to add
*/
public addLinkButtonComponents(
...input: RestOrArray<
APIButtonComponentWithURL | LinkButtonBuilder | ((builder: LinkButtonBuilder) => LinkButtonBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, LinkButtonBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds a channel select menu component to this action row.
*
* @param input - A function that returns a component builder or an already built builder
*/
public addChannelSelectMenuComponent(
input:
| APIChannelSelectComponent
| ChannelSelectMenuBuilder
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
): this {
this.data.components.push(resolveBuilder(input, ChannelSelectMenuBuilder));
return this;
}
/**
* Adds a mentionable select menu component to this action row.
*
* @param input - A function that returns a component builder or an already built builder
*/
public addMentionableSelectMenuComponent(
input:
| APIMentionableSelectComponent
| MentionableSelectMenuBuilder
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
): this {
this.data.components.push(resolveBuilder(input, MentionableSelectMenuBuilder));
return this;
}
/**
* Adds a role select menu component to this action row.
*
* @param input - A function that returns a component builder or an already built builder
*/
public addRoleSelectMenuComponent(
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
): this {
this.data.components.push(resolveBuilder(input, RoleSelectMenuBuilder));
return this;
}
/**
* Adds a string select menu component to this action row.
*
* @param input - A function that returns a component builder or an already built builder
*/
public addStringSelectMenuComponent(
input:
| APIStringSelectComponent
| StringSelectMenuBuilder
| ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder),
): this {
this.data.components.push(resolveBuilder(input, StringSelectMenuBuilder));
return this;
}
/**
* Adds a user select menu component to this action row.
*
* @param input - A function that returns a component builder or an already built builder
*/
public addUserSelectMenuComponent(
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
): this {
this.data.components.push(resolveBuilder(input, UserSelectMenuBuilder));
return this;
}
/**
* Adds a text input component to this action row.
*
* @param input - A function that returns a component builder or an already built builder
*/
public addTextInputComponent(
input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder),
): this {
this.data.components.push(resolveBuilder(input, TextInputBuilder));
return this;
}
/**
* Removes, replaces, or inserts components for this action row.
*
* @remarks
* This method behaves similarly
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
*
* It's useful for modifying and adjusting order of the already-existing components of an action row.
* @example
* Remove the first component:
* ```ts
* actionRow.spliceComponents(0, 1);
* ```
* @example
* Remove the first n components:
* ```ts
* const n = 4;
* actionRow.spliceComponents(0, n);
* ```
* @example
* Remove the last component:
* ```ts
* actionRow.spliceComponents(-1, 1);
* ```
* @param index - The index to start at
* @param deleteCount - The number of components to remove
* @param components - The replacing component objects
*/
public spliceComponents(index: number, deleteCount: number, ...components: AnyActionRowComponentBuilder[]): this {
this.data.components.splice(index, deleteCount, ...components);
public setComponents(...components: RestOrArray<ComponentType>) {
this.components.splice(0, this.components.length, ...normalizeArray(components));
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIActionRowComponent<APIActionRowComponentTypes> {
const { components, ...rest } = this.data;
const data = {
...structuredClone(rest),
components: components.map((component) => component.toJSON(validationOverride)),
};
validate(actionRowPredicate, data, validationOverride);
return data as APIActionRowComponent<APIActionRowComponentTypes>;
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>> {
return {
...this.data,
components: this.components.map((component) => component.toJSON()),
} as APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
}
}

View File

@@ -1,168 +1,116 @@
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
import { z } from 'zod';
import { customIdPredicate, refineURLPredicate } from '../Assertions.js';
import { s } from '@sapphire/shapeshift';
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
import { isValidationEnabled } from '../util/validation.js';
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
const labelPredicate = z.string().min(1).max(80);
export const customIdValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(100)
.setValidationEnabled(isValidationEnabled);
export const emojiPredicate = z
export const emojiValidator = s
.object({
id: z.string().optional(),
name: z.string().min(2).max(32).optional(),
animated: z.boolean().optional(),
id: s.string,
name: s.string,
animated: s.boolean,
})
.strict()
.refine((data) => data.id !== undefined || data.name !== undefined, {
message: "Either 'id' or 'name' must be provided",
});
.partial.strict.setValidationEnabled(isValidationEnabled);
const buttonPredicateBase = z.object({
type: z.literal(ComponentType.Button),
disabled: z.boolean().optional(),
});
export const disabledValidator = s.boolean;
const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
custom_id: customIdPredicate,
emoji: emojiPredicate.optional(),
label: labelPredicate,
});
export const buttonLabelValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(80)
.setValidationEnabled(isValidationEnabled);
const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }).strict();
const buttonSecondaryPredicate = buttonCustomIdPredicateBase
.extend({ style: z.literal(ButtonStyle.Secondary) })
.strict();
const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }).strict();
const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }).strict();
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
const buttonLinkPredicate = buttonPredicateBase
.extend({
style: z.literal(ButtonStyle.Link),
url: z
.string()
.url()
.refine(refineURLPredicate(['http:', 'https:', 'discord:'])),
emoji: emojiPredicate.optional(),
label: labelPredicate,
export const placeholderValidator = s.string.lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled);
export const minMaxValidator = s.number.int
.greaterThanOrEqual(0)
.lessThanOrEqual(25)
.setValidationEnabled(isValidationEnabled);
export const labelValueDescriptionValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(100)
.setValidationEnabled(isValidationEnabled);
export const jsonOptionValidator = s
.object({
label: labelValueDescriptionValidator,
value: labelValueDescriptionValidator,
description: labelValueDescriptionValidator.optional,
emoji: emojiValidator.optional,
default: s.boolean.optional,
})
.strict();
.setValidationEnabled(isValidationEnabled);
const buttonPremiumPredicate = buttonPredicateBase
.extend({
style: z.literal(ButtonStyle.Premium),
sku_id: z.string(),
export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
export const optionsValidator = optionValidator.array
.lengthGreaterThanOrEqual(0)
.setValidationEnabled(isValidationEnabled);
export const optionsLengthValidator = s.number.int
.greaterThanOrEqual(0)
.lessThanOrEqual(25)
.setValidationEnabled(isValidationEnabled);
export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId);
optionsValidator.parse(options);
}
export const defaultValidator = s.boolean;
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
labelValueDescriptionValidator.parse(label);
labelValueDescriptionValidator.parse(value);
}
export const channelTypesValidator = s.nativeEnum(ChannelType).array.setValidationEnabled(isValidationEnabled);
export const urlValidator = s.string
.url({
allowedProtocols: ['http:', 'https:', 'discord:'],
})
.strict();
.setValidationEnabled(isValidationEnabled);
export const buttonPredicate = z.discriminatedUnion('style', [
buttonLinkPredicate,
buttonPrimaryPredicate,
buttonSecondaryPredicate,
buttonSuccessPredicate,
buttonDangerPredicate,
buttonPremiumPredicate,
]);
const selectMenuBasePredicate = z.object({
placeholder: z.string().max(150).optional(),
min_values: z.number().min(0).max(25).optional(),
max_values: z.number().min(0).max(25).optional(),
custom_id: customIdPredicate,
disabled: z.boolean().optional(),
});
export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.ChannelSelect),
channel_types: z.nativeEnum(ChannelType).array().optional(),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
.array()
.max(25)
.optional(),
});
export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.MentionableSelect),
default_values: z
.object({
id: z.string(),
type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]),
})
.array()
.max(25)
.optional(),
});
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.RoleSelect),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
.array()
.max(25)
.optional(),
});
export const selectMenuStringOptionPredicate = z.object({
label: labelPredicate,
value: z.string().min(1).max(100),
description: z.string().min(1).max(100).optional(),
emoji: emojiPredicate.optional(),
default: z.boolean().optional(),
});
export const selectMenuStringPredicate = selectMenuBasePredicate
.extend({
type: z.literal(ComponentType.StringSelect),
options: selectMenuStringOptionPredicate.array().min(1).max(25),
})
.superRefine((menu, ctx) => {
const addIssue = (name: string, minimum: number) =>
ctx.addIssue({
code: 'too_small',
message: `The number of options must be greater than or equal to ${name}`,
inclusive: true,
minimum,
type: 'number',
path: ['options'],
});
if (menu.max_values !== undefined && menu.options.length < menu.max_values) {
addIssue('max_values', menu.max_values);
export function validateRequiredButtonParameters(
style?: ButtonStyle,
label?: string,
emoji?: APIMessageComponentEmoji,
customId?: string,
skuId?: string,
url?: string,
) {
if (style === ButtonStyle.Premium) {
if (!skuId) {
throw new RangeError('Premium buttons must have an SKU id.');
}
if (menu.min_values !== undefined && menu.options.length < menu.min_values) {
addIssue('min_values', menu.min_values);
if (customId || label || url || emoji) {
throw new RangeError('Premium buttons cannot have a custom id, label, URL, or emoji.');
}
} else {
if (skuId) {
throw new RangeError('Non-premium buttons must not have an SKU id.');
}
});
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.UserSelect),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
.array()
.max(25)
.optional(),
});
if (url && customId) {
throw new RangeError('URL and custom id are mutually exclusive.');
}
export const actionRowPredicate = z.object({
type: z.literal(ComponentType.ActionRow),
components: z.union([
z
.object({ type: z.literal(ComponentType.Button) })
.array()
.min(1)
.max(5),
z
.object({
type: z.union([
z.literal(ComponentType.ChannelSelect),
z.literal(ComponentType.MentionableSelect),
z.literal(ComponentType.RoleSelect),
z.literal(ComponentType.StringSelect),
z.literal(ComponentType.UserSelect),
// And this!
z.literal(ComponentType.TextInput),
]),
})
.array()
.length(1),
]),
});
if (!label && !emoji) {
throw new RangeError('Non-premium buttons must have a label and/or an emoji.');
}
if (style === ButtonStyle.Link) {
if (!url) {
throw new RangeError('Link buttons must have a URL.');
}
} else if (url) {
throw new RangeError('Non-premium and non-link buttons cannot have a URL.');
}
}
}

View File

@@ -1,5 +1,10 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIActionRowComponent, APIActionRowComponentTypes } from 'discord-api-types/v10';
import type {
APIActionRowComponent,
APIActionRowComponentTypes,
APIBaseComponent,
ComponentType,
} from 'discord-api-types/v10';
/**
* Any action row component data represented as an object.
@@ -9,15 +14,32 @@ export type AnyAPIActionRowComponent = APIActionRowComponent<APIActionRowCompone
/**
* The base component builder that contains common symbols for all sorts of components.
*
* @typeParam Component - The type of API data that is stored within the builder
* @typeParam DataType - The type of internal API data that is stored within the component
*/
export abstract class ComponentBuilder<Component extends AnyAPIActionRowComponent> implements JSONEncodable<Component> {
export abstract class ComponentBuilder<
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
> implements JSONEncodable<AnyAPIActionRowComponent>
{
/**
* The API data associated with this component.
*/
public readonly data: Partial<DataType>;
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public abstract toJSON(validationOverride?: boolean): Component;
public abstract toJSON(): AnyAPIActionRowComponent;
/**
* Constructs a new kind of component.
*
* @param data - The data to construct a component out of
*/
public constructor(data: Partial<DataType>) {
this.data = data;
}
}

View File

@@ -1,17 +1,12 @@
import type { APIButtonComponent, APIMessageComponent, APIModalComponent } from 'discord-api-types/v10';
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
import { ActionRowBuilder } from './ActionRow.js';
import type { AnyAPIActionRowComponent } from './Component.js';
import { ComponentBuilder } from './Component.js';
import type { BaseButtonBuilder } from './button/Button.js';
import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10';
import {
DangerButtonBuilder,
PrimaryButtonBuilder,
SecondaryButtonBuilder,
SuccessButtonBuilder,
} from './button/CustomIdButton.js';
import { LinkButtonBuilder } from './button/LinkButton.js';
import { PremiumButtonBuilder } from './button/PremiumButton.js';
ActionRowBuilder,
type AnyComponentBuilder,
type MessageComponentBuilder,
type ModalComponentBuilder,
} from './ActionRow.js';
import { ComponentBuilder } from './Component.js';
import { ButtonBuilder } from './button/Button.js';
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
@@ -19,48 +14,6 @@ import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import { 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.
*/
export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder;
/**
* Any button builder
*/
export type ButtonBuilder =
| DangerButtonBuilder
| LinkButtonBuilder
| PremiumButtonBuilder
| PrimaryButtonBuilder
| SecondaryButtonBuilder
| SuccessButtonBuilder;
/**
* The builders that may be used within an action row for messages.
*/
export type MessageActionRowComponentBuilder =
| ButtonBuilder
| ChannelSelectMenuBuilder
| MentionableSelectMenuBuilder
| RoleSelectMenuBuilder
| StringSelectMenuBuilder
| UserSelectMenuBuilder;
/**
* The builders that may be used within an action row for modals.
*/
export type ModalActionRowComponentBuilder = TextInputBuilder;
/**
* Any action row component builder.
*/
export type AnyActionRowComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
/**
* Components here are mapped to their respective builder.
*/
@@ -68,9 +21,9 @@ export interface MappedComponentTypes {
/**
* The action row component type is associated with an {@link ActionRowBuilder}.
*/
[ComponentType.ActionRow]: ActionRowBuilder;
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
/**
* The button component type is associated with a {@link BaseButtonBuilder}.
* The button component type is associated with a {@link ButtonBuilder}.
*/
[ComponentType.Button]: ButtonBuilder;
/**
@@ -122,7 +75,7 @@ export function createComponentBuilder<ComponentBuilder extends MessageComponent
export function createComponentBuilder(
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
): ComponentBuilder<AnyAPIActionRowComponent> {
): ComponentBuilder {
if (data instanceof ComponentBuilder) {
return data;
}
@@ -131,7 +84,7 @@ export function createComponentBuilder(
case ComponentType.ActionRow:
return new ActionRowBuilder(data);
case ComponentType.Button:
return createButtonBuilder(data);
return new ButtonBuilder(data);
case ComponentType.StringSelect:
return new StringSelectMenuBuilder(data);
case ComponentType.TextInput:
@@ -149,23 +102,3 @@ export function createComponentBuilder(
throw new Error(`Cannot properly serialize component type: ${data.type}`);
}
}
function createButtonBuilder(data: APIButtonComponent): ButtonBuilder {
switch (data.style) {
case ButtonStyle.Primary:
return new PrimaryButtonBuilder(data);
case ButtonStyle.Secondary:
return new SecondaryButtonBuilder(data);
case ButtonStyle.Success:
return new SuccessButtonBuilder(data);
case ButtonStyle.Danger:
return new DangerButtonBuilder(data);
case ButtonStyle.Link:
return new LinkButtonBuilder(data);
case ButtonStyle.Premium:
return new PremiumButtonBuilder(data);
default:
// @ts-expect-error This case can still occur if we get a newer unsupported button style
throw new Error(`Cannot properly serialize button with style: ${data.style}`);
}
}

View File

@@ -1,13 +1,115 @@
import type { APIButtonComponent } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { buttonPredicate } from '../Assertions.js';
import {
ComponentType,
type APIButtonComponent,
type APIButtonComponentWithCustomId,
type APIButtonComponentWithSKUId,
type APIButtonComponentWithURL,
type APIMessageComponentEmoji,
type ButtonStyle,
type Snowflake,
} from 'discord-api-types/v10';
import {
buttonLabelValidator,
buttonStyleValidator,
customIdValidator,
disabledValidator,
emojiValidator,
urlValidator,
validateRequiredButtonParameters,
} from '../Assertions.js';
import { ComponentBuilder } from '../Component.js';
/**
* A builder that creates API-compatible JSON data for buttons.
*/
export abstract class BaseButtonBuilder<ButtonData extends APIButtonComponent> extends ComponentBuilder<ButtonData> {
protected declare readonly data: Partial<ButtonData>;
export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
/**
* Creates a new button from API data.
*
* @param data - The API data to create this button with
* @example
* Creating a button from an API data object:
* ```ts
* const button = new ButtonBuilder({
* custom_id: 'a cool button',
* style: ButtonStyle.Primary,
* label: 'Click Me',
* emoji: {
* name: 'smile',
* id: '123456789012345678',
* },
* });
* ```
* @example
* Creating a button using setters and API data:
* ```ts
* const button = new ButtonBuilder({
* style: ButtonStyle.Secondary,
* label: 'Click Me',
* })
* .setEmoji({ name: '🙂' })
* .setCustomId('another cool button');
* ```
*/
public constructor(data?: Partial<APIButtonComponent>) {
super({ type: ComponentType.Button, ...data });
}
/**
* Sets the style of this button.
*
* @param style - The style to use
*/
public setStyle(style: ButtonStyle) {
this.data.style = buttonStyleValidator.parse(style);
return this;
}
/**
* Sets the URL for this button.
*
* @remarks
* This method is only available to buttons using the `Link` button style.
* Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`.
* @param url - The URL to use
*/
public setURL(url: string) {
(this.data as APIButtonComponentWithURL).url = urlValidator.parse(url);
return this;
}
/**
* Sets the custom id for this button.
*
* @remarks
* This method is only applicable to buttons that are not using the `Link` button style.
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
(this.data as APIButtonComponentWithCustomId).custom_id = customIdValidator.parse(customId);
return this;
}
/**
* Sets the SKU id that represents a purchasable SKU for this button.
*
* @remarks Only available when using premium-style buttons.
* @param skuId - The SKU id to use
*/
public setSKUId(skuId: Snowflake) {
(this.data as APIButtonComponentWithSKUId).sku_id = skuId;
return this;
}
/**
* Sets the emoji to display on this button.
*
* @param emoji - The emoji to use
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji = emojiValidator.parse(emoji);
return this;
}
/**
* Sets whether this button is disabled.
@@ -15,17 +117,35 @@ export abstract class BaseButtonBuilder<ButtonData extends APIButtonComponent> e
* @param disabled - Whether to disable this button
*/
public setDisabled(disabled = true) {
this.data.disabled = disabled;
this.data.disabled = disabledValidator.parse(disabled);
return this;
}
/**
* Sets the label for this button.
*
* @param label - The label to use
*/
public setLabel(label: string) {
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label = buttonLabelValidator.parse(label);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): ButtonData {
const clone = structuredClone(this.data);
validate(buttonPredicate, clone, validationOverride);
public toJSON(): APIButtonComponent {
validateRequiredButtonParameters(
this.data.style,
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label,
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji,
(this.data as APIButtonComponentWithCustomId).custom_id,
(this.data as APIButtonComponentWithSKUId).sku_id,
(this.data as APIButtonComponentWithURL).url,
);
return clone as ButtonData;
return {
...this.data,
} as APIButtonComponent;
}
}

View File

@@ -1,69 +0,0 @@
import { ButtonStyle, ComponentType, type APIButtonComponentWithCustomId } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { BaseButtonBuilder } from './Button.js';
import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js';
export type CustomIdButtonStyle = APIButtonComponentWithCustomId['style'];
/**
* A builder that creates API-compatible JSON data for buttons with custom IDs.
*/
export abstract class CustomIdButtonBuilder extends Mixin(
BaseButtonBuilder<APIButtonComponentWithCustomId>,
EmojiOrLabelButtonMixin,
) {
protected override readonly data: Partial<APIButtonComponentWithCustomId>;
protected constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.Button };
}
/**
* Sets the custom id for this button.
*
* @remarks
* This method is only applicable to buttons that are not using the `Link` button style.
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;
return this;
}
}
/**
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the primary style).
*/
export class PrimaryButtonBuilder extends CustomIdButtonBuilder {
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
super({ ...data, style: ButtonStyle.Primary });
}
}
/**
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the secondary style).
*/
export class SecondaryButtonBuilder extends CustomIdButtonBuilder {
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
super({ ...data, style: ButtonStyle.Secondary });
}
}
/**
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the success style).
*/
export class SuccessButtonBuilder extends CustomIdButtonBuilder {
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
super({ ...data, style: ButtonStyle.Success });
}
}
/**
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the danger style).
*/
export class DangerButtonBuilder extends CustomIdButtonBuilder {
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
super({ ...data, style: ButtonStyle.Danger });
}
}

View File

@@ -1,34 +0,0 @@
import {
ButtonStyle,
ComponentType,
type APIButtonComponent,
type APIButtonComponentWithURL,
} from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { BaseButtonBuilder } from './Button.js';
import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js';
/**
* A builder that creates API-compatible JSON data for buttons with links.
*/
export class LinkButtonBuilder extends Mixin(BaseButtonBuilder<APIButtonComponentWithURL>, EmojiOrLabelButtonMixin) {
protected override readonly data: Partial<APIButtonComponentWithURL>;
public constructor(data: Partial<APIButtonComponent> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Link };
}
/**
* Sets the URL for this button.
*
* @remarks
* This method is only available to buttons using the `Link` button style.
* Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`.
* @param url - The URL to use
*/
public setURL(url: string) {
this.data.url = url;
return this;
}
}

View File

@@ -1,26 +0,0 @@
import type { APIButtonComponentWithSKUId, Snowflake } from 'discord-api-types/v10';
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
import { BaseButtonBuilder } from './Button.js';
/**
* A builder that creates API-compatible JSON data for premium buttons.
*/
export class PremiumButtonBuilder extends BaseButtonBuilder<APIButtonComponentWithSKUId> {
protected override readonly data: Partial<APIButtonComponentWithSKUId>;
public constructor(data: Partial<APIButtonComponentWithSKUId> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Premium };
}
/**
* Sets the SKU id that represents a purchasable SKU for this button.
*
* @remarks Only available when using premium-style buttons.
* @param skuId - The SKU id to use
*/
public setSKUId(skuId: Snowflake) {
this.data.sku_id = skuId;
return this;
}
}

View File

@@ -1,44 +0,0 @@
import type { APIButtonComponent, APIButtonComponentWithSKUId, APIMessageComponentEmoji } from 'discord-api-types/v10';
export interface EmojiOrLabelButtonData
extends Pick<Exclude<APIButtonComponent, APIButtonComponentWithSKUId>, 'emoji' | 'label'> {}
export class EmojiOrLabelButtonMixin {
protected declare readonly data: EmojiOrLabelButtonData;
/**
* Sets the emoji to display on this button.
*
* @param emoji - The emoji to use
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
this.data.emoji = emoji;
return this;
}
/**
* Clears the emoji on this button.
*/
public clearEmoji() {
this.data.emoji = undefined;
return this;
}
/**
* Sets the label for this button.
*
* @param label - The label to use
*/
public setLabel(label: string) {
this.data.label = label;
return this;
}
/**
* Clears the label on this button.
*/
public clearLabel() {
this.data.label = undefined;
return this;
}
}

View File

@@ -1,5 +1,5 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APISelectMenuComponent } from 'discord-api-types/v10';
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
import { ComponentBuilder } from '../Component.js';
/**
@@ -7,29 +7,16 @@ import { ComponentBuilder } from '../Component.js';
*
* @typeParam SelectMenuType - The type of select menu this would be instantiated for.
*/
export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
extends ComponentBuilder<Data>
implements JSONEncodable<APISelectMenuComponent>
{
protected abstract readonly data: Partial<
Pick<Data, 'custom_id' | 'disabled' | 'max_values' | 'min_values' | 'placeholder'>
>;
export abstract class BaseSelectMenuBuilder<
SelectMenuType extends APISelectMenuComponent,
> extends ComponentBuilder<SelectMenuType> {
/**
* Sets the placeholder for this select menu.
*
* @param placeholder - The placeholder to use
*/
public setPlaceholder(placeholder: string) {
this.data.placeholder = placeholder;
return this;
}
/**
* Clears the placeholder for this select menu.
*/
public clearPlaceholder() {
this.data.placeholder = undefined;
this.data.placeholder = placeholderValidator.parse(placeholder);
return this;
}
@@ -39,7 +26,7 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
* @param minValues - The minimum values that must be selected
*/
public setMinValues(minValues: number) {
this.data.min_values = minValues;
this.data.min_values = minMaxValidator.parse(minValues);
return this;
}
@@ -49,7 +36,7 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
* @param maxValues - The maximum values that must be selected
*/
public setMaxValues(maxValues: number) {
this.data.max_values = maxValues;
this.data.max_values = minMaxValidator.parse(maxValues);
return this;
}
@@ -59,7 +46,7 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;
this.data.custom_id = customIdValidator.parse(customId);
return this;
}
@@ -69,7 +56,17 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
* @param disabled - Whether this select menu is disabled
*/
public setDisabled(disabled = true) {
this.data.disabled = disabled;
this.data.disabled = disabledValidator.parse(disabled);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public toJSON(): SelectMenuType {
customIdValidator.parse(this.data.custom_id);
return {
...this.data,
} as SelectMenuType;
}
}

View File

@@ -6,16 +6,13 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { validate } from '../../util/validation.js';
import { selectMenuChannelPredicate } from '../Assertions.js';
import { channelTypesValidator, customIdValidator, optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
/**
* A builder that creates API-compatible JSON data for channel select menus.
*/
export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSelectComponent> {
protected override readonly data: Partial<APIChannelSelectComponent>;
/**
* Creates a new select menu from API data.
*
@@ -39,9 +36,8 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
* .setMinValues(2);
* ```
*/
public constructor(data: Partial<APIChannelSelectComponent> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.ChannelSelect };
public constructor(data?: Partial<APIChannelSelectComponent>) {
super({ ...data, type: ComponentType.ChannelSelect });
}
/**
@@ -52,7 +48,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
public addChannelTypes(...types: RestOrArray<ChannelType>) {
const normalizedTypes = normalizeArray(types);
this.data.channel_types ??= [];
this.data.channel_types.push(...normalizedTypes);
this.data.channel_types.push(...channelTypesValidator.parse(normalizedTypes));
return this;
}
@@ -64,7 +60,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
public setChannelTypes(...types: RestOrArray<ChannelType>) {
const normalizedTypes = normalizeArray(types);
this.data.channel_types ??= [];
this.data.channel_types.splice(0, this.data.channel_types.length, ...normalizedTypes);
this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(normalizedTypes));
return this;
}
@@ -75,6 +71,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
*/
public addDefaultChannels(...channels: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(channels);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(
@@ -94,6 +91,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
*/
public setDefaultChannels(...channels: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(channels);
optionsLengthValidator.parse(normalizedValues.length);
this.data.default_values = normalizedValues.map((id) => ({
id,
@@ -104,12 +102,13 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIChannelSelectComponent {
const clone = structuredClone(this.data);
validate(selectMenuChannelPredicate, clone, validationOverride);
public override toJSON(): APIChannelSelectComponent {
customIdValidator.parse(this.data.custom_id);
return clone as APIChannelSelectComponent;
return {
...this.data,
} as APIChannelSelectComponent;
}
}

View File

@@ -6,16 +6,13 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { validate } from '../../util/validation.js';
import { selectMenuMentionablePredicate } from '../Assertions.js';
import { optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
/**
* A builder that creates API-compatible JSON data for mentionable select menus.
*/
export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMentionableSelectComponent> {
protected override readonly data: Partial<APIMentionableSelectComponent>;
/**
* Creates a new select menu from API data.
*
@@ -38,9 +35,8 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
* .setMinValues(1);
* ```
*/
public constructor(data: Partial<APIMentionableSelectComponent> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.MentionableSelect };
public constructor(data?: Partial<APIMentionableSelectComponent>) {
super({ ...data, type: ComponentType.MentionableSelect });
}
/**
@@ -50,6 +46,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
*/
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(roles);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(
@@ -69,6 +66,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
*/
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(users);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(
@@ -93,6 +91,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
>
) {
const normalizedValues = normalizeArray(values);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(...normalizedValues);
return this;
@@ -110,17 +109,8 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
>
) {
const normalizedValues = normalizeArray(values);
optionsLengthValidator.parse(normalizedValues.length);
this.data.default_values = normalizedValues;
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIMentionableSelectComponent {
const clone = structuredClone(this.data);
validate(selectMenuMentionablePredicate, clone, validationOverride);
return clone as APIMentionableSelectComponent;
}
}

View File

@@ -5,16 +5,13 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { validate } from '../../util/validation.js';
import { selectMenuRolePredicate } from '../Assertions.js';
import { optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
/**
* A builder that creates API-compatible JSON data for role select menus.
*/
export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectComponent> {
protected override readonly data: Partial<APIRoleSelectComponent>;
/**
* Creates a new select menu from API data.
*
@@ -37,9 +34,8 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
* .setMinValues(1);
* ```
*/
public constructor(data: Partial<APIRoleSelectComponent> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.RoleSelect };
public constructor(data?: Partial<APIRoleSelectComponent>) {
super({ ...data, type: ComponentType.RoleSelect });
}
/**
@@ -49,6 +45,7 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
*/
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(roles);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(
@@ -68,6 +65,7 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
*/
public setDefaultRoles(...roles: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(roles);
optionsLengthValidator.parse(normalizedValues.length);
this.data.default_values = normalizedValues.map((id) => ({
id,
@@ -76,14 +74,4 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIRoleSelectComponent {
const clone = structuredClone(this.data);
validate(selectMenuRolePredicate, clone, validationOverride);
return clone as APIRoleSelectComponent;
}
}

View File

@@ -1,30 +1,18 @@
/* eslint-disable jsdoc/check-param-names */
import { ComponentType } from 'discord-api-types/v10';
import type { APIStringSelectComponent, APISelectMenuOption } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { resolveBuilder } from '../../util/resolveBuilder.js';
import { validate } from '../../util/validation.js';
import { selectMenuStringPredicate } from '../Assertions.js';
import { jsonOptionValidator, optionsLengthValidator, validateRequiredSelectMenuParameters } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js';
export interface StringSelectMenuData extends Partial<Omit<APIStringSelectComponent, 'options'>> {
options: StringSelectMenuOptionBuilder[];
}
/**
* A builder that creates API-compatible JSON data for string select menus.
*/
export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSelectComponent> {
protected override readonly data: StringSelectMenuData;
/**
* The options for this select menu.
* The options within this select menu.
*/
public get options(): readonly StringSelectMenuOptionBuilder[] {
return this.data.options;
}
public readonly options: StringSelectMenuOptionBuilder[];
/**
* Creates a new select menu from API data.
@@ -57,13 +45,10 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
* });
* ```
*/
public constructor({ options = [], ...data }: Partial<APIStringSelectComponent> = {}) {
super();
this.data = {
...structuredClone(data),
options: options.map((option) => new StringSelectMenuOptionBuilder(option)),
type: ComponentType.StringSelect,
};
public constructor(data?: Partial<APIStringSelectComponent>) {
const { options, ...initData } = data ?? {};
super({ ...initData, type: ComponentType.StringSelect });
this.options = options?.map((option: APISelectMenuOption) => new StringSelectMenuOptionBuilder(option)) ?? [];
}
/**
@@ -71,18 +56,16 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
*
* @param options - The options to add
*/
public addOptions(
...options: RestOrArray<
| APISelectMenuOption
| StringSelectMenuOptionBuilder
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
>
) {
public addOptions(...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
const normalizedOptions = normalizeArray(options);
const resolved = normalizedOptions.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder));
this.data.options.push(...resolved);
optionsLengthValidator.parse(this.options.length + normalizedOptions.length);
this.options.push(
...normalizedOptions.map((normalizedOption) =>
normalizedOption instanceof StringSelectMenuOptionBuilder
? normalizedOption
: new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)),
),
);
return this;
}
@@ -91,14 +74,8 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
*
* @param options - The options to set
*/
public setOptions(
...options: RestOrArray<
| APISelectMenuOption
| StringSelectMenuOptionBuilder
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
>
) {
return this.spliceOptions(0, this.options.length, ...normalizeArray(options));
public setOptions(...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
return this.spliceOptions(0, this.options.length, ...options);
}
/**
@@ -131,33 +108,36 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
public spliceOptions(
index: number,
deleteCount: number,
...options: (
| APISelectMenuOption
| StringSelectMenuOptionBuilder
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
)[]
...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>
) {
const resolved = options.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder));
const normalizedOptions = normalizeArray(options);
this.data.options ??= [];
this.data.options.splice(index, deleteCount, ...resolved);
const clone = [...this.options];
clone.splice(
index,
deleteCount,
...normalizedOptions.map((normalizedOption) =>
normalizedOption instanceof StringSelectMenuOptionBuilder
? normalizedOption
: new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)),
),
);
optionsLengthValidator.parse(clone.length);
this.options.splice(0, this.options.length, ...clone);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIStringSelectComponent {
const { options, ...rest } = this.data;
const data = {
...(structuredClone(rest) as APIStringSelectComponent),
// selectMenuStringPredicate covers the validation of options
options: options.map((option) => option.toJSON(false)),
};
public override toJSON(): APIStringSelectComponent {
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
validate(selectMenuStringPredicate, data, validationOverride);
return data as APIStringSelectComponent;
return {
...this.data,
options: this.options.map((option) => option.toJSON()),
} as APIStringSelectComponent;
}
}

View File

@@ -1,14 +1,16 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { selectMenuStringOptionPredicate } from '../Assertions.js';
import {
defaultValidator,
emojiValidator,
labelValueDescriptionValidator,
validateRequiredSelectMenuOptionParameters,
} from '../Assertions.js';
/**
* A builder that creates API-compatible JSON data for string select menu options.
*/
export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
private readonly data: Partial<APISelectMenuOption>;
/**
* Creates a new string select menu option from API data.
*
@@ -31,9 +33,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
* .setLabel('woah');
* ```
*/
public constructor(data: Partial<APISelectMenuOption> = {}) {
this.data = structuredClone(data);
}
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
/**
* Sets the label for this option.
@@ -41,7 +41,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
* @param label - The label to use
*/
public setLabel(label: string) {
this.data.label = label;
this.data.label = labelValueDescriptionValidator.parse(label);
return this;
}
@@ -51,7 +51,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
* @param value - The value to use
*/
public setValue(value: string) {
this.data.value = value;
this.data.value = labelValueDescriptionValidator.parse(value);
return this;
}
@@ -61,15 +61,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Clears the description for this option.
*/
public clearDescription() {
this.data.description = undefined;
this.data.description = labelValueDescriptionValidator.parse(description);
return this;
}
@@ -79,7 +71,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
* @param isDefault - Whether this option is selected by default
*/
public setDefault(isDefault = true) {
this.data.default = isDefault;
this.data.default = defaultValidator.parse(isDefault);
return this;
}
@@ -89,25 +81,18 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
* @param emoji - The emoji to use
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
this.data.emoji = emoji;
this.data.emoji = emojiValidator.parse(emoji);
return this;
}
/**
* Clears the emoji for this option.
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
*/
public clearEmoji() {
this.data.emoji = undefined;
return this;
}
public toJSON(): APISelectMenuOption {
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public toJSON(validationOverride?: boolean): APISelectMenuOption {
const clone = structuredClone(this.data);
validate(selectMenuStringOptionPredicate, clone, validationOverride);
return clone as APISelectMenuOption;
return {
...this.data,
} as APISelectMenuOption;
}
}

View File

@@ -5,16 +5,13 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { validate } from '../../util/validation.js';
import { selectMenuUserPredicate } from '../Assertions.js';
import { optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
/**
* A builder that creates API-compatible JSON data for user select menus.
*/
export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectComponent> {
protected override readonly data: Partial<APIUserSelectComponent>;
/**
* Creates a new select menu from API data.
*
@@ -37,9 +34,8 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
* .setMinValues(1);
* ```
*/
public constructor(data: Partial<APIUserSelectComponent> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.UserSelect };
public constructor(data?: Partial<APIUserSelectComponent>) {
super({ ...data, type: ComponentType.UserSelect });
}
/**
@@ -49,8 +45,9 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
*/
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(users);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(
...normalizedValues.map((id) => ({
id,
@@ -68,6 +65,7 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
*/
public setDefaultUsers(...users: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(users);
optionsLengthValidator.parse(normalizedValues.length);
this.data.default_values = normalizedValues.map((id) => ({
id,
@@ -76,14 +74,4 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIUserSelectComponent {
const clone = structuredClone(this.data);
validate(selectMenuUserPredicate, clone, validationOverride);
return clone as APIUserSelectComponent;
}
}

View File

@@ -1,15 +1,27 @@
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
import { z } from 'zod';
import { customIdPredicate } from '../../Assertions.js';
import { s } from '@sapphire/shapeshift';
import { TextInputStyle } from 'discord-api-types/v10';
import { isValidationEnabled } from '../../util/validation.js';
import { customIdValidator } from '../Assertions.js';
export const textInputPredicate = z.object({
type: z.literal(ComponentType.TextInput),
custom_id: customIdPredicate,
label: z.string().min(1).max(45),
style: z.nativeEnum(TextInputStyle),
min_length: z.number().min(0).max(4_000).optional(),
max_length: z.number().min(1).max(4_000).optional(),
placeholder: z.string().max(100).optional(),
value: z.string().max(4_000).optional(),
required: z.boolean().optional(),
});
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
export const minLengthValidator = s.number.int
.greaterThanOrEqual(0)
.lessThanOrEqual(4_000)
.setValidationEnabled(isValidationEnabled);
export const maxLengthValidator = s.number.int
.greaterThanOrEqual(1)
.lessThanOrEqual(4_000)
.setValidationEnabled(isValidationEnabled);
export const requiredValidator = s.boolean;
export const valueValidator = s.string.lengthLessThanOrEqual(4_000).setValidationEnabled(isValidationEnabled);
export const placeholderValidator = s.string.lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled);
export const labelValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(45)
.setValidationEnabled(isValidationEnabled);
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
customIdValidator.parse(customId);
textInputStyleValidator.parse(style);
labelValidator.parse(label);
}

View File

@@ -1,14 +1,26 @@
import { isJSONEncodable, type Equatable, type JSONEncodable } from '@discordjs/util';
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import isEqual from 'fast-deep-equal';
import { customIdValidator } from '../Assertions.js';
import { ComponentBuilder } from '../Component.js';
import { textInputPredicate } from './Assertions.js';
import {
maxLengthValidator,
minLengthValidator,
placeholderValidator,
requiredValidator,
valueValidator,
validateRequiredParameters,
labelValidator,
textInputStyleValidator,
} from './Assertions.js';
/**
* A builder that creates API-compatible JSON data for text inputs.
*/
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
private readonly data: Partial<APITextInputComponent>;
export class TextInputBuilder
extends ComponentBuilder<APITextInputComponent>
implements Equatable<APITextInputComponent | JSONEncodable<APITextInputComponent>>
{
/**
* Creates a new text input from API data.
*
@@ -32,9 +44,8 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* .setStyle(TextInputStyle.Paragraph);
* ```
*/
public constructor(data: Partial<APITextInputComponent> = {}) {
super();
this.data = { ...structuredClone(data), type: ComponentType.TextInput };
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
super({ type: ComponentType.TextInput, ...data });
}
/**
@@ -43,7 +54,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;
this.data.custom_id = customIdValidator.parse(customId);
return this;
}
@@ -53,7 +64,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param label - The label to use
*/
public setLabel(label: string) {
this.data.label = label;
this.data.label = labelValidator.parse(label);
return this;
}
@@ -63,7 +74,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param style - The style to use
*/
public setStyle(style: TextInputStyle) {
this.data.style = style;
this.data.style = textInputStyleValidator.parse(style);
return this;
}
@@ -73,15 +84,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param minLength - The minimum length of text for this text input
*/
public setMinLength(minLength: number) {
this.data.min_length = minLength;
return this;
}
/**
* Clears the minimum length of text for this text input.
*/
public clearMinLength() {
this.data.min_length = undefined;
this.data.min_length = minLengthValidator.parse(minLength);
return this;
}
@@ -91,15 +94,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param maxLength - The maximum length of text for this text input
*/
public setMaxLength(maxLength: number) {
this.data.max_length = maxLength;
return this;
}
/**
* Clears the maximum length of text for this text input.
*/
public clearMaxLength() {
this.data.max_length = undefined;
this.data.max_length = maxLengthValidator.parse(maxLength);
return this;
}
@@ -109,15 +104,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param placeholder - The placeholder to use
*/
public setPlaceholder(placeholder: string) {
this.data.placeholder = placeholder;
return this;
}
/**
* Clears the placeholder for this text input.
*/
public clearPlaceholder() {
this.data.placeholder = undefined;
this.data.placeholder = placeholderValidator.parse(placeholder);
return this;
}
@@ -127,15 +114,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param value - The value to use
*/
public setValue(value: string) {
this.data.value = value;
return this;
}
/**
* Clears the value for this text input.
*/
public clearValue() {
this.data.value = undefined;
this.data.value = valueValidator.parse(value);
return this;
}
@@ -145,17 +124,29 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
* @param required - Whether this text input is required
*/
public setRequired(required = true) {
this.data.required = required;
this.data.required = requiredValidator.parse(required);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public toJSON(validationOverride?: boolean): APITextInputComponent {
const clone = structuredClone(this.data);
validate(textInputPredicate, clone, validationOverride);
public toJSON(): APITextInputComponent {
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
return clone as APITextInputComponent;
return {
...this.data,
} as APITextInputComponent;
}
/**
* Whether this is equal to another structure.
*/
public equals(other: APITextInputComponent | JSONEncodable<APITextInputComponent>): boolean {
if (isJSONEncodable(other)) {
return isEqual(other.toJSON(), this.data);
}
return isEqual(other, this.data);
}
}

View File

@@ -1,71 +1,68 @@
export * from './components/button/mixins/EmojiOrLabelButtonMixin.js';
export * as EmbedAssertions from './messages/embed/Assertions.js';
export * from './messages/embed/Embed.js';
// TODO: Consider removing this dep in the next major version
export * from '@discordjs/formatters';
export * as ComponentAssertions from './components/Assertions.js';
export * from './components/ActionRow.js';
export * from './components/button/Button.js';
export * from './components/button/CustomIdButton.js';
export * from './components/button/LinkButton.js';
export * from './components/button/PremiumButton.js';
export * from './components/Component.js';
export * from './components/Components.js';
export * from './components/textInput/TextInput.js';
export * as TextInputAssertions from './components/textInput/Assertions.js';
export * from './interactions/modals/Modal.js';
export * as ModalAssertions from './interactions/modals/Assertions.js';
export * from './components/selectMenu/BaseSelectMenu.js';
export * from './components/selectMenu/ChannelSelectMenu.js';
export * from './components/selectMenu/MentionableSelectMenu.js';
export * from './components/selectMenu/RoleSelectMenu.js';
export * from './components/selectMenu/StringSelectMenu.js';
// TODO: Remove those aliases in v2
export {
/**
* @deprecated Will be removed in the next major version, use {@link StringSelectMenuBuilder} instead.
*/
StringSelectMenuBuilder as SelectMenuBuilder,
} from './components/selectMenu/StringSelectMenu.js';
export {
/**
* @deprecated Will be removed in the next major version, use {@link StringSelectMenuOptionBuilder} instead.
*/
StringSelectMenuOptionBuilder as SelectMenuOptionBuilder,
} from './components/selectMenu/StringSelectMenuOption.js';
export * from './components/selectMenu/StringSelectMenuOption.js';
export * from './components/selectMenu/UserSelectMenu.js';
export * from './components/textInput/TextInput.js';
export * from './components/textInput/Assertions.js';
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js';
export * from './interactions/slashCommands/SlashCommandBuilder.js';
export * from './interactions/slashCommands/SlashCommandSubcommands.js';
export * from './interactions/slashCommands/options/boolean.js';
export * from './interactions/slashCommands/options/channel.js';
export * from './interactions/slashCommands/options/integer.js';
export * from './interactions/slashCommands/options/mentionable.js';
export * from './interactions/slashCommands/options/number.js';
export * from './interactions/slashCommands/options/role.js';
export * from './interactions/slashCommands/options/attachment.js';
export * from './interactions/slashCommands/options/string.js';
export * from './interactions/slashCommands/options/user.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.js';
export * from './interactions/slashCommands/mixins/NameAndDescription.js';
export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions.js';
export * from './interactions/slashCommands/mixins/SharedSubcommands.js';
export * from './interactions/slashCommands/mixins/SharedSlashCommand.js';
export * from './components/ActionRow.js';
export * from './components/Assertions.js';
export * from './components/Component.js';
export * from './components/Components.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.js';
export * from './interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.js';
export * from './interactions/commands/chatInput/mixins/SharedSubcommands.js';
export * from './interactions/commands/chatInput/options/ApplicationCommandOptionBase.js';
export * from './interactions/commands/chatInput/options/boolean.js';
export * from './interactions/commands/chatInput/options/channel.js';
export * from './interactions/commands/chatInput/options/integer.js';
export * from './interactions/commands/chatInput/options/mentionable.js';
export * from './interactions/commands/chatInput/options/number.js';
export * from './interactions/commands/chatInput/options/role.js';
export * from './interactions/commands/chatInput/options/attachment.js';
export * from './interactions/commands/chatInput/options/string.js';
export * from './interactions/commands/chatInput/options/user.js';
export * from './interactions/commands/chatInput/Assertions.js';
export * from './interactions/commands/chatInput/ChatInputCommand.js';
export * from './interactions/commands/chatInput/ChatInputCommandSubcommands.js';
export * from './interactions/commands/contextMenu/Assertions.js';
export * from './interactions/commands/contextMenu/ContextMenuCommand.js';
export * from './interactions/commands/contextMenu/MessageCommand.js';
export * from './interactions/commands/contextMenu/UserCommand.js';
export * from './interactions/commands/Command.js';
export * from './interactions/commands/SharedName.js';
export * from './interactions/commands/SharedNameAndDescription.js';
export * from './interactions/modals/Assertions.js';
export * from './interactions/modals/Modal.js';
export * from './messages/embed/Assertions.js';
export * from './messages/embed/Embed.js';
export * from './messages/embed/EmbedAuthor.js';
export * from './messages/embed/EmbedField.js';
export * from './messages/embed/EmbedFooter.js';
export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions.js';
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder.js';
export * from './util/componentUtil.js';
export * from './util/normalizeArray.js';
export * from './util/validation.js';
export * from './Assertions.js';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version
* that you are currently using.

View File

@@ -1,83 +0,0 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
ApplicationIntegrationType,
InteractionContextType,
Permissions,
RESTPostAPIApplicationCommandsJSONBody,
} from 'discord-api-types/v10';
import type { RestOrArray } from '../../util/normalizeArray.js';
import { normalizeArray } from '../../util/normalizeArray.js';
export interface CommandData
extends Partial<
Pick<
RESTPostAPIApplicationCommandsJSONBody,
'contexts' | 'default_member_permissions' | 'integration_types' | 'nsfw'
>
> {}
export abstract class CommandBuilder<Command extends RESTPostAPIApplicationCommandsJSONBody>
implements JSONEncodable<Command>
{
protected declare readonly data: CommandData;
/**
* Sets the contexts of this command.
*
* @param contexts - The contexts
*/
public setContexts(...contexts: RestOrArray<InteractionContextType>) {
this.data.contexts = normalizeArray(contexts);
return this;
}
/**
* Sets the integration types of this command.
*
* @param integrationTypes - The integration types
*/
public setIntegrationTypes(...integrationTypes: RestOrArray<ApplicationIntegrationType>) {
this.data.integration_types = normalizeArray(integrationTypes);
return this;
}
/**
* Sets the default permissions a member should have in order to run the command.
*
* @remarks
* You can set this to `'0'` to disable the command by default.
* @param permissions - The permissions bit field to set
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
*/
public setDefaultMemberPermissions(permissions: Permissions | bigint | number) {
this.data.default_member_permissions = typeof permissions === 'string' ? permissions : permissions.toString();
return this;
}
/**
* Clears the default permissions a member should have in order to run the command.
*/
public clearDefaultMemberPermissions() {
this.data.default_member_permissions = undefined;
return this;
}
/**
* Sets whether this command is NSFW.
*
* @param nsfw - Whether this command is NSFW
*/
public setNSFW(nsfw = true) {
this.data.nsfw = nsfw;
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public abstract toJSON(validationOverride?: boolean): Command;
}

View File

@@ -1,64 +0,0 @@
import type { LocaleString, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
export interface SharedNameData
extends Partial<Pick<RESTPostAPIApplicationCommandsJSONBody, 'name_localizations' | 'name'>> {}
/**
* This mixin holds name and description symbols for chat input commands.
*/
export class SharedName {
protected readonly data: SharedNameData = {};
/**
* Sets the name of this command.
*
* @param name - The name to use
*/
public setName(name: string): this {
this.data.name = name;
return this;
}
/**
* Sets a name localization for this command.
*
* @param locale - The locale to set
* @param localizedName - The localized name for the given `locale`
*/
public setNameLocalization(locale: LocaleString, localizedName: string) {
this.data.name_localizations ??= {};
this.data.name_localizations[locale] = localizedName;
return this;
}
/**
* Clears a name localization for this command.
*
* @param locale - The locale to clear
*/
public clearNameLocalization(locale: LocaleString) {
this.data.name_localizations ??= {};
this.data.name_localizations[locale] = undefined;
return this;
}
/**
* Sets the name localizations for this command.
*
* @param localizedNames - The object of localized names to set
*/
public setNameLocalizations(localizedNames: Partial<Record<LocaleString, string>>) {
this.data.name_localizations = structuredClone(localizedNames);
return this;
}
/**
* Clears all name localizations for this command.
*/
public clearNameLocalizations() {
this.data.name_localizations = undefined;
return this;
}
}

View File

@@ -1,67 +0,0 @@
import type { APIApplicationCommand, LocaleString } from 'discord-api-types/v10';
import type { SharedNameData } from './SharedName.js';
import { SharedName } from './SharedName.js';
export interface SharedNameAndDescriptionData
extends SharedNameData,
Partial<Pick<APIApplicationCommand, 'description_localizations' | 'description'>> {}
/**
* This mixin holds name and description symbols for chat input commands.
*/
export class SharedNameAndDescription extends SharedName {
protected override readonly data: SharedNameAndDescriptionData = {};
/**
* Sets the description of this command.
*
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Sets a description localization for this command.
*
* @param locale - The locale to set
* @param localizedDescription - The localized description for the given `locale`
*/
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string) {
this.data.description_localizations ??= {};
this.data.description_localizations[locale] = localizedDescription;
return this;
}
/**
* Clears a description localization for this command.
*
* @param locale - The locale to clear
*/
public clearDescriptionLocalization(locale: LocaleString) {
this.data.description_localizations ??= {};
this.data.description_localizations[locale] = undefined;
return this;
}
/**
* Sets the description localizations for this command.
*
* @param localizedDescriptions - The object of localized descriptions to set
*/
public setDescriptionLocalizations(localizedDescriptions: Partial<Record<LocaleString, string>>) {
this.data.description_localizations = structuredClone(localizedDescriptions);
return this;
}
/**
* Clears all description localizations for this command.
*/
public clearDescriptionLocalizations() {
this.data.description_localizations = undefined;
return this;
}
}

View File

@@ -1,154 +0,0 @@
import {
ApplicationIntegrationType,
InteractionContextType,
ApplicationCommandOptionType,
} from 'discord-api-types/v10';
import type { ZodTypeAny } from 'zod';
import { z } from 'zod';
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
import { ApplicationCommandOptionAllowedChannelTypes } from './mixins/ApplicationCommandOptionChannelTypesMixin.js';
const namePredicate = z
.string()
.min(1)
.max(32)
.regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u);
const descriptionPredicate = z.string().min(1).max(100);
const sharedNameAndDescriptionPredicate = z.object({
name: namePredicate,
name_localizations: localeMapPredicate.optional(),
description: descriptionPredicate,
description_localizations: localeMapPredicate.optional(),
});
const numericMixinNumberOptionPredicate = z.object({
max_value: z.number().safe().optional(),
min_value: z.number().safe().optional(),
});
const numericMixinIntegerOptionPredicate = z.object({
max_value: z.number().safe().int().optional(),
min_value: z.number().safe().int().optional(),
});
const channelMixinOptionPredicate = z.object({
channel_types: z
.union(
ApplicationCommandOptionAllowedChannelTypes.map((type) => z.literal(type)) as unknown as [
ZodTypeAny,
ZodTypeAny,
...ZodTypeAny[],
],
)
.array()
.optional(),
});
const autocompleteMixinOptionPredicate = z.object({
autocomplete: z.literal(true),
choices: z.union([z.never(), z.never().array(), z.undefined()]),
});
const choiceValueStringPredicate = z.string().min(1).max(100);
const choiceValueNumberPredicate = z.number().safe();
const choiceBasePredicate = z.object({
name: choiceValueStringPredicate,
name_localizations: localeMapPredicate.optional(),
});
const choiceStringPredicate = choiceBasePredicate.extend({
value: choiceValueStringPredicate,
});
const choiceNumberPredicate = choiceBasePredicate.extend({
value: choiceValueNumberPredicate,
});
const choiceBaseMixinPredicate = z.object({
autocomplete: z.literal(false).optional(),
});
const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({
choices: choiceStringPredicate.array().max(25).optional(),
});
const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({
choices: choiceNumberPredicate.array().max(25).optional(),
});
const basicOptionTypes = [
ApplicationCommandOptionType.Attachment,
ApplicationCommandOptionType.Boolean,
ApplicationCommandOptionType.Channel,
ApplicationCommandOptionType.Integer,
ApplicationCommandOptionType.Mentionable,
ApplicationCommandOptionType.Number,
ApplicationCommandOptionType.Role,
ApplicationCommandOptionType.String,
ApplicationCommandOptionType.User,
] as const;
const basicOptionTypesPredicate = z.union(
basicOptionTypes.map((type) => z.literal(type)) as unknown as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]],
);
export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({
required: z.boolean().optional(),
type: basicOptionTypesPredicate,
});
const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
autocompleteMixinOptionPredicate,
choiceStringMixinPredicate,
]);
const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
autocompleteMixinOptionPredicate,
choiceNumberMixinPredicate,
]);
export const channelOptionPredicate = basicOptionPredicate.merge(channelMixinOptionPredicate);
export const integerOptionPredicate = basicOptionPredicate
.merge(numericMixinIntegerOptionPredicate)
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
export const numberOptionPredicate = basicOptionPredicate
.merge(numericMixinNumberOptionPredicate)
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
export const stringOptionPredicate = basicOptionPredicate
.extend({
max_length: z.number().min(0).max(6_000).optional(),
min_length: z.number().min(1).max(6_000).optional(),
})
.and(autocompleteOrStringChoicesMixinOptionPredicate);
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
contexts: z.array(z.nativeEnum(InteractionContextType)).optional(),
default_member_permissions: memberPermissionsPredicate.optional(),
integration_types: z.array(z.nativeEnum(ApplicationIntegrationType)).optional(),
nsfw: z.boolean().optional(),
});
// Because you can only add options via builders, there's no need to validate whole objects here otherwise
const chatInputCommandOptionsPredicate = z.union([
z.object({ type: basicOptionTypesPredicate }).array(),
z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(),
z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(),
]);
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
options: chatInputCommandOptionsPredicate.optional(),
});
export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
options: z
.array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }))
.min(1)
.max(25),
});
export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.Subcommand),
options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25),
});

View File

@@ -1,40 +0,0 @@
import { ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { validate } from '../../../util/validation.js';
import { CommandBuilder } from '../Command.js';
import { SharedNameAndDescription } from '../SharedNameAndDescription.js';
import { chatInputCommandPredicate } from './Assertions.js';
import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js';
import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js';
/**
* A builder that creates API-compatible JSON data for chat input commands.
*
* @mixes CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>
* @mixes SharedChatInputCommandOptions
* @mixes SharedNameAndDescription
* @mixes SharedChatInputCommandSubcommands
*/
export class ChatInputCommandBuilder extends Mixin(
CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>,
SharedChatInputCommandOptions,
SharedNameAndDescription,
SharedChatInputCommandSubcommands,
) {
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public toJSON(validationOverride?: boolean): RESTPostAPIChatInputApplicationCommandsJSONBody {
const { options, ...rest } = this.data;
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
type: ApplicationCommandType.ChatInput,
options: options?.map((option) => option.toJSON(validationOverride)),
};
validate(chatInputCommandPredicate, data, validationOverride);
return data;
}
}

View File

@@ -1,107 +0,0 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
APIApplicationCommandSubcommandOption,
APIApplicationCommandSubcommandGroupOption,
} from 'discord-api-types/v10';
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js';
import { resolveBuilder } from '../../../util/resolveBuilder.js';
import { validate } from '../../../util/validation.js';
import type { SharedNameAndDescriptionData } from '../SharedNameAndDescription.js';
import { SharedNameAndDescription } from '../SharedNameAndDescription.js';
import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js';
import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js';
export interface ChatInputCommandSubcommandGroupData {
options?: ChatInputCommandSubcommandBuilder[];
}
/**
* Represents a folder for subcommands.
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
*/
export class ChatInputCommandSubcommandGroupBuilder
extends SharedNameAndDescription
implements JSONEncodable<APIApplicationCommandSubcommandGroupOption>
{
protected declare readonly data: ChatInputCommandSubcommandGroupData & SharedNameAndDescriptionData;
public get options(): readonly ChatInputCommandSubcommandBuilder[] {
return (this.data.options ??= []);
}
/**
* Adds a new subcommand to this group.
*
* @param input - A function that returns a subcommand builder or an already built builder
*/
public addSubcommands(
...input: RestOrArray<
| ChatInputCommandSubcommandBuilder
| ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder)
>
) {
const normalized = normalizeArray(input);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const result = normalized.map((builder) => resolveBuilder(builder, ChatInputCommandSubcommandBuilder));
this.data.options ??= [];
this.data.options.push(...result);
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandGroupOption {
const { options, ...rest } = this.data;
const data = {
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandGroupOption, 'type'>),
type: ApplicationCommandOptionType.SubcommandGroup as const,
options: options?.map((option) => option.toJSON(validationOverride)) ?? [],
};
validate(chatInputCommandSubcommandGroupPredicate, data, validationOverride);
return data;
}
}
/**
* A builder that creates API-compatible JSON data for chat input command subcommands.
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
*/
export class ChatInputCommandSubcommandBuilder
extends Mixin(SharedNameAndDescription, SharedChatInputCommandOptions)
implements JSONEncodable<APIApplicationCommandSubcommandOption>
{
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandOption {
const { options, ...rest } = this.data;
const data = {
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandOption, 'type'>),
type: ApplicationCommandOptionType.Subcommand as const,
options: options?.map((option) => option.toJSON(validationOverride)) ?? [],
};
validate(chatInputCommandSubcommandPredicate, data, validationOverride);
return data;
}
}

View File

@@ -1,47 +0,0 @@
import type { APIApplicationCommandIntegerOption } from 'discord-api-types/v10';
export interface ApplicationCommandNumericOptionMinMaxValueData
extends Pick<APIApplicationCommandIntegerOption, 'max_value' | 'min_value'> {}
/**
* This mixin holds minimum and maximum symbols used for options.
*/
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
protected declare readonly data: ApplicationCommandNumericOptionMinMaxValueData;
/**
* Sets the maximum number value of this option.
*
* @param max - The maximum value this option can be
*/
public setMaxValue(max: number): this {
this.data.max_value = max;
return this;
}
/**
* Removes the maximum number value of this option.
*/
public clearMaxValue(): this {
this.data.max_value = undefined;
return this;
}
/**
* Sets the minimum number value of this option.
*
* @param min - The minimum value this option can be
*/
public setMinValue(min: number): this {
this.data.min_value = min;
return this;
}
/**
* Removes the minimum number value of this option.
*/
public clearMinValue(): this {
this.data.min_value = undefined;
return this;
}
}

View File

@@ -1,52 +0,0 @@
import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray';
export const ApplicationCommandOptionAllowedChannelTypes = [
ChannelType.GuildText,
ChannelType.GuildVoice,
ChannelType.GuildCategory,
ChannelType.GuildAnnouncement,
ChannelType.AnnouncementThread,
ChannelType.PublicThread,
ChannelType.PrivateThread,
ChannelType.GuildStageVoice,
ChannelType.GuildForum,
ChannelType.GuildMedia,
] as const;
/**
* Allowed channel types used for a channel option.
*/
export type ApplicationCommandOptionAllowedChannelTypes = (typeof ApplicationCommandOptionAllowedChannelTypes)[number];
export interface ApplicationCommandOptionChannelTypesData
extends Pick<APIApplicationCommandChannelOption, 'channel_types'> {}
/**
* This mixin holds channel type symbols used for options.
*/
export class ApplicationCommandOptionChannelTypesMixin {
protected declare readonly data: ApplicationCommandOptionChannelTypesData;
/**
* Adds channel types to this option.
*
* @param channelTypes - The channel types
*/
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
this.data.channel_types ??= [];
this.data.channel_types.push(...normalizeArray(channelTypes));
return this;
}
/**
* Sets the channel types for this option.
*
* @param channelTypes - The channel types
*/
public setChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
this.data.channel_types = normalizeArray(channelTypes);
return this;
}
}

View File

@@ -1,29 +0,0 @@
import type {
APIApplicationCommandIntegerOption,
APIApplicationCommandNumberOption,
APIApplicationCommandStringOption,
} from 'discord-api-types/v10';
export type AutocompletableOptions =
| APIApplicationCommandIntegerOption
| APIApplicationCommandNumberOption
| APIApplicationCommandStringOption;
export interface ApplicationCommandOptionWithAutocompleteData extends Pick<AutocompletableOptions, 'autocomplete'> {}
/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithAutocompleteMixin {
protected declare readonly data: ApplicationCommandOptionWithAutocompleteData;
/**
* Whether this option uses autocomplete.
*
* @param autocomplete - Whether this option should use autocomplete
*/
public setAutocomplete(autocomplete = true): this {
this.data.autocomplete = autocomplete;
return this;
}
}

View File

@@ -1,38 +0,0 @@
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
// Unlike other places, we're not `Pick`ing from discord-api-types. The union includes `[]` and it breaks everything.
export interface ApplicationCommandOptionWithChoicesData {
choices?: APIApplicationCommandOptionChoice<number | string>[];
}
/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithChoicesMixin<ChoiceType extends number | string> {
protected declare readonly data: ApplicationCommandOptionWithChoicesData;
/**
* Adds multiple choices to this option.
*
* @param choices - The choices to add
*/
public addChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
const normalizedChoices = normalizeArray(choices);
this.data.choices ??= [];
this.data.choices.push(...normalizedChoices);
return this;
}
/**
* Sets multiple choices for this option.
*
* @param choices - The choices to set
*/
public setChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
this.data.choices = normalizeArray(choices);
return this;
}
}

View File

@@ -1,200 +0,0 @@
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
import { resolveBuilder } from '../../../../util/resolveBuilder.js';
import type { ApplicationCommandOptionBase } from '../options/ApplicationCommandOptionBase.js';
import { ChatInputCommandAttachmentOption } from '../options/attachment.js';
import { ChatInputCommandBooleanOption } from '../options/boolean.js';
import { ChatInputCommandChannelOption } from '../options/channel.js';
import { ChatInputCommandIntegerOption } from '../options/integer.js';
import { ChatInputCommandMentionableOption } from '../options/mentionable.js';
import { ChatInputCommandNumberOption } from '../options/number.js';
import { ChatInputCommandRoleOption } from '../options/role.js';
import { ChatInputCommandStringOption } from '../options/string.js';
import { ChatInputCommandUserOption } from '../options/user.js';
export interface SharedChatInputCommandOptionsData {
options?: ApplicationCommandOptionBase[];
}
/**
* This mixin holds symbols that can be shared in chat input command options.
*
* @typeParam TypeAfterAddingOptions - The type this class should return after adding an option.
*/
export class SharedChatInputCommandOptions {
protected declare readonly data: SharedChatInputCommandOptionsData;
public get options(): readonly ApplicationCommandOptionBase[] {
return (this.data.options ??= []);
}
/**
* Adds boolean options.
*
* @param options - Options to add
*/
public addBooleanOptions(
...options: RestOrArray<
ChatInputCommandBooleanOption | ((builder: ChatInputCommandBooleanOption) => ChatInputCommandBooleanOption)
>
) {
return this.sharedAddOptions(ChatInputCommandBooleanOption, ...options);
}
/**
* Adds user options.
*
* @param options - Options to add
*/
public addUserOptions(
...options: RestOrArray<
ChatInputCommandUserOption | ((builder: ChatInputCommandUserOption) => ChatInputCommandUserOption)
>
) {
return this.sharedAddOptions(ChatInputCommandUserOption, ...options);
}
/**
* Adds channel options.
*
* @param options - Options to add
*/
public addChannelOptions(
...options: RestOrArray<
ChatInputCommandChannelOption | ((builder: ChatInputCommandChannelOption) => ChatInputCommandChannelOption)
>
) {
return this.sharedAddOptions(ChatInputCommandChannelOption, ...options);
}
/**
* Adds role options.
*
* @param options - Options to add
*/
public addRoleOptions(
...options: RestOrArray<
ChatInputCommandRoleOption | ((builder: ChatInputCommandRoleOption) => ChatInputCommandRoleOption)
>
) {
return this.sharedAddOptions(ChatInputCommandRoleOption, ...options);
}
/**
* Adds attachment options.
*
* @param options - Options to add
*/
public addAttachmentOptions(
...options: RestOrArray<
| ChatInputCommandAttachmentOption
| ((builder: ChatInputCommandAttachmentOption) => ChatInputCommandAttachmentOption)
>
) {
return this.sharedAddOptions(ChatInputCommandAttachmentOption, ...options);
}
/**
* Adds mentionable options.
*
* @param options - Options to add
*/
public addMentionableOptions(
...options: RestOrArray<
| ChatInputCommandMentionableOption
| ((builder: ChatInputCommandMentionableOption) => ChatInputCommandMentionableOption)
>
) {
return this.sharedAddOptions(ChatInputCommandMentionableOption, ...options);
}
/**
* Adds string options.
*
* @param options - Options to add
*/
public addStringOptions(
...options: RestOrArray<
ChatInputCommandStringOption | ((builder: ChatInputCommandStringOption) => ChatInputCommandStringOption)
>
) {
return this.sharedAddOptions(ChatInputCommandStringOption, ...options);
}
/**
* Adds integer options.
*
* @param options - Options to add
*/
public addIntegerOptions(
...options: RestOrArray<
ChatInputCommandIntegerOption | ((builder: ChatInputCommandIntegerOption) => ChatInputCommandIntegerOption)
>
) {
return this.sharedAddOptions(ChatInputCommandIntegerOption, ...options);
}
/**
* Adds number options.
*
* @param options - Options to add
*/
public addNumberOptions(
...options: RestOrArray<
ChatInputCommandNumberOption | ((builder: ChatInputCommandNumberOption) => ChatInputCommandNumberOption)
>
) {
return this.sharedAddOptions(ChatInputCommandNumberOption, ...options);
}
/**
* Removes, replaces, or inserts options for this command.
*
* @remarks
* This method behaves similarly
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
*
* It's useful for modifying and adjusting order of the already-existing options for this command.
* @example
* Remove the first option:
* ```ts
* actionRow.spliceOptions(0, 1);
* ```
* @example
* Remove the first n options:
* ```ts
* const n = 4;
* actionRow.spliceOptions(0, n);
* ```
* @example
* Remove the last option:
* ```ts
* actionRow.spliceOptions(-1, 1);
* ```
* @param index - The index to start at
* @param deleteCount - The number of options to remove
* @param options - The replacing option objects
*/
public spliceOptions(index: number, deleteCount: number, ...options: ApplicationCommandOptionBase[]): this {
this.data.options ??= [];
this.data.options.splice(index, deleteCount, ...options);
return this;
}
/**
* Where the actual adding magic happens. ✨
*
* @internal
*/
private sharedAddOptions<OptionBuilder extends ApplicationCommandOptionBase>(
Instance: new () => OptionBuilder,
...options: RestOrArray<OptionBuilder | ((builder: OptionBuilder) => OptionBuilder)>
): this {
const normalized = normalizeArray(options);
const resolved = normalized.map((option) => resolveBuilder(option, Instance));
this.data.options ??= [];
this.data.options.push(...resolved);
return this;
}
}

View File

@@ -1,60 +0,0 @@
import type { RestOrArray } from '../../../../util/normalizeArray.js';
import { normalizeArray } from '../../../../util/normalizeArray.js';
import { resolveBuilder } from '../../../../util/resolveBuilder.js';
import {
ChatInputCommandSubcommandGroupBuilder,
ChatInputCommandSubcommandBuilder,
} from '../ChatInputCommandSubcommands.js';
export interface SharedChatInputCommandSubcommandsData {
options?: (ChatInputCommandSubcommandBuilder | ChatInputCommandSubcommandGroupBuilder)[];
}
/**
* This mixin holds symbols that can be shared in chat input subcommands.
*
* @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group.
*/
export class SharedChatInputCommandSubcommands {
protected declare readonly data: SharedChatInputCommandSubcommandsData;
/**
* Adds subcommand groups to this command.
*
* @param input - Subcommand groups to add
*/
public addSubcommandGroups(
...input: RestOrArray<
| ChatInputCommandSubcommandGroupBuilder
| ((subcommandGroup: ChatInputCommandSubcommandGroupBuilder) => ChatInputCommandSubcommandGroupBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandGroupBuilder));
this.data.options ??= [];
this.data.options.push(...resolved);
return this;
}
/**
* Adds subcommands to this command.
*
* @param input - Subcommands to add
*/
public addSubcommands(
...input: RestOrArray<
| ChatInputCommandSubcommandBuilder
| ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandBuilder));
this.data.options ??= [];
this.data.options.push(...resolved);
return this;
}
}

View File

@@ -1,56 +0,0 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
APIApplicationCommandBasicOption,
APIApplicationCommandOption,
ApplicationCommandOptionType,
} from 'discord-api-types/v10';
import type { z } from 'zod';
import { validate } from '../../../../util/validation.js';
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
import { basicOptionPredicate } from '../Assertions.js';
export interface ApplicationCommandOptionBaseData extends Partial<Pick<APIApplicationCommandOption, 'required'>> {
type: ApplicationCommandOptionType;
}
/**
* The base application command option builder that contains common symbols for application command builders.
*/
export abstract class ApplicationCommandOptionBase
extends SharedNameAndDescription
implements JSONEncodable<APIApplicationCommandBasicOption>
{
protected static readonly predicate: z.ZodTypeAny = basicOptionPredicate;
protected declare readonly data: ApplicationCommandOptionBaseData & SharedNameAndDescriptionData;
public constructor(type: ApplicationCommandOptionType) {
super();
this.data.type = type;
}
/**
* Sets whether this option is required.
*
* @param required - Whether this option should be required
*/
public setRequired(required = true) {
this.data.required = required;
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIApplicationCommandBasicOption {
const clone = structuredClone(this.data);
validate((this.constructor as typeof ApplicationCommandOptionBase).predicate, clone, validationOverride);
return clone as APIApplicationCommandBasicOption;
}
}

View File

@@ -1,11 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command attachment option.
*/
export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Attachment);
}
}

View File

@@ -1,11 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command boolean option.
*/
export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Boolean);
}
}

View File

@@ -1,19 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { channelOptionPredicate } from '../Assertions.js';
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command channel option.
*/
export class ChatInputCommandChannelOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandOptionChannelTypesMixin,
) {
protected static override readonly predicate = channelOptionPredicate;
public constructor() {
super(ApplicationCommandOptionType.Channel);
}
}

View File

@@ -1,23 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { integerOptionPredicate } from '../Assertions.js';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command integer option.
*/
export class ChatInputCommandIntegerOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin<number>,
) {
protected static override readonly predicate = integerOptionPredicate;
public constructor() {
super(ApplicationCommandOptionType.Integer);
}
}

View File

@@ -1,11 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command mentionable option.
*/
export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Mentionable);
}
}

View File

@@ -1,23 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { numberOptionPredicate } from '../Assertions.js';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command number option.
*/
export class ChatInputCommandNumberOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin<number>,
) {
protected static override readonly predicate = numberOptionPredicate;
public constructor() {
super(ApplicationCommandOptionType.Number);
}
}

View File

@@ -1,11 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command role option.
*/
export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Role);
}
}

View File

@@ -1,65 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { stringOptionPredicate } from '../Assertions.js';
import type { ApplicationCommandOptionWithAutocompleteData } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import type { ApplicationCommandOptionWithChoicesData } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
import type { ApplicationCommandOptionBaseData } from './ApplicationCommandOptionBase.js';
/**
* A chat input command string option.
*/
export class ChatInputCommandStringOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin<string>,
) {
protected static override readonly predicate = stringOptionPredicate;
protected declare readonly data: ApplicationCommandOptionBaseData &
ApplicationCommandOptionWithAutocompleteData &
ApplicationCommandOptionWithChoicesData &
Partial<Pick<APIApplicationCommandStringOption, 'max_length' | 'min_length'>>;
public constructor() {
super(ApplicationCommandOptionType.String);
}
/**
* Sets the maximum length of this string option.
*
* @param max - The maximum length this option can be
*/
public setMaxLength(max: number): this {
this.data.max_length = max;
return this;
}
/**
* Clears the maximum length of this string option.
*/
public clearMaxLength(): this {
this.data.max_length = undefined;
return this;
}
/**
* Sets the minimum length of this string option.
*
* @param min - The minimum length this option can be
*/
public setMinLength(min: number): this {
this.data.min_length = min;
return this;
}
/**
* Clears the minimum length of this string option.
*/
public clearMinLength(): this {
this.data.min_length = undefined;
return this;
}
}

View File

@@ -1,11 +0,0 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command user option.
*/
export class ChatInputCommandUserOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.User);
}
}

View File

@@ -1,30 +0,0 @@
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10';
import { z } from 'zod';
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
const namePredicate = z
.string()
.min(1)
.max(32)
// eslint-disable-next-line prefer-named-capture-group
.regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u);
const contextsPredicate = z.array(z.nativeEnum(InteractionContextType));
const integrationTypesPredicate = z.array(z.nativeEnum(ApplicationIntegrationType));
const baseContextMenuCommandPredicate = z.object({
contexts: contextsPredicate.optional(),
default_member_permissions: memberPermissionsPredicate.optional(),
name: namePredicate,
name_localizations: localeMapPredicate.optional(),
integration_types: integrationTypesPredicate.optional(),
nsfw: z.boolean().optional(),
});
export const userCommandPredicate = baseContextMenuCommandPredicate.extend({
type: z.literal(ApplicationCommandType.User),
});
export const messageCommandPredicate = baseContextMenuCommandPredicate.extend({
type: z.literal(ApplicationCommandType.Message),
});

View File

@@ -1,29 +0,0 @@
import type { ApplicationCommandType, RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { CommandBuilder } from '../Command.js';
import { SharedName } from '../SharedName.js';
/**
* The type a context menu command can be.
*/
export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User;
/**
* A builder that creates API-compatible JSON data for context menu commands.
*/
export abstract class ContextMenuCommandBuilder extends Mixin(
CommandBuilder<RESTPostAPIContextMenuApplicationCommandsJSONBody>,
SharedName,
) {
protected override readonly data: Partial<RESTPostAPIContextMenuApplicationCommandsJSONBody>;
public constructor(data: Partial<RESTPostAPIContextMenuApplicationCommandsJSONBody> = {}) {
super();
this.data = structuredClone(data);
}
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public abstract override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody;
}

View File

@@ -1,16 +0,0 @@
import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { validate } from '../../../util/validation.js';
import { messageCommandPredicate } from './Assertions.js';
import { ContextMenuCommandBuilder } from './ContextMenuCommand.js';
export class MessageContextCommandBuilder extends ContextMenuCommandBuilder {
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody {
const data = { ...structuredClone(this.data), type: ApplicationCommandType.Message };
validate(messageCommandPredicate, data, validationOverride);
return data as RESTPostAPIContextMenuApplicationCommandsJSONBody;
}
}

View File

@@ -1,16 +0,0 @@
import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { validate } from '../../../util/validation.js';
import { userCommandPredicate } from './Assertions.js';
import { ContextMenuCommandBuilder } from './ContextMenuCommand.js';
export class UserContextCommandBuilder extends ContextMenuCommandBuilder {
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody {
const data = { ...structuredClone(this.data), type: ApplicationCommandType.User };
validate(userCommandPredicate, data, validationOverride);
return data as RESTPostAPIContextMenuApplicationCommandsJSONBody;
}
}

Some files were not shown because too many files have changed in this diff Show More