diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index f64aa2388402..000000000000
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,12 +0,0 @@
-**Please describe the changes this PR makes and why it should be merged:**
-
-**Status and versioning classification:**
-
-
diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml
index 8d34cc93634f..4a4ec06541b3 100644
--- a/.github/workflows/cleanup-cache.yml
+++ b/.github/workflows/cleanup-cache.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Cleanup caches
run: |
diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml
index d23bce7ad884..3d305eb186fb 100644
--- a/.github/workflows/deploy-website.yml
+++ b/.github/workflows/deploy-website.yml
@@ -14,7 +14,7 @@ jobs:
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Node.js v24
uses: actions/setup-node@v6
diff --git a/.github/workflows/deprecate-version.yml b/.github/workflows/deprecate-version.yml
index f16ef54a4fed..26f7efe69a9e 100644
--- a/.github/workflows/deprecate-version.yml
+++ b/.github/workflows/deprecate-version.yml
@@ -36,7 +36,7 @@ jobs:
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Node.js v24
uses: actions/setup-node@v6
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index b61f566b2098..429f79483aef 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -38,7 +38,7 @@ jobs:
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || '' }}
@@ -56,7 +56,7 @@ jobs:
- name: Checkout main repository
if: ${{ inputs.ref && inputs.ref != 'main' }}
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
path: 'main'
@@ -213,7 +213,7 @@ jobs:
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Node.js v24
uses: actions/setup-node@v6
diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml
index 15ee80bacec1..6b3d959981f7 100644
--- a/.github/workflows/label-sync.yml
+++ b/.github/workflows/label-sync.yml
@@ -15,7 +15,7 @@ jobs:
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Label sync
uses: crazy-max/ghaction-github-labeler@v5
diff --git a/.github/workflows/publish-dev-docker.yml b/.github/workflows/publish-dev-docker.yml
index 11c37e4319c0..550b190f28de 100644
--- a/.github/workflows/publish-dev-docker.yml
+++ b/.github/workflows/publish-dev-docker.yml
@@ -3,29 +3,88 @@ on:
schedule:
- cron: '0 */12 * * *'
workflow_dispatch:
+env:
+ IMAGE_NAME: discordjs/proxy
jobs:
- docker-publish:
- name: Docker publish
- runs-on: ubuntu-latest
- if: github.repository_owner == 'discordjs'
+ build:
+ name: Build ${{ matrix.platform }}
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-24.04-arm
steps:
+ - name: Prepare
+ run: |
+ platform=${{ matrix.platform }}
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
+
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- - name: Install Node.js v24
- uses: actions/setup-node@v6
+ - name: Build and push by digest
+ id: build
+ uses: docker/build-push-action@v6
with:
- node-version: 24
- package-manager-cache: false
+ context: .
+ file: packages/proxy-container/Dockerfile
+ platforms: ${{ matrix.platform }}
+ outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
- - name: Install dependencies
- uses: ./packages/actions/src/pnpmCache
+ - name: Export digest
+ run: |
+ mkdir -p /tmp/digests
+ digest="${{ steps.build.outputs.digest }}"
+ touch "/tmp/digests/${digest#sha256:}"
+
+ - name: Upload digest
+ uses: actions/upload-artifact@v5
+ with:
+ name: digests-${{ env.PLATFORM_PAIR }}
+ path: /tmp/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
+ merge:
+ name: Create and push manifest list
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v6
+ with:
+ path: /tmp/digests
+ pattern: digests-*
+ merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
- run: echo ${{ secrets.DOCKER_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
+
+ - name: Create manifest list and push
+ working-directory: /tmp/digests
+ run: |
+ docker buildx imagetools create -t ${{ env.IMAGE_NAME }}:latest \
+ $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *)
- - name: Build & push docker image
- run: docker build -f packages/proxy-container/Dockerfile -t discordjs/proxy:latest --push .
+ - name: Inspect image
+ run: |
+ docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:latest
diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml
index 58c675a5c3ea..c63b44b040a4 100644
--- a/.github/workflows/publish-dev.yml
+++ b/.github/workflows/publish-dev.yml
@@ -34,7 +34,7 @@ jobs:
private-key: ${{ secrets.DISCORDJS_APP_KEY_RELEASE }}
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
@@ -55,7 +55,7 @@ jobs:
- name: Checkout main repository (non-main ref)
if: ${{ inputs.ref != 'main' }}
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
path: 'main'
diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml
index 5af3a4b05bcf..5b29d28e4ee6 100644
--- a/.github/workflows/publish-docker.yml
+++ b/.github/workflows/publish-docker.yml
@@ -1,28 +1,98 @@
name: Publish docker images
on:
workflow_dispatch:
+env:
+ IMAGE_NAME: discordjs/proxy
jobs:
- docker-publish:
- name: Docker publish
- runs-on: ubuntu-latest
+ build:
+ name: Build ${{ matrix.platform }}
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-24.04-arm
steps:
+ - name: Prepare
+ run: |
+ platform=${{ matrix.platform }}
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
+
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
- - name: Install Node.js v24
- uses: actions/setup-node@v6
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
+
+ - name: Build and push by digest
+ id: build
+ uses: docker/build-push-action@v6
with:
- node-version: 24
- package-manager-cache: false
+ context: .
+ file: packages/proxy-container/Dockerfile
+ platforms: ${{ matrix.platform }}
+ outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
- - name: Install dependencies
- uses: ./packages/actions/src/pnpmCache
+ - name: Export digest
+ run: |
+ mkdir -p /tmp/digests
+ digest="${{ steps.build.outputs.digest }}"
+ touch "/tmp/digests/${digest#sha256:}"
+
+ - name: Upload digest
+ uses: actions/upload-artifact@v5
+ with:
+ name: digests-${{ env.PLATFORM_PAIR }}
+ path: /tmp/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
+ merge:
+ name: Create and push manifest list
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v6
+ with:
+ path: /tmp/digests
+ pattern: digests-*
+ merge-multiple: true
+
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Get Major Version
+ id: version
+ run: |
+ FULL_VER=$(jq --raw-output '.version' packages/proxy-container/package.json)
+ MAJOR=$(echo $FULL_VER | cut -d '.' -f1)
+ echo "major=$MAJOR" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
- run: echo ${{ secrets.DOCKER_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
+
+ - name: Create manifest list and push
+ working-directory: /tmp/digests
+ run: |
+ docker buildx imagetools create -t ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.major }} \
+ $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *)
- - name: Build & push docker image
- run: docker build -f packages/proxy-container/Dockerfile -t discordjs/proxy:$(cut -d '.' -f1 <<< $(jq --raw-output '.version' packages/proxy-container/package.json)) --push .
+ - name: Inspect image
+ run: |
+ docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.major }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e3faf8c33f96..092824237c51 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -55,7 +55,7 @@ jobs:
permission-contents: write
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
ref: ${{ inputs.ref || '' }}
@@ -75,7 +75,7 @@ jobs:
- name: Checkout main repository
if: ${{ inputs.ref && inputs.ref != 'main' }}
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
path: 'main'
diff --git a/.github/workflows/remove-tag.yml b/.github/workflows/remove-tag.yml
index 55987b88d8d7..0cfca401d567 100644
--- a/.github/workflows/remove-tag.yml
+++ b/.github/workflows/remove-tag.yml
@@ -35,7 +35,7 @@ jobs:
fail-fast: false
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v6
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 67581de9f672..4e2e0a904879 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -15,7 +15,7 @@ jobs:
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0
diff --git a/.github/workflows/upload-readmes.yml b/.github/workflows/upload-readmes.yml
new file mode 100644
index 000000000000..88f4c4aca8e6
--- /dev/null
+++ b/.github/workflows/upload-readmes.yml
@@ -0,0 +1,42 @@
+name: Upload README.md files
+on:
+ push:
+ branches:
+ - 'main'
+ paths:
+ - 'packages/*/README.md'
+ workflow_dispatch:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
+ cancel-in-progress: true
+jobs:
+ upload-readmes:
+ name: Upload README.md files
+ runs-on: ubuntu-latest
+ env:
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+ if: github.repository_owner == 'discordjs'
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Install Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ package-manager-cache: false
+
+ - name: Install dependencies
+ uses: ./packages/actions/src/pnpmCache
+
+ - name: Build dependencies
+ run: pnpm --filter @discordjs/actions... run build
+
+ - name: Upload README.md files
+ env:
+ CF_R2_READMES_ACCESS_KEY_ID: ${{ secrets.CF_R2_READMES_ACCESS_KEY_ID }}
+ CF_R2_READMES_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_READMES_SECRET_ACCESS_KEY }}
+ CF_R2_READMES_BUCKET: ${{ secrets.CF_R2_READMES_BUCKET }}
+ CF_R2_READMES_URL: ${{ secrets.CF_R2_READMES_URL }}
+ uses: ./packages/actions/src/uploadReadmeFiles
diff --git a/apps/guide/content/docs/legacy/interactions/images/modal-example.png b/apps/guide/content/docs/legacy/interactions/images/modal-example.png
index 9cee3d67d630..7b494dcd257c 100644
Binary files a/apps/guide/content/docs/legacy/interactions/images/modal-example.png and b/apps/guide/content/docs/legacy/interactions/images/modal-example.png differ
diff --git a/apps/guide/content/docs/legacy/interactions/images/selectephem.png b/apps/guide/content/docs/legacy/interactions/images/selectephem.png
deleted file mode 100644
index 2f109b358862..000000000000
Binary files a/apps/guide/content/docs/legacy/interactions/images/selectephem.png and /dev/null differ
diff --git a/apps/guide/content/docs/legacy/interactions/modals.mdx b/apps/guide/content/docs/legacy/interactions/modals.mdx
index 4bdc32bc4a6f..2367dfac7faf 100644
--- a/apps/guide/content/docs/legacy/interactions/modals.mdx
+++ b/apps/guide/content/docs/legacy/interactions/modals.mdx
@@ -2,30 +2,27 @@
title: Modals
---
-With modals you can create pop-up forms that allow users to provide you with formatted inputs through submissions. We'll cover how to create, show, and receive modal forms using discord.js!
+Modals are pop-up forms that allow you to prompt users for additional input. This form-like interaction response blocks the user from interacting with Discord until the modal is submitted or dismissed. In this section, we will cover how to create, show, and receive modals using discord.js!
- This page is a follow-up to the [interactions (slash commands) page](../slash-commands/advanced-creation). Please
- carefully read that section first, so that you can understand the methods used in this section.
+ This page is a follow-up to the [interactions (slash commands) page](../slash-commands/advanced-creation). Reading
+ that page first will help you understand the concepts introduced in this page.
## Building and responding with modals
-Unlike message components, modals aren't strictly components themselves. They're a callback structure used to respond to interactions.
+With the `ModalBuilder` class, discord.js offers a convenient way to build modals step by step using setters and callbacks.
- You can have a maximum of five `ActionRowBuilder`s per modal builder, and one `TextInputBuilder` within an
- `ActionRowBuilder`. Currently, you can only use `TextInputBuilder`s in modal action rows builders.
+ You can have a maximum of five top-level components per modal, each of which can be a label or a text display
+ component.
-To create a modal you construct a new `ModalBuilder`. You can then use the setters to add the custom id and title.
-
```js
const { Events, ModalBuilder } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
-
if (interaction.commandName === 'ping') {
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
@@ -35,105 +32,321 @@ client.on(Events.InteractionCreate, async (interaction) => {
```
- The custom id is a developer-defined string of up to 100 characters. Use this field to ensure you can uniquely define
- all incoming interactions from your modals!
+ The `customId` is a developer-defined string of up to 100 characters and uniquely identifies this modal instance. You
+ can use it to differentiate incoming interactions.
-The next step is to add the input fields in which users responding can enter free-text. Adding inputs is similar to adding components to messages.
+The next step is adding components to the modal, which may either request input or present information.
-At the end, we then call `ChatInputCommandInteraction#showModal` to display the modal to the user.
+### Label
-
-If you're using typescript you'll need to specify the type of components your action row holds. This can be done by specifying the generic parameter in `ActionRowBuilder`:
+Label components wrap around other modal components (text input, select menus, etc.) to add a label and description to it.
+Since labels are not stand-alone components, we will use this example label to wrap a text input component in the next section:
+
+```js
+const { LabelBuilder, ModalBuilder } = require('discord.js');
-```diff
-- new ActionRowBuilder()
-+ new ActionRowBuilder()
+client.on(Events.InteractionCreate, async (interaction) => {
+ if (!interaction.isChatInputCommand()) return;
+ if (interaction.commandName === 'ping') {
+ // [!code focus:11]
+ // Create the modal
+ const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
+
+ // [!code ++:5]
+ const hobbiesLabel = new LabelBuilder()
+ // The label is a large header text that identifies the interactive component for the user.
+ .setLabel('What are some of your favorite hobbies?')
+ // The description is an additional optional subtext that aids the label.
+ .setDescription('Activities you like to participate in');
+
+ // [!code ++:2]
+ // Add label to the modal
+ modal.addLabelComponents(hobbiesLabel);
+ }
+});
```
+
+ The `label` field has a max length of 45 characters. The `description` field has a max length of 100 characters.
+### Text input
+
+Text input components prompt users for single or multi line free-form text.
+
```js
-const { ActionRowBuilder, Events, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
+const { LabelBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
-
if (interaction.commandName === 'ping') {
+ // [!code focus:10]
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
- // Add components to modal
-
- // Create the text input components
- const favoriteColorInput = new TextInputBuilder()
- .setCustomId('favoriteColorInput')
- // The label is the prompt the user sees for this input
- .setLabel("What's your favorite color?")
- // Short means only a single line of text
- .setStyle(TextInputStyle.Short);
-
+ // [!code ++:6]
const hobbiesInput = new TextInputBuilder()
.setCustomId('hobbiesInput')
+ // Short means a single line of text.
+ .setStyle(TextInputStyle.Short)
+ // Placeholder text displayed inside the text input box
+ .setPlaceholder('card games, films, books, etc.');
+
+ // [!code focus:10]
+ const hobbiesLabel = new LabelBuilder()
+ // The label is a large header that identifies the interactive component for the user.
.setLabel("What's some of your favorite hobbies?")
- // Paragraph means multiple lines of text.
- .setStyle(TextInputStyle.Paragraph);
+ // The description is an additional optional subtext that aids the label.
+ .setDescription('Activities you like to participate in')
+ // [!code ++:2]
+ // Set text input as the component of the label
+ .setTextInputComponent(hobbiesInput);
+
+ // Add the label to the modal
+ modal.addLabelComponents(hobbiesLabel);
+ }
+});
+```
+
+#### Input styles
+
+Discord offers two different input styles:
+
+- `Short`, a single-line text entry
+- `Paragraph`, a multi-line text entry
+
+#### Input properties
+
+A text input field can be customized in a number of ways to apply validation or set default values via the following `TextInputBuilder` methods:
+
+```js
+const input = new TextInputBuilder()
+ // Set the component id (this is not the custom id)
+ .setId(1)
+ // Set the maximum number of characters allowed
+ .setMaxLength(1_000)
+ // Set the minimum number of characters required for submission
+ .setMinLength(10)
+ // Set a default value to prefill the text input
+ .setValue('Default')
+ // Require a value in this text input field (defaults to true)
+ .setRequired(true);
+```
- // An action row only holds one text input,
- // so you need one action row per text input.
- const firstActionRow = new ActionRowBuilder().addComponents(favoriteColorInput);
- const secondActionRow = new ActionRowBuilder().addComponents(hobbiesInput);
+
+ The `id` field is used to differentiate components within interactions (which text input, selection, etc.). In
+ contrast, the `customId` covered earlier identifies the interaction (which modal, command, etc.).
+
+
+### Select menu
+
+Select menus allow you to limit user input to a preselected list of values. Discord also offers select menus linked directly to native Discord entities like users, roles, and channels.
+Since they behave very similarly to how they do in messages, please refer to the [corresponding guide page](../interactive-components/select-menus) for more information on configuring select menus.
+
+Here again, you wrap the select menu with a label component to add context to the selection and add the label to the modal:
- // Add inputs to the modal
- modal.addComponents(firstActionRow, secondActionRow);
+```js
+// ...
+
+client.on(Events.InteractionCreate, async (interaction) => {
+ if (!interaction.isChatInputCommand()) return;
+ if (interaction.commandName === 'ping') {
+ // Create the modal
+ const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
- // Show the modal to the user
- await interaction.showModal(modal); // [!code word:showModal]
+ // ...
+
+ // [!code focus:24]
+ // [!code ++:23]
+ const favoriteStarterSelect = new StringSelectMenuBuilder()
+ .setCustomId('starter')
+ .setPlaceholder('Make a selection!')
+ // Modal only property on select menus to prevent submission, defaults to true
+ .setRequired(true)
+ .addOptions(
+ // String select menu options
+ new StringSelectMenuOptionBuilder()
+ // Label displayed to user
+ .setLabel('Bulbasaur')
+ // Description of option
+ .setDescription('The dual-type Grass/Poison Seed Pokémon.')
+ // Value returned to you in modal submission
+ .setValue('bulbasaur'),
+ new StringSelectMenuOptionBuilder()
+ .setLabel('Charmander')
+ .setDescription('The Fire-type Lizard Pokémon.')
+ .setValue('charmander'),
+ new StringSelectMenuOptionBuilder()
+ .setLabel('Squirtle')
+ .setDescription('The Water-type Tiny Turtle Pokémon.')
+ .setValue('squirtle'),
+ );
+
+ // ...
+
+ // [!code focus:4]
+ // [!code ++:4]
+ const favoriteStarterLabel = new LabelBuilder()
+ .setLabel("What's your favorite Gen 1 Pokémon starter?")
+ // Set string select menu as component of the label
+ .setStringSelectMenuComponent(favoriteStarterSelect);
+
+ // [!code focus:3]
+ // Add labels to modal
+ modal.addLabelComponents(hobbiesLabel); // [!code --]
+ modal.addLabelComponents(hobbiesLabel, favoriteStarterLabel); // [!code ++]
}
});
```
-Restart your bot and invoke the `/ping` command again. You should see a popup form resembling the image below:
+### Text display
-
+Text display components offer you a way to give additional context to the user that doesn't fit into labels or isn't directly connected to any specific input field.
+
+```js
+// ...
+
+client.on(Events.InteractionCreate, async (interaction) => {
+ if (!interaction.isChatInputCommand()) return;
+ if (interaction.commandName === 'ping') {
+ // Create the modal
+ const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
+
+ // ...
+
+ // [!code focus:3]
+ // [!code ++:3]
+ const text = new TextDisplayBuilder().setContent(
+ 'Text that could not fit in to a label or description\n-# Markdown can also be used',
+ );
+
+ // [!code focus:5]
+ // Add components to modal
+ modal
+ // [!code --]
+ .addLabelComponents(hobbiesLabel, favoriteStarterLabel);
+ // [!code ++:2]
+ .addLabelComponents(hobbiesLabel, favoriteStarterLabel)
+ .addTextDisplayComponents(text);
+ }
+});
+```
+
+### File upload
+
+File upload components allow you to prompt the user to upload a file from their system.
- Showing a modal must be the first response to an interaction. You cannot `defer()` or `deferUpdate()` then show a
- modal later.
+ Discord **does not send the file data** itself in the resulting interaction. You will have to download it from
+ Discords CDN to process and validate it. Do not execute arbitrary code people upload via your app!
-### Input styles
+```js
+// ...
+
+client.on(Events.InteractionCreate, async (interaction) => {
+ if (!interaction.isChatInputCommand()) return;
+ if (interaction.commandName === 'ping') {
+ // Create the modal
+ const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
+
+ // ...
-Currently there are two different input styles available:
+ // [!code focus:2]
+ // [!code ++]
+ const pictureOfTheWeekUpload = new FileUploadBuilder().setCustomId('picture');
-- `Short`, a single-line text entry;
-- `Paragraph`, a multi-line text entry similar to the HTML `
-### String select menu options
+## String select menu options
String select menu options are custom-defined by the user, as shown in the example above. At a minimum, each option must have it's `label` and `value` defined. The label is shown to the user, while the value is included in the interaction sent to the bot.
diff --git a/apps/guide/content/docs/legacy/popular-topics/display-components.mdx b/apps/guide/content/docs/legacy/popular-topics/display-components.mdx
index dc9b363ca39d..269c5934822f 100644
--- a/apps/guide/content/docs/legacy/popular-topics/display-components.mdx
+++ b/apps/guide/content/docs/legacy/popular-topics/display-components.mdx
@@ -32,6 +32,9 @@ Text Display components let you add markdown-formatted text to your message and
Sending user and role mentions in text display components **will notify users and roles**! You can and should control
mentions with the `allowedMentions` message option.
+
+ Text display components can be used in modals. See the [modal guide](../interactions/modals#text-display) for usage.
+
The example below shows how you can send a Text Display component in a channel.
diff --git a/apps/guide/content/docs/meta.json b/apps/guide/content/docs/meta.json
index 109f9a4e4ff5..bc81a15aa552 100644
--- a/apps/guide/content/docs/meta.json
+++ b/apps/guide/content/docs/meta.json
@@ -1,3 +1,3 @@
{
- "pages": ["legacy", "voice"]
+ "pages": ["legacy", "v15", "voice"]
}
diff --git a/apps/guide/content/docs/v15/index.mdx b/apps/guide/content/docs/v15/index.mdx
new file mode 100644
index 000000000000..30a891b248b2
--- /dev/null
+++ b/apps/guide/content/docs/v15/index.mdx
@@ -0,0 +1,359 @@
+---
+title: Updating to v15
+icon: ArrowDownToLine
+---
+
+import { Github } from 'lucide-react';
+
+
+ **Version 15 is in a pre-release** state, but should be usable!
+ That being said, we do not recommend you update your production instance without careful and thorough testing!
+ Please report any bugs you experience at our GitHub repository:
+
+ https://github.com/discordjs/discord.js/issues
+
+
+
+## Before you start
+
+Make sure you're using the latest LTS version of Node. To check your Node version, use `node --version` in your terminal or command prompt, and if it's not high enough, update it! There are many resources online to help you with this step based on your host system.
+
+## Breaking Changes
+
+### ActionRow
+
+`ActionRow.from()` has been removed. Use `ActionRowBuilder.from()` instead.
+
+### ApplicationCommand
+
+`ApplicationCommand#dmPermission` and `ApplicationCommand#setDMPermission()` have been removed. This was legacy functionality for commands—use contexts instead.
+
+### ApplicationCommandManager
+
+`ApplicationCommandManager#fetch()` method has been updated for consistency with other managers. Previously, it accepted two parameters: `id` (a snowflake or an options object) and an `options` object. Now, it only accepts a single `options` argument, which can be a snowflake or an options object that may include an `id` property.
+
+### AnnouncementChannel
+
+`AnnouncementChannel#addFollower()` now returns `FollowedChannelData` instead of a snowflake. This helps to expose the created webhook id in the target channel.
+
+### BaseInteraction
+
+`BaseInteraction#isAnySelectMenu()` has been removed. Use `BaseInteraction#isSelectMenu()` instead, which has been repurposed to accept all select menu component types.
+
+### Client
+
+#### Emojis
+
+`Client#emojis` has been removed due to confusion with the introduction of application emojis and performance impact. Use the `resolveGuildEmoji()` utility function to get a cached guild emoji.
+
+#### Ping
+
+`Client#ping` has been added to replace the old `WebSocketManager#ping`. This will be `null` when the heartbeat from the gateway is yet to be received.
+
+#### Premium sticker packs
+
+`Client#fetchPremiumStickerPacks()` has been removed. Use `Client#fetchStickerPacks()` instead.
+
+#### Ready event
+
+`client.on("")` has been removed. `"clientReady"` is the replacement. If you used `client.on(Events.ClientReady)`, you do not need to change anything.
+
+#### Shard disconnect event
+
+`client.on("shardDisconnect")` has been removed as the WebSocket manager replaces this functionality.
+
+#### Shard error event
+
+`client.on("shardError")` has been removed as the WebSocket manager replaces this functionality.
+
+#### Shard ready event
+
+`client.on("shardReady")` has been removed as the WebSocket manager replaces this functionality.
+
+#### Shard reconnecting event
+
+`client.on("shardReconnecting")` has been removed as the WebSocket manager replaces this functionality.
+
+#### Shard resume event
+
+`client.on("shardResume")` has been removed as the WebSocket manager replaces this functionality.
+
+#### Webhook update event
+
+`client.on("webhookUpdate")` has been removed. `"webhooksUpdate"` is the replacement. If you used `client.on(Events.WebhooksUpdate)`, you do not need to change anything.
+
+#### WebSocket
+
+The underlying WebSocket behaviour has changed. In version 14, this was a non-breaking implementation of [ws](https://discord.js.org/docs/packages/ws/stable). Now, it is fully integrated. See these pull requests for more information:
+
+- [discordjs/discord.js#10420](https://github.com/discordjs/discord.js/pull/10420)
+- [discordjs/discord.js#10556](https://github.com/discordjs/discord.js/pull/10556)
+
+### ClientEvents
+
+`ClientEvents` type has been removed. Use `ClientEventTypes` instead. This change ensures consistency with the rest of the event types across the library.
+
+### ClientOptions
+
+Removed `ClientOptions#shards` and `ClientOptions#shardCount` in favor of `ClientOptions#ws#shardIds` and `ClientOptions#ws#shardCount`.
+
+### ClientUser
+
+`ClientUser#setPresence()` now returns a promise which resolves when the gateway call was sent successfully.
+
+### ClientPresence
+
+`ClientPresence#set()` now returns a promise which resolves when the gateway call was sent successfully.
+
+### CommandInteractionOptionResolver
+
+`CommandInteractionOptionResolver#getFocused()`'s parameter has been removed. `AutocompleteFocusedOption` will always be returned.
+
+### Constants
+
+`DeletableMessageTypes` has been removed. Use `UndeletableMessageTypes` instead.
+
+### DiscordjsErrorCodes
+
+The following error codes have been removed as they either have no use or are handled in another package instead:
+
+- `WSCloseRequested`
+- `WSConnectionExists`
+- `WSNotOpen`
+- `ManagerDestroyed`
+- `ShardingInvalid`
+- `ShardingRequired`
+- `InvalidIntents`
+- `DisallowedIntents`
+- `ButtonLabel`
+- `ButtonURL`
+- `ButtonCustomId`
+- `SelectMenuCustomId`
+- `SelectMenuPlaceholder`
+- `SelectOptionLabel`
+- `SelectOptionValue`
+- `SelectOptionDescription`
+- `UserBannerNotFetched`
+- `ImageFormat`
+- `ImageSize`
+- `SplitMaxLen`
+- `MissingManageEmojisAndStickersPermission`
+- `VanityURL`
+- `InteractionEphemeralReplied`
+
+### Emoji
+
+#### Image URL is now dynamic
+
+`Emoji#imageURL()` now dynamically handles the extension. Previously, you would have to do this:
+
+```js
+emoji.imageURL({ extension: emoji.animated ? 'gif' : 'webp' });
+```
+
+Now, you can simply do this:
+
+```js
+emoji.imageURL();
+```
+
+#### Emoji URL getter removal
+
+`Emoji#url` has been removed. To allow more granular control of the returned extension, Use `Emoji#imageURL()` instead.
+
+### EventEmitter
+
+`BaseClient`, `Shard`, `ShardingManager`, and `Collector` now extend `AsyncEventEmitter` instead of `EventEmitter`. This comes from [@vladfrangu/async_event_emitter](https://npmjs.com/package/@vladfrangu/async_event_emitter).
+
+### Events
+
+- `Events.ShardError` has been removed.
+- `Events.ShardReady` has been removed.
+- `Events.ShardReconnecting` has been removed.
+- `Events.ShardResume` has been removed.
+- `Events.WebhooksUpdate` now returns a string of `"webhooksUpdate"`. Previously, it returned `"webhookUpdate"`. This is to bring it in line with the name of Discord's gateway event (`WEBHOOKS_UPDATE`).
+- `Events.ClientReady` now returns a string of `"clientReady"`. Previously, it returned `"ready"`. This is to ensure there's no confusion with Discord's `READY` gateway event.
+
+### Formatters
+
+This utility has been removed. Everything in this class is redundant as all methods of the class can be imported from discord.js directly.
+
+```js
+import { Formatters } from 'discord.js'; // [!code --]
+import { userMention } from 'discord.js'; // [!code ++]
+
+Formatters.userMention('123456789012345678'); // [!code --]
+userMention('123456789012345678'); // [!code ++]
+```
+
+### Guild
+
+Removed `Guild#shard` as WebSocket shards are now handled by @discordjs/ws.
+
+### GuildApplicationCommandManager
+
+`GuildApplicationCommandManager#fetch()` method has been updated for consistency with other managers. Previously, it accepted two parameters: `id` (a snowflake or an options object) and an `options` object. Now, it only accepts a single `options` argument, which can be a snowflake or an options object that may include an `id` property.
+
+### GuildAuditLogs
+
+`GuildAuditLogsEntry.Targets.All` has been removed. It was not being used.
+
+### GuildBanManager
+
+`GuildBanManager#create()` no longer accepts `deleteMessageDays`. This is replaced with `deleteMessageSeconds`.
+
+### GuildChannelManager
+
+`GuildChannelManager#addFollower()` now returns `FollowedChannelData` instead of a snowflake. This helps to expose the created webhook id in the target channel.
+
+### GuildMemberResolvable
+
+`GuildMemberResolvable` type has been removed. It was defined as `GuildMember | UserResolvable`, but `UserResolvable` already includes `GuildMember`. Use `UserResolvable` instead.
+
+### MessageManager
+
+`MessageManager#crosspost()` has been moved to `GuildMessageManager`. This means it will no longer be exposed in `DMMessageManager`.
+
+### IntegrationApplication
+
+`IntegrationApplication#hook` has been removed.
+
+### InteractionResponse
+
+`InteractionResponse` has been removed. This class was encountered when responding to an interaction without `fetchReply` to allow ease of creating interaction collectors. This is no longer necessary as `withResponse` exposes the message.
+
+### InteractionResponses
+
+#### Ephemeral option removal
+
+Previously, you would respond to an interaction ephemerally like so:
+
+```js
+// Way 1:
+await interaction.reply({ content: 'This is an ephemeral response.', ephemeral: true });
+
+// Way 2:
+await interaction.reply({ content: 'This is an ephemeral response.', flags: MessageFlags.Ephemeral });
+```
+
+There are two ways to achieve the same behaviour, so the "helper" option has been removed. In this case, that would be `ephemeral`, as all that did was assign `MessageFlags.Ephemeral` internally.
+
+#### Fetch reply option removal
+
+`fetchReply` has been removed in favor of `withResponse`. If you were using the `fetchReply` option or fetching the response of an interaction, it is recommended to use `withResponse` instead, as the message is exposed without an additional API call:
+
+```js
+const message = await interaction.reply({ content: 'Hello!', fetchReply: true }); // [!code --]
+const response = await interaction.reply({ content: 'Hello!', withResponse: true }); // [!code ++:2]
+const { message } = response.resource;
+```
+
+#### Premium response type
+
+Discord no longer supports the `PREMIUM_REQUIRED` interaction response type. In the past, you would have done this:
+
+```js
+if (!premiumLogicCheck) {
+ // User does not have access to our premium features.
+ await interaction.sendPremiumRequired();
+ return;
+}
+
+await interaction.reply('You have access to our premium features!');
+```
+
+However, you would have already noticed that this no longer works, so this method has been removed. Sending a premium button has been the replacement ever since.
+
+### Invite
+
+`Invite#stageInstance` has been removed.
+
+### InviteStageInstance
+
+`InviteStageInstance` has been removed.
+
+### Message
+
+`Message#interaction` has been removed. Use `Message#interactionMetadata` instead.
+
+### MessagePayload
+
+`MessagePayload#isInteraction` no longer serves a purpose and has been removed.
+
+### NewsChannel
+
+`NewsChannel` has been renamed to `AnnouncementChannel`.
+
+### PermissionOverwrites
+
+`PermissionOverwrites.resolve()` previously relied on cache if a snowflake was passed. This method no longer relies on cache and instead requires an explicit `type` if supplied.
+
+### RoleManager
+
+`RoleManager#fetch()` used to return `null` when fetching a role that did not exist. This logic has been removed and will throw an error instead.
+
+### SelectMenuBuilder
+
+`SelectMenuBuilder` has been removed. Use `StringSelectMenuBuilder` instead.
+
+### SelectMenuComponent
+
+`SelectMenuComponent` has been removed. Use `StringSelectMenuComponent` instead.
+
+### SelectMenuInteraction
+
+`SelectMenuInteraction` has been removed. Use `StringSelectMenuInteraction` instead.
+
+### SelectMenuOptionBuilder
+
+`SelectMenuOptionBuilder` has been removed. Use `StringSelectMenuOptionBuilder` instead.
+
+### ShardClientUtil
+
+`ShardClientUtil#ids` and `ShardClientUtil#count` have been removed in favor of `Client#ws#getShardIds()` and `Client#ws#getShardCount()`.
+
+### StageInstance
+
+`StageInstance#discoverableDisabled` has been removed.
+
+### TeamMember
+
+`TeamMember#permissions` has been removed. Use `TeamMemberManager#role` instead.
+
+### TextBasedChannel
+
+`TextBasedChannel#bulkDelete()` could return a collection containing `undefined` values. This was because in order to return these messages, the cache must be checked, especially when only snowflakes were provided. The return type of this method has thus changed to only return an array of snowflakes that were bulk deleted.
+
+### ThreadChannel
+
+`ThreadChannel#fetchOwner()` used to return `null` when the thread owner was not present in the thread. This logic has been removed and will throw an error instead.
+
+### ThreadManager
+
+`ThreadManager#fetch()` now throws an error when the provided thread id doesn't belong to the current channel.
+
+### ThreadMember
+
+The reason parameter of `ThreadMember#add()` and `ThreadMember#remove()` have been removed. Discord did not respect this parameter, so it did not do anything.
+
+### ThreadMemberManager
+
+The reason parameter of `ThreadMemberManager#remove()` has been removed. Discord did not respect this parameter, so it did not do anything.
+
+### User
+
+#### Avatar decoration
+
+Discord no longer sends avatar decoration data via `User#avatarDecoration`, so this property has been removed. `User#avatarDecorationData` is the replacement.
+
+#### Flags
+
+`User#fetchFlags()` has been removed. All this did was fetch the user and return only its `flags` property. It was quite redundant.
+
+### UserManager
+
+`UserManager#fetchFlags()` has been removed. All this did was fetch the user and return only its `flags` property. It was quite redundant.
+
+### WebSocketShardEvents
+
+`WebSocketShardEvents` has been replaced with `WebSocketShardEvents` from @discordjs/ws.
diff --git a/apps/guide/content/docs/v15/meta.json b/apps/guide/content/docs/v15/meta.json
new file mode 100644
index 000000000000..3445d038636a
--- /dev/null
+++ b/apps/guide/content/docs/v15/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "discord.js v15",
+ "description": "Work in progress...",
+ "pages": ["external:[LibraryBig][Documentation](https://discord.js.org/docs/packages/discord.js/main)", "index"],
+ "icon": "FlaskConical",
+ "root": true
+}
diff --git a/apps/guide/package.json b/apps/guide/package.json
index 6821ca0d0859..2276e9db3522 100644
--- a/apps/guide/package.json
+++ b/apps/guide/package.json
@@ -10,7 +10,7 @@
"build:local": "cross-env NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod",
"build:prod": "pnpm run build:next",
"build:next": "next build",
- "build": "next build --webpack",
+ "build": "next build",
"preview": "next start",
"preview:cf": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy:cf": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
@@ -48,69 +48,69 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
- "@opennextjs/cloudflare": "^1.11.0",
+ "@opennextjs/cloudflare": "^1.14.4",
"@react-icons/all-files": "^4.1.0",
- "@vercel/analytics": "^1.5.0",
+ "@vercel/analytics": "^1.6.1",
"cmdk": "^1.1.1",
"cva": "1.0.0-beta.3",
- "fumadocs-core": "^16.0.6",
- "fumadocs-mdx": "^13.0.3",
- "fumadocs-twoslash": "^3.1.9",
- "fumadocs-ui": "^16.0.6",
+ "fumadocs-core": "^16.2.3",
+ "fumadocs-mdx": "^14.1.0",
+ "fumadocs-twoslash": "^3.1.10",
+ "fumadocs-ui": "^16.2.3",
"geist": "^1.5.1",
"immer": "^10.2.0",
- "jotai": "^2.15.0",
+ "jotai": "^2.15.2",
"jotai-immer": "^0.4.1",
"lucide-react": "^0.548.0",
- "mermaid": "^11.12.1",
- "motion": "^12.23.24",
- "next": "^16.0.1",
+ "mermaid": "^11.12.2",
+ "motion": "^12.23.25",
+ "next": "^16.0.7",
"next-mdx-remote-client": "^2.1.7",
"next-themes": "^0.4.6",
- "nuqs": "^2.7.2",
+ "nuqs": "^2.8.3",
"p-retry": "^7.1.0",
- "react": "^19.2.0",
+ "react": "^19.2.1",
"react-aria": "^3.44.0",
"react-aria-components": "^1.13.0",
- "react-dom": "^19.2.0",
+ "react-dom": "^19.2.1",
"react-error-boundary": "^6.0.0",
- "safe-mdx": "^1.3.8",
- "sharp": "^0.34.4",
- "tailwind-merge": "^3.3.1",
+ "safe-mdx": "^1.3.9",
+ "sharp": "^0.34.5",
+ "tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
"twoslash": "^0.3.4",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
- "@next/env": "^16.0.1",
- "@shikijs/rehype": "^3.14.0",
- "@tailwindcss/postcss": "^4.1.16",
+ "@next/env": "^16.0.7",
+ "@shikijs/rehype": "^3.19.0",
+ "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
- "@tailwindcss/vite": "^4.1.16",
+ "@tailwindcss/vite": "^4.1.17",
"@types/mdx": "^2.0.13",
- "@types/node": "^24.9.2",
- "@types/react": "^19.2.2",
- "@types/react-dom": "^19.2.2",
- "autoprefixer": "^10.4.21",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "autoprefixer": "^10.4.22",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"cpy-cli": "^6.0.0",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-pretty": "^7.0.0",
"git-describe": "^4.1.1",
"postcss": "^8.5.6",
- "prettier": "^3.6.2",
- "prettier-plugin-tailwindcss": "^0.7.1",
+ "prettier": "^3.7.4",
+ "prettier-plugin-tailwindcss": "^0.7.2",
"remark-gfm": "^4.0.1",
"remark-rehype": "^11.1.2",
- "shiki": "^3.14.0",
- "tailwindcss": "^4.1.16",
+ "shiki": "^3.19.0",
+ "tailwindcss": "^4.1.17",
"tailwindcss-react-aria-components": "^2.0.1",
- "turbo": "^2.5.8",
+ "turbo": "^2.6.3",
"typescript": "^5.9.3",
- "vercel": "^48.7.1",
- "wrangler": "^4.45.3"
+ "vercel": "^48.12.1",
+ "wrangler": "^4.53.0"
},
"engines": {
"node": ">=22.12.0"
diff --git a/apps/guide/src/lib/source.ts b/apps/guide/src/lib/source.ts
index be59f8d798fe..873de5282a65 100644
--- a/apps/guide/src/lib/source.ts
+++ b/apps/guide/src/lib/source.ts
@@ -1,7 +1,7 @@
import { loader } from 'fumadocs-core/source';
import { icons } from 'lucide-react';
import { createElement } from 'react';
-import { docs } from '../../.source';
+import { docs } from '../../.source/server';
export const source = loader({
icon(icon) {
diff --git a/apps/guide/src/middleware.ts b/apps/guide/src/middleware.ts
index 442667467d99..297bb6457e51 100644
--- a/apps/guide/src/middleware.ts
+++ b/apps/guide/src/middleware.ts
@@ -9,7 +9,11 @@ export function middleware(request: NextRequest) {
}
// Redirect old urls to /legacy
- if (!request.nextUrl.pathname.startsWith('/legacy') && !request.nextUrl.pathname.startsWith('/voice')) {
+ if (
+ !request.nextUrl.pathname.startsWith('/legacy') &&
+ !request.nextUrl.pathname.startsWith('/voice') &&
+ !request.nextUrl.pathname.startsWith('/v15')
+ ) {
const newUrl = request.nextUrl.clone();
newUrl.pathname = `/legacy${newUrl.pathname}`;
return NextResponse.redirect(newUrl);
diff --git a/apps/guide/src/styles/base.css b/apps/guide/src/styles/base.css
index 3386a0dadd22..034bb05347ac 100644
--- a/apps/guide/src/styles/base.css
+++ b/apps/guide/src/styles/base.css
@@ -28,6 +28,7 @@
:root {
--legacy-color: hsl(153, 48%, 41%);
--voice-color: hsl(211.3, 66.1%, 65.3%);
+ --v15-color: oklch(70.4% 0.191 22.216);
}
.legacy {
@@ -37,3 +38,7 @@
.voice {
--color-fd-primary: var(--voice-color);
}
+
+.v15 {
+ --color-fd-primary: var(--v15-color);
+}
diff --git a/apps/website/package.json b/apps/website/package.json
index 12575fd0d5c1..289ec7ebd237 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -51,11 +51,11 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
- "@opennextjs/cloudflare": "^1.11.0",
+ "@opennextjs/cloudflare": "^1.14.4",
"@radix-ui/react-collapsible": "^1.1.12",
"@react-icons/all-files": "^4.1.0",
- "@tanstack/react-query": "^5.90.5",
- "@vercel/analytics": "^1.5.0",
+ "@tanstack/react-query": "^5.90.12",
+ "@vercel/analytics": "^1.6.1",
"@vercel/edge-config": "^1.4.3",
"@vercel/postgres": "^0.10.0",
"cloudflare": "^5.2.0",
@@ -63,57 +63,57 @@
"cva": "1.0.0-beta.3",
"geist": "^1.5.1",
"immer": "^10.2.0",
- "jotai": "^2.15.0",
+ "jotai": "^2.15.2",
"jotai-immer": "^0.4.1",
"lucide-react": "^0.548.0",
"meilisearch": "^0.53.0",
- "motion": "^12.23.24",
- "next": "^16.0.1",
+ "motion": "^12.23.25",
+ "next": "^16.0.7",
"next-mdx-remote-client": "^2.1.7",
"next-themes": "^0.4.6",
- "nuqs": "^2.7.2",
- "overlayscrollbars": "^2.12.0",
+ "nuqs": "^2.8.3",
+ "overlayscrollbars": "^2.13.0",
"overlayscrollbars-react": "^0.5.6",
- "react": "^19.2.0",
+ "react": "^19.2.1",
"react-aria": "^3.44.0",
"react-aria-components": "^1.13.0",
- "react-dom": "^19.2.0",
+ "react-dom": "^19.2.1",
"react-error-boundary": "^6.0.0",
- "safe-mdx": "^1.3.8",
- "sharp": "^0.34.4",
- "tailwind-merge": "^3.3.1",
+ "safe-mdx": "^1.3.9",
+ "sharp": "^0.34.5",
+ "tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
- "@next/env": "^16.0.1",
- "@shikijs/rehype": "^3.14.0",
- "@tailwindcss/postcss": "^4.1.16",
+ "@next/env": "^16.0.7",
+ "@shikijs/rehype": "^3.19.0",
+ "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
- "@tailwindcss/vite": "^4.1.16",
- "@types/node": "^24.9.2",
- "@types/react": "^19.2.2",
- "@types/react-dom": "^19.2.2",
- "autoprefixer": "^10.4.21",
+ "@tailwindcss/vite": "^4.1.17",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "autoprefixer": "^10.4.22",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"cpy-cli": "^6.0.0",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-pretty": "^7.0.0",
"git-describe": "^4.1.1",
"postcss": "^8.5.6",
- "prettier": "^3.6.2",
- "prettier-plugin-tailwindcss": "^0.7.1",
+ "prettier": "^3.7.4",
+ "prettier-plugin-tailwindcss": "^0.7.2",
"remark-gfm": "^4.0.1",
"remark-rehype": "^11.1.2",
- "shiki": "^3.14.0",
- "tailwindcss": "^4.1.16",
+ "shiki": "^3.19.0",
+ "tailwindcss": "^4.1.17",
"tailwindcss-react-aria-components": "^2.0.1",
- "turbo": "^2.5.8",
+ "turbo": "^2.6.3",
"typescript": "^5.9.3",
- "vercel": "^48.7.1",
- "wrangler": "^4.45.3"
+ "vercel": "^48.12.1",
+ "wrangler": "^4.53.0"
},
"engines": {
"node": ">=22.12.0"
diff --git a/apps/website/src/app/opengraph-image.tsx b/apps/website/src/app/opengraph-image.tsx
index aa66b2b335c1..13fa49ff991b 100644
--- a/apps/website/src/app/opengraph-image.tsx
+++ b/apps/website/src/app/opengraph-image.tsx
@@ -27,23 +27,21 @@ async function loadGoogleFont(font: string, text: string) {
export default async function Image() {
return new ImageResponse(
- (
-
-
+
+
+
-
-
-
- The most popular
-
-
way to build Discord
-
bots.
+
+
+ The most popular
+
way to build Discord
+
bots.
- ),
+
,
{
...size,
fonts: [
diff --git a/apps/website/src/components/CmdK.tsx b/apps/website/src/components/CmdK.tsx
index 7b1f18d05ebb..86b617d2930b 100644
--- a/apps/website/src/components/CmdK.tsx
+++ b/apps/website/src/components/CmdK.tsx
@@ -16,7 +16,7 @@ import { resolveKind } from '@/util/resolveNodeKind';
const client = new MeiliSearch({
host: 'https://search.discordjs.dev',
- apiKey: 'b51923c6abb574b1e97be9a03dc6414b6c69fb0c5696d0ef01a82b0f77d223db',
+ apiKey: 'f3482b8e976a8b1092394aafbfb91f391242f40b0a6f45a008a5a72b354fb07e',
});
export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
diff --git a/package.json b/package.json
index 771b8a32cc26..11c280343631 100644
--- a/package.json
+++ b/package.json
@@ -51,33 +51,33 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"devDependencies": {
- "@commitlint/cli": "^20.1.0",
- "@commitlint/config-angular": "^20.0.0",
- "@favware/cliff-jumper": "^4.1.0",
+ "@commitlint/cli": "^20.2.0",
+ "@commitlint/config-angular": "^20.2.0",
+ "@favware/cliff-jumper": "^6.0.0",
"@favware/npm-deprecate": "^2.0.0",
"@types/lodash.merge": "^4.6.9",
- "@unocss/eslint-plugin": "^66.5.4",
- "@vitest/coverage-v8": "^3.2.4",
+ "@unocss/eslint-plugin": "^66.5.10",
+ "@vitest/coverage-v8": "^4.0.15",
"conventional-changelog-cli": "^5.0.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"husky": "^9.1.7",
"is-ci": "^4.1.0",
- "lint-staged": "^16.2.6",
+ "lint-staged": "^16.2.7",
"lodash.merge": "^4.6.2",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "typescript-eslint": "^8.46.2",
- "unocss": "^66.5.4",
- "vercel": "^48.7.1",
- "vitest": "^3.2.4"
+ "typescript-eslint": "^8.48.1",
+ "unocss": "^66.5.10",
+ "vercel": "^48.12.1",
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
},
- "packageManager": "pnpm@10.20.0"
+ "packageManager": "pnpm@10.24.0"
}
diff --git a/packages/actions/package.json b/packages/actions/package.json
index f946f2b48d6f..9f9119b14b29 100644
--- a/packages/actions/package.json
+++ b/packages/actions/package.json
@@ -44,32 +44,32 @@
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@actions/glob": "^0.5.0",
- "@aws-sdk/client-s3": "^3.921.0",
+ "@aws-sdk/client-s3": "^3.946.0",
"@discordjs/scripts": "workspace:^",
"cloudflare": "^5.2.0",
"commander": "^14.0.2",
"meilisearch": "^0.38.0",
"p-limit": "^7.2.0",
- "p-queue": "^9.0.0",
+ "p-queue": "^9.0.1",
"tslib": "^2.8.1",
"undici": "7.16.0"
},
"devDependencies": {
"@npm/types": "^2.1.0",
- "@types/bun": "^1.3.1",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@types/bun": "^1.3.3",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/actions/src/uploadReadmeFiles/action.yml b/packages/actions/src/uploadReadmeFiles/action.yml
new file mode 100644
index 000000000000..4436ddbdf3a7
--- /dev/null
+++ b/packages/actions/src/uploadReadmeFiles/action.yml
@@ -0,0 +1,5 @@
+name: 'Upload README.md files'
+description: 'Uploads the README.md files for packages that have api-extractor.json to the website.'
+runs:
+ using: node24
+ main: ../../dist/uploadReadmeFiles/index.js
diff --git a/packages/actions/src/uploadReadmeFiles/index.ts b/packages/actions/src/uploadReadmeFiles/index.ts
new file mode 100644
index 000000000000..6a0be8bd8af0
--- /dev/null
+++ b/packages/actions/src/uploadReadmeFiles/index.ts
@@ -0,0 +1,59 @@
+import { readFile } from 'node:fs/promises';
+import process from 'node:process';
+import { info, setFailed } from '@actions/core';
+import { create } from '@actions/glob';
+import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
+
+if (
+ !process.env.CF_R2_READMES_ACCESS_KEY_ID ||
+ !process.env.CF_R2_READMES_SECRET_ACCESS_KEY ||
+ !process.env.CF_R2_READMES_BUCKET ||
+ !process.env.CF_R2_READMES_URL
+) {
+ setFailed('Missing environment variables.');
+ process.exit(1);
+}
+
+const S3READMEFiles = new S3Client({
+ region: 'auto',
+ endpoint: process.env.CF_R2_READMES_URL,
+ credentials: {
+ accessKeyId: process.env.CF_R2_READMES_ACCESS_KEY_ID,
+ secretAccessKey: process.env.CF_R2_READMES_SECRET_ACCESS_KEY,
+ },
+ requestChecksumCalculation: 'WHEN_REQUIRED',
+ responseChecksumValidation: 'WHEN_REQUIRED',
+});
+
+const promises = [];
+
+// Find all packages with an api-extractor.json file.
+const globber = await create('packages/*/api-extractor.json');
+
+for await (const apiExtractorFile of globber.globGenerator()) {
+ const readmePath = apiExtractorFile.replace('/api-extractor.json', '/README.md');
+ const packageName = apiExtractorFile.split('/').at(-2)!;
+ const readmeKey = `${packageName}/home-README.md`;
+ info(`Uploading ${readmePath}...`);
+
+ promises.push(
+ // eslint-disable-next-line promise/prefer-await-to-then
+ readFile(readmePath, 'utf8').then(async (readmeData) =>
+ S3READMEFiles.send(
+ new PutObjectCommand({
+ Bucket: process.env.CF_R2_READMES_BUCKET,
+ Key: readmeKey,
+ Body: readmeData,
+ }),
+ ),
+ ),
+ );
+}
+
+try {
+ await Promise.all(promises);
+ info('All README.md files uploaded successfully!');
+} catch (error) {
+ setFailed(`Failed to upload README files: ${error}`);
+ process.exit(1);
+}
diff --git a/packages/actions/tsup.config.ts b/packages/actions/tsup.config.ts
index d1438612c62e..6bd00cc05781 100644
--- a/packages/actions/tsup.config.ts
+++ b/packages/actions/tsup.config.ts
@@ -6,6 +6,7 @@ export default createTsupConfig({
'src/formatTag/index.ts',
'src/releasePackages/index.ts',
'src/uploadDocumentation/index.ts',
+ 'src/uploadReadmeFiles/index.ts',
'src/uploadSearchIndices/index.ts',
'src/uploadSplitDocumentation/index.ts',
],
diff --git a/packages/api-extractor-model/package.json b/packages/api-extractor-model/package.json
index fe3885cbf6de..5514c00ac42f 100644
--- a/packages/api-extractor-model/package.json
+++ b/packages/api-extractor-model/package.json
@@ -36,16 +36,16 @@
"@rushstack/node-core-library": "5.13.1"
},
"devDependencies": {
- "@types/node": "^22.18.13",
+ "@types/node": "^22.19.1",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3"
}
}
diff --git a/packages/api-extractor-model/src/items/ApiPropertyItem.ts b/packages/api-extractor-model/src/items/ApiPropertyItem.ts
index c6f6fafe8dd9..082c10ea84f4 100644
--- a/packages/api-extractor-model/src/items/ApiPropertyItem.ts
+++ b/packages/api-extractor-model/src/items/ApiPropertyItem.ts
@@ -15,7 +15,8 @@ import { type IApiDeclaredItemOptions, ApiDeclaredItem, type IApiDeclaredItemJso
* @public
*/
export interface IApiPropertyItemOptions
- extends IApiNameMixinOptions,
+ extends
+ IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiOptionalMixinOptions,
IApiReadonlyMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiCallSignature.ts b/packages/api-extractor-model/src/model/ApiCallSignature.ts
index 917837f385c0..4ccf010a4a37 100644
--- a/packages/api-extractor-model/src/model/ApiCallSignature.ts
+++ b/packages/api-extractor-model/src/model/ApiCallSignature.ts
@@ -18,7 +18,8 @@ import {
* @public
*/
export interface IApiCallSignatureOptions
- extends IApiTypeParameterListMixinOptions,
+ extends
+ IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiClass.ts b/packages/api-extractor-model/src/model/ApiClass.ts
index 7f8e86ec101b..e57525ff8298 100644
--- a/packages/api-extractor-model/src/model/ApiClass.ts
+++ b/packages/api-extractor-model/src/model/ApiClass.ts
@@ -32,7 +32,8 @@ import { HeritageType } from './HeritageType.js';
* @public
*/
export interface IApiClassOptions
- extends IApiItemContainerMixinOptions,
+ extends
+ IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiAbstractMixinOptions,
IApiReleaseTagMixinOptions,
@@ -48,10 +49,7 @@ export interface IExcerptTokenRangeWithTypeParameters extends IExcerptTokenRange
}
export interface IApiClassJson
- extends IApiDeclaredItemJson,
- IApiAbstractMixinJson,
- IApiTypeParameterListMixinJson,
- IApiExportedMixinJson {
+ extends IApiDeclaredItemJson, IApiAbstractMixinJson, IApiTypeParameterListMixinJson, IApiExportedMixinJson {
extendsTokenRange?: IExcerptTokenRangeWithTypeParameters | undefined;
implementsTokenRanges: IExcerptTokenRangeWithTypeParameters[];
}
diff --git a/packages/api-extractor-model/src/model/ApiConstructSignature.ts b/packages/api-extractor-model/src/model/ApiConstructSignature.ts
index 3457487b0fd0..0905934de6c9 100644
--- a/packages/api-extractor-model/src/model/ApiConstructSignature.ts
+++ b/packages/api-extractor-model/src/model/ApiConstructSignature.ts
@@ -18,7 +18,8 @@ import {
* @public
*/
export interface IApiConstructSignatureOptions
- extends IApiTypeParameterListMixinOptions,
+ extends
+ IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiConstructor.ts b/packages/api-extractor-model/src/model/ApiConstructor.ts
index c2b6342f81e2..ffdeb7a633f7 100644
--- a/packages/api-extractor-model/src/model/ApiConstructor.ts
+++ b/packages/api-extractor-model/src/model/ApiConstructor.ts
@@ -14,7 +14,8 @@ import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/A
* @public
*/
export interface IApiConstructorOptions
- extends IApiParameterListMixinOptions,
+ extends
+ IApiParameterListMixinOptions,
IApiProtectedMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions {}
diff --git a/packages/api-extractor-model/src/model/ApiEnum.ts b/packages/api-extractor-model/src/model/ApiEnum.ts
index ccb5e546c35c..667574f741ef 100644
--- a/packages/api-extractor-model/src/model/ApiEnum.ts
+++ b/packages/api-extractor-model/src/model/ApiEnum.ts
@@ -16,7 +16,8 @@ import type { ApiEnumMember } from './ApiEnumMember.js';
* @public
*/
export interface IApiEnumOptions
- extends IApiItemContainerMixinOptions,
+ extends
+ IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
diff --git a/packages/api-extractor-model/src/model/ApiEnumMember.ts b/packages/api-extractor-model/src/model/ApiEnumMember.ts
index 4bb1844b4630..783f9d7dedc3 100644
--- a/packages/api-extractor-model/src/model/ApiEnumMember.ts
+++ b/packages/api-extractor-model/src/model/ApiEnumMember.ts
@@ -14,10 +14,7 @@ import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/A
* @public
*/
export interface IApiEnumMemberOptions
- extends IApiNameMixinOptions,
- IApiReleaseTagMixinOptions,
- IApiDeclaredItemOptions,
- IApiInitializerMixinOptions {}
+ extends IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiInitializerMixinOptions {}
/**
* Options for customizing the sort order of {@link ApiEnum} members.
diff --git a/packages/api-extractor-model/src/model/ApiEvent.ts b/packages/api-extractor-model/src/model/ApiEvent.ts
index b00668bddb33..75352916f57f 100644
--- a/packages/api-extractor-model/src/model/ApiEvent.ts
+++ b/packages/api-extractor-model/src/model/ApiEvent.ts
@@ -14,10 +14,7 @@ import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/A
* @public
*/
export interface IApiEventOptions
- extends IApiNameMixinOptions,
- IApiParameterListMixinOptions,
- IApiReleaseTagMixinOptions,
- IApiDeclaredItemOptions {}
+ extends IApiNameMixinOptions, IApiParameterListMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions {}
/**
* Represents a TypeScript event declaration that belongs to an `ApiClass`.
diff --git a/packages/api-extractor-model/src/model/ApiFunction.ts b/packages/api-extractor-model/src/model/ApiFunction.ts
index 9809fa7f78f9..a855aaf38038 100644
--- a/packages/api-extractor-model/src/model/ApiFunction.ts
+++ b/packages/api-extractor-model/src/model/ApiFunction.ts
@@ -20,7 +20,8 @@ import {
* @public
*/
export interface IApiFunctionOptions
- extends IApiNameMixinOptions,
+ extends
+ IApiNameMixinOptions,
IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiIndexSignature.ts b/packages/api-extractor-model/src/model/ApiIndexSignature.ts
index ed1305d13816..3ab4338e3915 100644
--- a/packages/api-extractor-model/src/model/ApiIndexSignature.ts
+++ b/packages/api-extractor-model/src/model/ApiIndexSignature.ts
@@ -15,7 +15,8 @@ import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/A
* @public
*/
export interface IApiIndexSignatureOptions
- extends IApiParameterListMixinOptions,
+ extends
+ IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiReadonlyMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiInterface.ts b/packages/api-extractor-model/src/model/ApiInterface.ts
index 177563ba4893..060a24bcd913 100644
--- a/packages/api-extractor-model/src/model/ApiInterface.ts
+++ b/packages/api-extractor-model/src/model/ApiInterface.ts
@@ -35,7 +35,8 @@ import { HeritageType } from './HeritageType.js';
* @public
*/
export interface IApiInterfaceOptions
- extends IApiItemContainerMixinOptions,
+ extends
+ IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiTypeParameterListMixinOptions,
IApiReleaseTagMixinOptions,
@@ -45,7 +46,8 @@ export interface IApiInterfaceOptions
}
export interface IApiInterfaceJson
- extends IApiItemContainerJson,
+ extends
+ IApiItemContainerJson,
IApiNameMixinJson,
IApiTypeParameterListMixinJson,
IApiReleaseTagMixinJson,
diff --git a/packages/api-extractor-model/src/model/ApiMethod.ts b/packages/api-extractor-model/src/model/ApiMethod.ts
index f379becadffa..66bcb747d7f8 100644
--- a/packages/api-extractor-model/src/model/ApiMethod.ts
+++ b/packages/api-extractor-model/src/model/ApiMethod.ts
@@ -23,7 +23,8 @@ import {
* @public
*/
export interface IApiMethodOptions
- extends IApiNameMixinOptions,
+ extends
+ IApiNameMixinOptions,
IApiAbstractMixinOptions,
IApiOptionalMixinOptions,
IApiParameterListMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiMethodSignature.ts b/packages/api-extractor-model/src/model/ApiMethodSignature.ts
index 4baac8a8fdea..5ee24ab5cd8f 100644
--- a/packages/api-extractor-model/src/model/ApiMethodSignature.ts
+++ b/packages/api-extractor-model/src/model/ApiMethodSignature.ts
@@ -18,7 +18,8 @@ import {
* @public
*/
export interface IApiMethodSignatureOptions
- extends IApiNameMixinOptions,
+ extends
+ IApiNameMixinOptions,
IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiNamespace.ts b/packages/api-extractor-model/src/model/ApiNamespace.ts
index 4dbdb8d7674d..13cade9deccd 100644
--- a/packages/api-extractor-model/src/model/ApiNamespace.ts
+++ b/packages/api-extractor-model/src/model/ApiNamespace.ts
@@ -15,7 +15,8 @@ import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/A
* @public
*/
export interface IApiNamespaceOptions
- extends IApiItemContainerMixinOptions,
+ extends
+ IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
diff --git a/packages/api-extractor-model/src/model/ApiPackage.ts b/packages/api-extractor-model/src/model/ApiPackage.ts
index 99818ad949ab..d9b141b17bd0 100644
--- a/packages/api-extractor-model/src/model/ApiPackage.ts
+++ b/packages/api-extractor-model/src/model/ApiPackage.ts
@@ -28,9 +28,7 @@ import { DeserializerContext, ApiJsonSchemaVersion } from './DeserializerContext
* @public
*/
export interface IApiPackageOptions
- extends IApiItemContainerMixinOptions,
- IApiNameMixinOptions,
- IApiDocumentedItemOptions {
+ extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiDocumentedItemOptions {
dependencies?: Record
| undefined;
projectFolderUrl?: string | undefined;
tsdocConfiguration: TSDocConfiguration;
diff --git a/packages/api-extractor-model/src/model/ApiProperty.ts b/packages/api-extractor-model/src/model/ApiProperty.ts
index df13ea750e7b..3016f757b4e0 100644
--- a/packages/api-extractor-model/src/model/ApiProperty.ts
+++ b/packages/api-extractor-model/src/model/ApiProperty.ts
@@ -15,7 +15,8 @@ import { ApiStaticMixin, type IApiStaticMixinOptions } from '../mixins/ApiStatic
* @public
*/
export interface IApiPropertyOptions
- extends IApiPropertyItemOptions,
+ extends
+ IApiPropertyItemOptions,
IApiAbstractMixinOptions,
IApiProtectedMixinOptions,
IApiStaticMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiTypeAlias.ts b/packages/api-extractor-model/src/model/ApiTypeAlias.ts
index fc866a0afaf8..e8a4e9925f1a 100644
--- a/packages/api-extractor-model/src/model/ApiTypeAlias.ts
+++ b/packages/api-extractor-model/src/model/ApiTypeAlias.ts
@@ -25,7 +25,8 @@ import type { DeserializerContext } from './DeserializerContext.js';
* @public
*/
export interface IApiTypeAliasOptions
- extends IApiNameMixinOptions,
+ extends
+ IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiTypeParameterListMixinOptions,
diff --git a/packages/api-extractor-model/src/model/ApiVariable.ts b/packages/api-extractor-model/src/model/ApiVariable.ts
index 2b3a7438d638..97ee9abd225c 100644
--- a/packages/api-extractor-model/src/model/ApiVariable.ts
+++ b/packages/api-extractor-model/src/model/ApiVariable.ts
@@ -22,7 +22,8 @@ import type { DeserializerContext } from './DeserializerContext.js';
* @public
*/
export interface IApiVariableOptions
- extends IApiNameMixinOptions,
+ extends
+ IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiReadonlyMixinOptions,
IApiDeclaredItemOptions,
diff --git a/packages/api-extractor-model/src/model/Deserializer.ts b/packages/api-extractor-model/src/model/Deserializer.ts
index 454912626b39..1d8ae2c3acdf 100644
--- a/packages/api-extractor-model/src/model/Deserializer.ts
+++ b/packages/api-extractor-model/src/model/Deserializer.ts
@@ -267,7 +267,8 @@ function mapParam(
}
interface IApiMethodJson
- extends IApiAbstractMixinJson,
+ extends
+ IApiAbstractMixinJson,
IApiDeclaredItemJson,
IApiNameMixinJson,
IApiOptionalMixinJson,
@@ -279,10 +280,7 @@ interface IApiMethodJson
IApiTypeParameterListMixinJson {}
interface IApiConstructorJson
- extends IApiParameterListJson,
- IApiProtectedMixinJson,
- IApiReleaseTagMixinJson,
- IApiDeclaredItemJson {}
+ extends IApiParameterListJson, IApiProtectedMixinJson, IApiReleaseTagMixinJson, IApiDeclaredItemJson {}
function mapMethod(method: DocgenMethodJson, _package: string, parent?: DocgenClassJson): IApiMethodJson {
const excerptTokens: IExcerptToken[] = [];
diff --git a/packages/api-extractor-utils/package.json b/packages/api-extractor-utils/package.json
index adbd1e12ad6a..f6b9e4890945 100644
--- a/packages/api-extractor-utils/package.json
+++ b/packages/api-extractor-utils/package.json
@@ -50,16 +50,16 @@
"@microsoft/tsdoc": "~0.15.1"
},
"devDependencies": {
- "@types/node": "^22.18.13",
+ "@types/node": "^22.19.1",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3"
},
"engines": {
diff --git a/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts b/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts
index 1aef550113fa..7864719f79e1 100644
--- a/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts
+++ b/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts
@@ -77,10 +77,7 @@ export interface ApiParameterListJSON {
}
export interface ApiMethodSignatureJSON
- extends ApiItemJSON,
- ApiTypeParameterListJSON,
- ApiParameterListJSON,
- ApiInheritableJSON {
+ extends ApiItemJSON, ApiTypeParameterListJSON, ApiParameterListJSON, ApiInheritableJSON {
mergedSiblings: ApiMethodSignatureJSON[];
optional: boolean;
overloadIndex: number;
diff --git a/packages/api-extractor/package.json b/packages/api-extractor/package.json
index aa059bd4c435..1106e050369a 100644
--- a/packages/api-extractor/package.json
+++ b/packages/api-extractor/package.json
@@ -63,19 +63,19 @@
"typescript": "~5.5.4"
},
"devDependencies": {
- "@types/lodash": "^4.17.20",
- "@types/node": "^22.18.13",
+ "@types/lodash": "^4.17.21",
+ "@types/node": "^22.19.1",
"@types/resolve": "^1.20.6",
"@types/semver": "^7.7.1",
"cpy-cli": "^6.0.0",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8"
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3"
}
}
diff --git a/packages/brokers/.cliff-jumperrc.json b/packages/brokers/.cliff-jumperrc.json
index a4afa1ac00fd..f5d4bfcf1c24 100644
--- a/packages/brokers/.cliff-jumperrc.json
+++ b/packages/brokers/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "brokers",
"org": "discordjs",
"packagePath": "packages/brokers",
diff --git a/packages/brokers/__tests__/index.test.ts b/packages/brokers/__tests__/index.test.ts
index 4c3d11f9743c..f4d8951d37c6 100644
--- a/packages/brokers/__tests__/index.test.ts
+++ b/packages/brokers/__tests__/index.test.ts
@@ -15,7 +15,7 @@ const mockRedisClient = {
test('pubsub with custom encoding', async () => {
const encode = vi.fn((data) => data);
- const broker = new PubSubRedisBroker(mockRedisClient, { encode, group: 'group' });
+ const broker = new PubSubRedisBroker(mockRedisClient, { encode, name: 'yeet', group: 'group' });
await broker.publish('test', 'test');
expect(encode).toHaveBeenCalledWith('test');
});
diff --git a/packages/brokers/package.json b/packages/brokers/package.json
index f3a11a167d58..ed24826053fd 100644
--- a/packages/brokers/package.json
+++ b/packages/brokers/package.json
@@ -74,20 +74,20 @@
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/brokers/src/brokers/Broker.ts b/packages/brokers/src/brokers/Broker.ts
index 1b401d3e7f1b..0493e6ad16b6 100644
--- a/packages/brokers/src/brokers/Broker.ts
+++ b/packages/brokers/src/brokers/Broker.ts
@@ -54,8 +54,7 @@ export interface IBaseBroker {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface IPubSubBroker
- extends IBaseBroker,
- AsyncEventEmitter> {
+ extends IBaseBroker, AsyncEventEmitter> {
/**
* Publishes an event
*/
@@ -63,8 +62,7 @@ export interface IPubSubBroker
}
export interface IRPCBroker, TResponses extends Record>
- extends IBaseBroker,
- AsyncEventEmitter> {
+ extends IBaseBroker, AsyncEventEmitter> {
/**
* Makes an RPC call
*/
diff --git a/packages/brokers/src/brokers/redis/BaseRedis.ts b/packages/brokers/src/brokers/redis/BaseRedis.ts
index 1e262afb10bd..239670db32d2 100644
--- a/packages/brokers/src/brokers/redis/BaseRedis.ts
+++ b/packages/brokers/src/brokers/redis/BaseRedis.ts
@@ -81,9 +81,9 @@ export const DefaultRedisBrokerOptions = {
* Helper class with shared Redis logic
*/
export abstract class BaseRedisBroker<
- TEvents extends Record,
- TResponses extends Record | undefined = undefined,
- >
+ TEvents extends Record,
+ TResponses extends Record | undefined = undefined,
+>
extends AsyncEventEmitter>
implements IBaseBroker
{
diff --git a/packages/builders/.cliff-jumperrc.json b/packages/builders/.cliff-jumperrc.json
index cf8235be4bcc..30a09b56845a 100644
--- a/packages/builders/.cliff-jumperrc.json
+++ b/packages/builders/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "builders",
"org": "discordjs",
"packagePath": "packages/builders",
diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts
index fc0c86c2ab82..c6bac13a9b4d 100644
--- a/packages/builders/__tests__/components/button.test.ts
+++ b/packages/builders/__tests__/components/button.test.ts
@@ -5,7 +5,13 @@ import {
type APIButtonComponentWithURL,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
-import { PrimaryButtonBuilder, PremiumButtonBuilder, LinkButtonBuilder } from '../../src/index.js';
+import {
+ PrimaryButtonBuilder,
+ PremiumButtonBuilder,
+ LinkButtonBuilder,
+ DangerButtonBuilder,
+ SecondaryButtonBuilder,
+} from '../../src/index.js';
const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
@@ -13,7 +19,7 @@ const longStr =
describe('Button Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid fields THEN builder does not throw', () => {
- expect(() => new PrimaryButtonBuilder().setCustomId('custom').setLabel('test')).not.toThrowError();
+ expect(() => new PrimaryButtonBuilder().setCustomId('custom').setLabel('test').toJSON()).not.toThrowError();
expect(() => {
const button = new PrimaryButtonBuilder()
@@ -25,6 +31,26 @@ describe('Button Components', () => {
button.toJSON();
}).not.toThrowError();
+ expect(() => {
+ const button = new SecondaryButtonBuilder().setCustomId('custom').setLabel('a'.repeat(80));
+ button.toJSON();
+ }).not.toThrowError();
+
+ expect(() => {
+ const button = new DangerButtonBuilder().setCustomId('custom').setEmoji({ name: 'ok' });
+ button.toJSON();
+ }).not.toThrowError();
+
+ expect(() => {
+ const button = new LinkButtonBuilder().setURL('https://discord.js.org').setLabel('a'.repeat(80));
+ button.toJSON();
+ }).not.toThrowError();
+
+ expect(() => {
+ const button = new LinkButtonBuilder().setURL('https://discord.js.org').setEmoji({ name: 'ok' });
+ button.toJSON();
+ }).not.toThrowError();
+
expect(() => {
const button = new PremiumButtonBuilder().setSKUId('123456789012345678');
button.toJSON();
diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts
index 544bf79a5d47..f7137f95180f 100644
--- a/packages/builders/__tests__/components/selectMenu.test.ts
+++ b/packages/builders/__tests__/components/selectMenu.test.ts
@@ -7,6 +7,9 @@ const selectMenuWithId = () => new StringSelectMenuBuilder({ custom_id: 'hi' });
const selectMenuOption = () => new StringSelectMenuOptionBuilder();
const longStr = 'a'.repeat(256);
+const selectMenuOptionLabelAboveLimit = 'a'.repeat(101);
+const selectMenuOptionValueAboveLimit = 'a'.repeat(101);
+const selectMenuOptionDescriptionAboveLimit = 'a'.repeat(101);
const selectMenuOptionData: APISelectMenuOption = {
label: 'test',
@@ -48,26 +51,49 @@ function mapStringSelectMenuOptionBuildersToJson(selectMenu: StringSelectMenuBui
describe('Select Menu Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid inputs THEN Select Menu does not throw', () => {
- expect(() => selectMenu().setCustomId('foo')).not.toThrowError();
- expect(() => selectMenu().setMaxValues(10)).not.toThrowError();
- expect(() => selectMenu().setMinValues(3)).not.toThrowError();
- expect(() => selectMenu().setDisabled(true)).not.toThrowError();
- expect(() => selectMenu().setDisabled()).not.toThrowError();
- expect(() => selectMenu().setPlaceholder('description')).not.toThrowError();
+ expect(() =>
+ selectMenu().setCustomId('foo').addOptions({ label: 'test', value: 'test' }).toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId().setMaxValues(10).addOptions({ label: 'test', value: 'test' }).toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId()
+ .setMinValues(3)
+ .addOptions(
+ { label: 'test1', value: 'test1' },
+ { label: 'test2', value: 'test2' },
+ { label: 'test3', value: 'test3' },
+ )
+ .toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId().setDisabled(true).addOptions({ label: 'test', value: 'test' }).toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId().setDisabled().addOptions({ label: 'test', value: 'test' }).toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId().setPlaceholder('description').addOptions({ label: 'test', value: 'test' }).toJSON(),
+ ).not.toThrowError();
const option = selectMenuOption()
.setLabel('test')
.setValue('test')
.setDefault(true)
.setEmoji({ name: 'test' })
.setDescription('description');
- expect(() => selectMenu().addOptions(option)).not.toThrowError();
- expect(() => selectMenu().setOptions(option)).not.toThrowError();
- expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
- expect(() => selectMenu().addOptions([option])).not.toThrowError();
- expect(() => selectMenu().setOptions([option])).not.toThrowError();
- expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
+ expect(() => selectMenuWithId().addOptions(option).toJSON()).not.toThrowError();
+ expect(() => selectMenuWithId().setOptions(option).toJSON()).not.toThrowError();
+ expect(() => selectMenuWithId().setOptions({ label: 'test', value: 'test' }).toJSON()).not.toThrowError();
+ expect(() => selectMenuWithId().addOptions([option]).toJSON()).not.toThrowError();
+ expect(() => selectMenuWithId().setOptions([option]).toJSON()).not.toThrowError();
expect(() =>
- selectMenu()
+ selectMenuWithId()
+ .setOptions([{ label: 'test', value: 'test' }])
+ .toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId()
.addOptions({
label: 'test',
value: 'test',
@@ -87,26 +113,37 @@ describe('Select Menu Components', () => {
animated: true,
},
},
- ]),
+ ])
+ .toJSON(),
).not.toThrowError();
const options = Array.from({ length: 25 }).fill({ label: 'test', value: 'test' });
- expect(() => selectMenu().addOptions(...options)).not.toThrowError();
- expect(() => selectMenu().setOptions(...options)).not.toThrowError();
- expect(() => selectMenu().addOptions(options)).not.toThrowError();
- expect(() => selectMenu().setOptions(options)).not.toThrowError();
+ expect(() =>
+ selectMenuWithId()
+ .addOptions(...options)
+ .toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ selectMenuWithId()
+ .setOptions(...options)
+ .toJSON(),
+ ).not.toThrowError();
+ expect(() => selectMenuWithId().addOptions(options).toJSON()).not.toThrowError();
+ expect(() => selectMenuWithId().setOptions(options).toJSON()).not.toThrowError();
expect(() =>
- selectMenu()
+ selectMenuWithId()
.addOptions({ label: 'test', value: 'test' })
- .addOptions(...Array.from({ length: 24 }).fill({ label: 'test', value: 'test' })),
+ .addOptions(...Array.from({ length: 24 }).fill({ label: 'test', value: 'test' }))
+ .toJSON(),
).not.toThrowError();
expect(() =>
- selectMenu()
+ selectMenuWithId()
.addOptions([{ label: 'test', value: 'test' }])
- .addOptions(Array.from({ length: 24 }).fill({ label: 'test', value: 'test' })),
+ .addOptions(Array.from({ length: 24 }).fill({ label: 'test', value: 'test' }))
+ .toJSON(),
).not.toThrowError();
});
@@ -196,26 +233,30 @@ describe('Select Menu Components', () => {
expect(() => {
selectMenuOption()
- .setLabel(longStr)
- .setValue(longStr)
+ .setLabel(selectMenuOptionLabelAboveLimit)
+ .setValue(selectMenuOptionValueAboveLimit)
// @ts-expect-error: Invalid default value
.setDefault(-1)
// @ts-expect-error: Invalid emoji
.setEmoji({ name: 1 })
- .setDescription(longStr)
+ .setDescription(selectMenuOptionDescriptionAboveLimit)
.toJSON();
}).toThrowError();
});
test('GIVEN valid option types THEN does not throw', () => {
expect(() =>
- selectMenu().addOptions({
- label: 'test',
- value: 'test',
- }),
+ selectMenuWithId()
+ .addOptions({
+ label: 'test',
+ value: 'test',
+ })
+ .toJSON(),
).not.toThrowError();
- expect(() => selectMenu().addOptions(selectMenuOption().setLabel('test').setValue('test'))).not.toThrowError();
+ expect(() =>
+ selectMenuWithId().addOptions(selectMenuOption().setLabel('test').setValue('test')).toJSON(),
+ ).not.toThrowError();
});
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
diff --git a/packages/builders/__tests__/components/v2/container.test.ts b/packages/builders/__tests__/components/v2/container.test.ts
index 86cad4fab5ad..fe99986bf477 100644
--- a/packages/builders/__tests__/components/v2/container.test.ts
+++ b/packages/builders/__tests__/components/v2/container.test.ts
@@ -64,11 +64,11 @@ const containerWithSeparatorDataNoColor: APIContainerComponent = {
describe('Container Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
- expect(() => new ContainerBuilder().addSeparatorComponents(new SeparatorBuilder())).not.toThrowError();
- expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder())).not.toThrowError();
- expect(() => new ContainerBuilder().addSeparatorComponents([new SeparatorBuilder()])).not.toThrowError();
+ expect(() => new ContainerBuilder().addSeparatorComponents(new SeparatorBuilder()).toJSON()).not.toThrowError();
+ expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder()).toJSON()).not.toThrowError();
+ expect(() => new ContainerBuilder().addSeparatorComponents([new SeparatorBuilder()]).toJSON()).not.toThrowError();
expect(() =>
- new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]),
+ new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]).toJSON(),
).not.toThrowError();
});
diff --git a/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts b/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts
index 0affe76c8b8a..48a212165c6d 100644
--- a/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts
+++ b/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts
@@ -237,9 +237,9 @@ describe('ChatInput Commands', () => {
});
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();
+ expect(() => getBuilder().setName('hi_there').setDescription(':3').toJSON()).not.toThrowError();
+ expect(() => getBuilder().setName('o_comandă').setDescription(':3').toJSON()).not.toThrowError();
+ expect(() => getBuilder().setName('どうも').setDescription(':3').toJSON()).not.toThrowError();
});
test('GIVEN invalid returns for builder THEN throw error', () => {
@@ -376,6 +376,29 @@ describe('ChatInput Commands', () => {
});
});
+ describe('Subcommand builder and subcommand group builder', () => {
+ test('GIVEN both types THEN does not throw error', () => {
+ expect(() =>
+ getBuilder()
+ .setName('test')
+ .setDescription('Test command')
+ .addSubcommands((subcommand) =>
+ subcommand.setName('subcommand').setDescription('Description of subcommand'),
+ )
+ .addSubcommandGroups((subcommandGroup) =>
+ subcommandGroup
+ .setName('group')
+ .setDescription('Description of group')
+
+ .addSubcommands((subcommand) =>
+ subcommand.setName('subcommand').setDescription('Description of group subcommand'),
+ ),
+ )
+ .toJSON(),
+ ).not.toThrowError();
+ });
+ });
+
describe('ChatInput command localizations', () => {
const expectedSingleLocale = { [Locale.EnglishUS]: 'foobar' };
const expectedMultipleLocales = {
@@ -384,8 +407,12 @@ describe('ChatInput Commands', () => {
};
test('GIVEN valid name localizations THEN does not throw error', () => {
- expect(() => getBuilder().setNameLocalization(Locale.EnglishUS, 'foobar')).not.toThrowError();
- expect(() => getBuilder().setNameLocalizations({ [Locale.EnglishUS]: 'foobar' })).not.toThrowError();
+ expect(() => getNamedBuilder().setNameLocalization(Locale.EnglishUS, 'foobar').toJSON()).not.toThrowError();
+ expect(() =>
+ getNamedBuilder()
+ .setNameLocalizations({ [Locale.EnglishUS]: 'foobar' })
+ .toJSON(),
+ ).not.toThrowError();
});
test('GIVEN invalid name localizations THEN does throw error', () => {
@@ -451,19 +478,19 @@ describe('ChatInput Commands', () => {
describe('permissions', () => {
test('GIVEN valid permission string THEN does not throw error', () => {
- expect(() => getNamedBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
+ expect(() => getNamedBuilder().setDefaultMemberPermissions('1').toJSON()).not.toThrowError();
});
test('GIVEN valid permission bitfield THEN does not throw error', () => {
expect(() =>
- getNamedBuilder().setDefaultMemberPermissions(
- PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles,
- ),
+ getNamedBuilder()
+ .setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles)
+ .toJSON(),
).not.toThrowError();
});
test('GIVEN null permissions THEN does not throw error', () => {
- expect(() => getNamedBuilder().clearDefaultMemberPermissions()).not.toThrowError();
+ expect(() => getNamedBuilder().clearDefaultMemberPermissions().toJSON()).not.toThrowError();
});
test('GIVEN invalid inputs THEN does throw error', () => {
@@ -476,7 +503,7 @@ describe('ChatInput Commands', () => {
getNamedBuilder().addBooleanOptions(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(),
).not.toThrowError();
- expect(() => getNamedBuilder().addChannelOptions(getChannelOption())).not.toThrowError();
+ expect(() => getNamedBuilder().addChannelOptions(getChannelOption()).toJSON()).not.toThrowError();
});
});
diff --git a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts
index 36b569af1ff5..c8805f5098fb 100644
--- a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts
+++ b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts
@@ -21,10 +21,10 @@ describe('Context Menu Commands', () => {
expect(() => getBuilder().setName('A COMMAND').toJSON()).not.toThrowError();
// Translation: a_command
- expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
+ expect(() => getBuilder().setName('o_comandă').toJSON()).not.toThrowError();
// Translation: thx (according to GTranslate)
- expect(() => getBuilder().setName('どうも')).not.toThrowError();
+ expect(() => getBuilder().setName('どうも').toJSON()).not.toThrowError();
expect(() => getBuilder().setName('🎉').toJSON()).not.toThrowError();
expect(() => getBuilder().setName('').toJSON()).not.toThrowError();
@@ -41,8 +41,15 @@ describe('Context Menu Commands', () => {
};
test('GIVEN valid name localizations THEN does not throw error', () => {
- expect(() => getBuilder().setNameLocalization(Locale.EnglishUS, 'foobar')).not.toThrowError();
- expect(() => getBuilder().setNameLocalizations({ [Locale.EnglishUS]: 'foobar' })).not.toThrowError();
+ expect(() =>
+ getBuilder().setName('test').setNameLocalization(Locale.EnglishUS, 'foobar').toJSON(),
+ ).not.toThrowError();
+ expect(() =>
+ getBuilder()
+ .setName('test')
+ .setNameLocalizations({ [Locale.EnglishUS]: 'foobar' })
+ .toJSON(),
+ ).not.toThrowError();
});
test('GIVEN invalid name localizations THEN does throw error', () => {
@@ -71,12 +78,15 @@ describe('Context Menu Commands', () => {
describe('permissions', () => {
test('GIVEN valid permission string THEN does not throw error', () => {
- expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
+ expect(() => getBuilder().setName('test').setDefaultMemberPermissions('1').toJSON()).not.toThrowError();
});
test('GIVEN valid permission bitfield THEN does not throw error', () => {
expect(() =>
- getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles),
+ getBuilder()
+ .setName('test')
+ .setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles)
+ .toJSON(),
).not.toThrowError();
});
@@ -90,11 +100,14 @@ describe('Context Menu Commands', () => {
describe('contexts', () => {
test('GIVEN a builder with valid contexts THEN does not throw an error', () => {
expect(() =>
- getBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]),
+ getBuilder()
+ .setName('test')
+ .setContexts([InteractionContextType.Guild, InteractionContextType.BotDM])
+ .toJSON(),
).not.toThrowError();
expect(() =>
- getBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM),
+ getBuilder().setName('test').setContexts(InteractionContextType.Guild, InteractionContextType.BotDM).toJSON(),
).not.toThrowError();
});
@@ -110,17 +123,17 @@ describe('Context Menu Commands', () => {
describe('integration types', () => {
test('GIVEN a builder with valid integration types THEN does not throw an error', () => {
expect(() =>
- getBuilder().setIntegrationTypes([
- ApplicationIntegrationType.GuildInstall,
- ApplicationIntegrationType.UserInstall,
- ]),
+ getBuilder()
+ .setName('test')
+ .setIntegrationTypes([ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall])
+ .toJSON(),
).not.toThrowError();
expect(() =>
- getBuilder().setIntegrationTypes(
- ApplicationIntegrationType.GuildInstall,
- ApplicationIntegrationType.UserInstall,
- ),
+ getBuilder()
+ .setName('test')
+ .setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
+ .toJSON(),
).not.toThrowError();
});
diff --git a/packages/builders/__tests__/messages/fileBody.test.ts b/packages/builders/__tests__/messages/fileBody.test.ts
index 2759ce610f8b..3f46707e0dee 100644
--- a/packages/builders/__tests__/messages/fileBody.test.ts
+++ b/packages/builders/__tests__/messages/fileBody.test.ts
@@ -6,7 +6,7 @@ import { AttachmentBuilder, MessageBuilder } from '../../src/index.js';
test('AttachmentBuilder stores and exposes file data', () => {
const data = Buffer.from('hello world');
const attachment = new AttachmentBuilder()
- .setId('0')
+ .setId(1)
.setFilename('greeting.txt')
.setFileData(data)
.setFileContentType('text/plain');
@@ -14,7 +14,7 @@ test('AttachmentBuilder stores and exposes file data', () => {
expect(attachment.getRawFile()).toStrictEqual({
contentType: 'text/plain',
data,
- key: 'files[0]',
+ key: 'files[1]',
name: 'greeting.txt',
});
@@ -24,10 +24,21 @@ test('AttachmentBuilder stores and exposes file data', () => {
expect(attachment.getRawFile()).toBe(undefined);
});
+test('AttachmentBuilder handles 0 as a valid id', () => {
+ const data = Buffer.from('test data');
+ const attachment = new AttachmentBuilder().setId(0).setFilename('test.txt').setFileData(data);
+
+ expect(attachment.getRawFile()).toStrictEqual({
+ data,
+ key: 'files[0]',
+ name: 'test.txt',
+ });
+});
+
test('MessageBuilder.toFileBody returns JSON body and files', () => {
const msg = new MessageBuilder().setContent('here is a file').addAttachments(
new AttachmentBuilder()
- .setId('0')
+ .setId(0)
.setFilename('file.bin')
.setFileData(Buffer.from([1, 2, 3]))
.setFileContentType('application/octet-stream'),
@@ -47,7 +58,9 @@ test('MessageBuilder.toFileBody returns JSON body and files', () => {
});
test('MessageBuilder.toFileBody returns empty files when attachments reference existing uploads', () => {
- const msg = new MessageBuilder().addAttachments(new AttachmentBuilder().setId('123').setFilename('existing.png'));
+ const msg = new MessageBuilder().addAttachments(
+ new AttachmentBuilder().setId('1234567890123456789').setFilename('existing.png'),
+ );
const { body, files } = msg.toFileBody();
expect(body).toEqual(msg.toJSON());
diff --git a/packages/builders/__tests__/messages/message.test.ts b/packages/builders/__tests__/messages/message.test.ts
index 3e05eecd08ac..01d94c8801e0 100644
--- a/packages/builders/__tests__/messages/message.test.ts
+++ b/packages/builders/__tests__/messages/message.test.ts
@@ -24,6 +24,19 @@ describe('Message', () => {
expect(() => message.toJSON()).toThrow();
});
+ test('GIVEN empty allowed mentions THEN return valid toJSON data', () => {
+ const allowedMentions = new AllowedMentionsBuilder();
+ expect(allowedMentions.toJSON()).toStrictEqual({});
+
+ const message = new MessageBuilder().setContent('test').setAllowedMentions();
+
+ expect(message.toJSON()).toStrictEqual({
+ ...base,
+ allowed_mentions: {},
+ content: 'test',
+ });
+ });
+
test('GIVEN parse: [users] and empty users THEN return valid toJSON data', () => {
const allowedMentions = new AllowedMentionsBuilder();
allowedMentions.setUsers();
@@ -64,7 +77,7 @@ describe('Message', () => {
row.addPrimaryButtonComponents((button) => button.setCustomId('abc').setLabel('def')),
)
.setStickerIds('123', '456')
- .addAttachments((attachment) => attachment.setId('hi!').setFilename('abc'))
+ .addAttachments((attachment) => attachment.setId(0).setFilename('abc'))
.setFlags(MessageFlags.Ephemeral)
.setEnforceNonce(false)
.updatePoll((poll) => poll.addAnswers({ poll_media: { text: 'foo' } }).setQuestion({ text: 'foo' }));
@@ -83,7 +96,7 @@ describe('Message', () => {
},
],
sticker_ids: ['123', '456'],
- attachments: [{ id: 'hi!', filename: 'abc' }],
+ attachments: [{ id: 0, filename: 'abc' }],
flags: 64,
enforce_nonce: false,
poll: {
diff --git a/packages/builders/package.json b/packages/builders/package.json
index 1f446e864d62..6029059d7f9e 100644
--- a/packages/builders/package.json
+++ b/packages/builders/package.json
@@ -66,28 +66,28 @@
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@discordjs/util": "workspace:^",
- "discord-api-types": "^0.38.31",
+ "discord-api-types": "^0.38.36",
"ts-mixer": "^6.0.4",
"tslib": "^2.8.1",
- "zod": "^4.1.12"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/builders/src/Assertions.ts b/packages/builders/src/Assertions.ts
index c672c95c08dc..b5bfb333122b 100644
--- a/packages/builders/src/Assertions.ts
+++ b/packages/builders/src/Assertions.ts
@@ -3,6 +3,7 @@ import { z } from 'zod';
export const idPredicate = z.int().min(0).max(2_147_483_647).optional();
export const customIdPredicate = z.string().min(1).max(100);
+export const snowflakePredicate = z.string().regex(/^(?:0|[1-9]\d*)$/);
export const memberPermissionsPredicate = z.coerce.bigint();
diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts
index ffded9cbd22b..492696a8272b 100644
--- a/packages/builders/src/components/ActionRow.ts
+++ b/packages/builders/src/components/ActionRow.ts
@@ -34,8 +34,9 @@ import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import { TextInputBuilder } from './textInput/TextInput.js';
-export interface ActionRowBuilderData
- extends Partial, 'components'>> {
+export interface ActionRowBuilderData extends Partial<
+ Omit, 'components'>
+> {
components: AnyActionRowComponentBuilder[];
}
diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts
index 67b9b44fc0c4..36f18202e076 100644
--- a/packages/builders/src/components/Assertions.ts
+++ b/packages/builders/src/components/Assertions.ts
@@ -1,12 +1,10 @@
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
import { z } from 'zod';
-import { idPredicate, customIdPredicate } from '../Assertions.js';
-
-const labelPredicate = z.string().min(1).max(80);
+import { idPredicate, customIdPredicate, snowflakePredicate } from '../Assertions.js';
export const emojiPredicate = z
.strictObject({
- id: z.string().optional(),
+ id: snowflakePredicate.optional(),
name: z.string().min(2).max(32).optional(),
animated: z.boolean().optional(),
})
@@ -19,27 +17,37 @@ const buttonPredicateBase = z.strictObject({
disabled: z.boolean().optional(),
});
-const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
- custom_id: customIdPredicate,
- emoji: emojiPredicate.optional(),
- label: labelPredicate,
-});
+const buttonLabelPredicate = z.string().min(1).max(80);
-const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) });
-const buttonSecondaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Secondary) });
-const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) });
-const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) });
+const buttonCustomIdPredicateBase = buttonPredicateBase
+ .extend({
+ custom_id: customIdPredicate,
+ emoji: emojiPredicate.optional(),
+ label: buttonLabelPredicate.optional(),
+ })
+ .refine((data) => data.emoji !== undefined || data.label !== undefined, {
+ message: 'Buttons with a custom id must have either an emoji or a label.',
+ });
-const buttonLinkPredicate = buttonPredicateBase.extend({
- style: z.literal(ButtonStyle.Link),
- url: z.url({ protocol: /^(?:https?|discord)$/ }).max(512),
- emoji: emojiPredicate.optional(),
- label: labelPredicate,
-});
+const buttonPrimaryPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Primary) });
+const buttonSecondaryPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Secondary) });
+const buttonSuccessPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Success) });
+const buttonDangerPredicate = buttonCustomIdPredicateBase.safeExtend({ style: z.literal(ButtonStyle.Danger) });
+
+const buttonLinkPredicate = buttonPredicateBase
+ .extend({
+ style: z.literal(ButtonStyle.Link),
+ url: z.url({ protocol: /^(?:https?|discord)$/ }).max(512),
+ emoji: emojiPredicate.optional(),
+ label: buttonLabelPredicate.optional(),
+ })
+ .refine((data) => data.emoji !== undefined || data.label !== undefined, {
+ message: 'Link buttons must have either an emoji or a label.',
+ });
const buttonPremiumPredicate = buttonPredicateBase.extend({
style: z.literal(ButtonStyle.Premium),
- sku_id: z.string(),
+ sku_id: snowflakePredicate,
});
export const buttonPredicate = z.discriminatedUnion('style', [
@@ -64,7 +72,7 @@ export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.ChannelSelect),
channel_types: z.enum(ChannelType).array().optional(),
default_values: z
- .object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
+ .object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.Channel) })
.array()
.max(25)
.optional(),
@@ -74,7 +82,7 @@ export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.MentionableSelect),
default_values: z
.object({
- id: z.string(),
+ id: snowflakePredicate,
type: z.literal([SelectMenuDefaultValueType.Role, SelectMenuDefaultValueType.User]),
})
.array()
@@ -85,14 +93,14 @@ export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.RoleSelect),
default_values: z
- .object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
+ .object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.Role) })
.array()
.max(25)
.optional(),
});
export const selectMenuStringOptionPredicate = z.object({
- label: labelPredicate,
+ label: z.string().min(1).max(100),
value: z.string().min(1).max(100),
description: z.string().min(1).max(100).optional(),
emoji: emojiPredicate.optional(),
@@ -142,7 +150,7 @@ export const selectMenuStringPredicate = selectMenuBasePredicate
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.UserSelect),
default_values: z
- .object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
+ .object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.User) })
.array()
.max(25)
.optional(),
diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts
index 9a9a59d12b3d..b875cea35a88 100644
--- a/packages/builders/src/components/Component.ts
+++ b/packages/builders/src/components/Component.ts
@@ -10,9 +10,9 @@ export interface ComponentBuilderBaseData {
*
* @typeParam Component - The type of API data that is stored within the builder
*/
-export abstract class ComponentBuilder>
- implements JSONEncodable
-{
+export abstract class ComponentBuilder<
+ Component extends APIBaseComponent,
+> implements JSONEncodable {
/**
* @internal
*/
diff --git a/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts b/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts
index 382641df6761..6259f8940dca 100644
--- a/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts
+++ b/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts
@@ -1,7 +1,9 @@
import type { APIButtonComponent, APIButtonComponentWithSKUId, APIMessageComponentEmoji } from 'discord-api-types/v10';
-export interface EmojiOrLabelButtonData
- extends Pick, 'emoji' | 'label'> {}
+export interface EmojiOrLabelButtonData extends Pick<
+ Exclude,
+ 'emoji' | 'label'
+> {}
/**
* A mixin that adds emoji and label symbols to a button builder.
diff --git a/packages/builders/src/interactions/commands/Command.ts b/packages/builders/src/interactions/commands/Command.ts
index 89016d18bf0a..bd8ff53b6f15 100644
--- a/packages/builders/src/interactions/commands/Command.ts
+++ b/packages/builders/src/interactions/commands/Command.ts
@@ -8,20 +8,16 @@ import type {
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 interface CommandData extends Partial<
+ Pick
+> {}
/**
* The base class for all command builders.
*/
-export abstract class CommandBuilder
- implements JSONEncodable
-{
+export abstract class CommandBuilder<
+ Command extends RESTPostAPIApplicationCommandsJSONBody,
+> implements JSONEncodable {
/**
* The API data associated with this command.
*
diff --git a/packages/builders/src/interactions/commands/SharedName.ts b/packages/builders/src/interactions/commands/SharedName.ts
index c9625bd180bb..17b317cc484b 100644
--- a/packages/builders/src/interactions/commands/SharedName.ts
+++ b/packages/builders/src/interactions/commands/SharedName.ts
@@ -1,7 +1,8 @@
import type { Locale, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
-export interface SharedNameData
- extends Partial> {}
+export interface SharedNameData extends Partial<
+ Pick
+> {}
/**
* This mixin holds name and description symbols for chat input commands.
diff --git a/packages/builders/src/interactions/commands/SharedNameAndDescription.ts b/packages/builders/src/interactions/commands/SharedNameAndDescription.ts
index 949e15c09082..0dd7e3a326da 100644
--- a/packages/builders/src/interactions/commands/SharedNameAndDescription.ts
+++ b/packages/builders/src/interactions/commands/SharedNameAndDescription.ts
@@ -3,8 +3,7 @@ import type { SharedNameData } from './SharedName.js';
import { SharedName } from './SharedName.js';
export interface SharedNameAndDescriptionData
- extends SharedNameData,
- Partial> {}
+ extends SharedNameData, Partial> {}
/**
* This mixin holds name and description symbols for chat input commands.
diff --git a/packages/builders/src/interactions/commands/chatInput/Assertions.ts b/packages/builders/src/interactions/commands/chatInput/Assertions.ts
index 8d16afd8ac44..2a313db8e276 100644
--- a/packages/builders/src/interactions/commands/chatInput/Assertions.ts
+++ b/packages/builders/src/interactions/commands/chatInput/Assertions.ts
@@ -112,8 +112,8 @@ export const numberOptionPredicate = z
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(),
+ max_length: z.number().min(1).max(6_000).optional(),
+ min_length: z.number().min(0).max(6_000).optional(),
})
.and(autocompleteOrStringChoicesMixinOptionPredicate);
@@ -127,8 +127,14 @@ const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
// 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(),
+ z
+ .object({
+ type: z.union([
+ z.literal(ApplicationCommandOptionType.Subcommand),
+ z.literal(ApplicationCommandOptionType.SubcommandGroup),
+ ]),
+ })
+ .array(),
]);
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts
index 10764b7e3992..db7bd4cb5261 100644
--- a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts
+++ b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts
@@ -1,7 +1,9 @@
import type { APIApplicationCommandIntegerOption } from 'discord-api-types/v10';
-export interface ApplicationCommandNumericOptionMinMaxValueData
- extends Pick {}
+export interface ApplicationCommandNumericOptionMinMaxValueData extends Pick<
+ APIApplicationCommandIntegerOption,
+ 'max_value' | 'min_value'
+> {}
/**
* This mixin holds minimum and maximum symbols used for options.
diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts
index ef80706aba8a..5012ad5a4901 100644
--- a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts
+++ b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts
@@ -19,8 +19,10 @@ export const ApplicationCommandOptionAllowedChannelTypes = [
*/
export type ApplicationCommandOptionAllowedChannelType = (typeof ApplicationCommandOptionAllowedChannelTypes)[number];
-export interface ApplicationCommandOptionChannelTypesData
- extends Pick {}
+export interface ApplicationCommandOptionChannelTypesData extends Pick<
+ APIApplicationCommandChannelOption,
+ 'channel_types'
+> {}
/**
* This mixin holds channel type symbols used for options.
diff --git a/packages/builders/src/messages/Assertions.ts b/packages/builders/src/messages/Assertions.ts
index 32f97512ef18..a913deac8d6b 100644
--- a/packages/builders/src/messages/Assertions.ts
+++ b/packages/builders/src/messages/Assertions.ts
@@ -1,6 +1,7 @@
import { Buffer } from 'node:buffer';
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
import { z } from 'zod';
+import { snowflakePredicate } from '../Assertions.js';
import { embedPredicate } from './embed/Assertions.js';
import { pollPredicate } from './poll/Assertions.js';
@@ -15,7 +16,7 @@ export const rawFilePredicate = z.object({
export const attachmentPredicate = z.object({
// As a string it only makes sense for edits when we do have an attachment snowflake
- id: z.union([z.string(), z.number()]),
+ id: z.union([snowflakePredicate, z.number()]),
description: z.string().max(1_024).optional(),
duration_secs: z
.number()
diff --git a/packages/builders/src/messages/Attachment.ts b/packages/builders/src/messages/Attachment.ts
index ba631a53930c..0c9b2494cbdc 100644
--- a/packages/builders/src/messages/Attachment.ts
+++ b/packages/builders/src/messages/Attachment.ts
@@ -28,6 +28,11 @@ export class AttachmentBuilder implements JSONEncodable {
* Creates a new attachment builder.
*
* @param data - The API data to create this attachment with
+ * @example
+ * ```ts
+ * const attachment = new AttachmentBuilder().setId(1).setFileData(':)').setFilename('smiley.txt')
+ * ```
+ * @remarks Please note that the `id` field is required, it's rather easy to miss!
*/
public constructor(data: Partial = {}) {
this.data = structuredClone(data);
@@ -148,7 +153,7 @@ export class AttachmentBuilder implements JSONEncodable {
return {
...this.fileData,
name: this.data.filename,
- key: this.data.id ? `files[${this.data.id}]` : undefined,
+ key: this.data.id === undefined ? undefined : `files[${this.data.id}]`,
};
}
diff --git a/packages/builders/src/messages/Message.ts b/packages/builders/src/messages/Message.ts
index 85a5bed7c241..66feafa48d17 100644
--- a/packages/builders/src/messages/Message.ts
+++ b/packages/builders/src/messages/Message.ts
@@ -38,13 +38,12 @@ import { MessageReferenceBuilder } from './MessageReference.js';
import { EmbedBuilder } from './embed/Embed.js';
import { PollBuilder } from './poll/Poll.js';
-export interface MessageBuilderData
- extends Partial<
- Omit<
- RESTPostAPIChannelMessageJSONBody,
- 'allowed_mentions' | 'attachments' | 'components' | 'embeds' | 'message_reference' | 'poll'
- >
- > {
+export interface MessageBuilderData extends Partial<
+ Omit<
+ RESTPostAPIChannelMessageJSONBody,
+ 'allowed_mentions' | 'attachments' | 'components' | 'embeds' | 'message_reference' | 'poll'
+ >
+> {
allowed_mentions?: AllowedMentionsBuilder;
attachments: AttachmentBuilder[];
components: MessageTopLevelComponentBuilder[];
@@ -240,7 +239,7 @@ export class MessageBuilder
allowedMentions:
| AllowedMentionsBuilder
| APIAllowedMentions
- | ((builder: AllowedMentionsBuilder) => AllowedMentionsBuilder),
+ | ((builder: AllowedMentionsBuilder) => AllowedMentionsBuilder) = new AllowedMentionsBuilder(),
): this {
this.data.allowed_mentions = resolveBuilder(allowedMentions, AllowedMentionsBuilder);
return this;
diff --git a/packages/collection/.cliff-jumperrc.json b/packages/collection/.cliff-jumperrc.json
index 1061740e30cd..766a974b2b8f 100644
--- a/packages/collection/.cliff-jumperrc.json
+++ b/packages/collection/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "collection",
"org": "discordjs",
"packagePath": "packages/collection",
diff --git a/packages/collection/package.json b/packages/collection/package.json
index 6632635f9759..b504c8534b87 100644
--- a/packages/collection/package.json
+++ b/packages/collection/package.json
@@ -63,20 +63,20 @@
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/core/.cliff-jumperrc.json b/packages/core/.cliff-jumperrc.json
index 9d7626d730b3..c63a97c957c8 100644
--- a/packages/core/.cliff-jumperrc.json
+++ b/packages/core/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "core",
"org": "discordjs",
"packagePath": "packages/core",
diff --git a/packages/core/package.json b/packages/core/package.json
index 7ea3056eedab..ce2b58b9c9bb 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -70,25 +70,25 @@
"@discordjs/ws": "workspace:^",
"@sapphire/snowflake": "^3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.7",
- "discord-api-types": "^0.38.31"
+ "discord-api-types": "^0.38.36"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/core/src/api/interactions.ts b/packages/core/src/api/interactions.ts
index f159f2648210..0be2439d118f 100644
--- a/packages/core/src/api/interactions.ts
+++ b/packages/core/src/api/interactions.ts
@@ -16,8 +16,7 @@ import {
import type { WebhooksAPI } from './webhook.js';
export interface CreateInteractionResponseOptions
- extends APIInteractionResponseCallbackData,
- RESTPostAPIInteractionCallbackQuery {
+ extends APIInteractionResponseCallbackData, RESTPostAPIInteractionCallbackQuery {
files?: RawFile[];
}
diff --git a/packages/create-discord-bot/.cliff-jumperrc.json b/packages/create-discord-bot/.cliff-jumperrc.json
index b914ffec4ede..2631146f64cc 100644
--- a/packages/create-discord-bot/.cliff-jumperrc.json
+++ b/packages/create-discord-bot/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "create-discord-bot",
"packagePath": "packages/create-discord-bot",
"tagTemplate": "{{name}}@{{new-version}}",
diff --git a/packages/create-discord-bot/package.json b/packages/create-discord-bot/package.json
index 2deb475fcc90..643db0349e7f 100644
--- a/packages/create-discord-bot/package.json
+++ b/packages/create-discord-bot/package.json
@@ -56,18 +56,18 @@
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
"@types/prompts": "^2.4.9",
"@types/validate-npm-package-name": "^4.0.2",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
"typescript": "~5.9.3"
},
"engines": {
diff --git a/packages/create-discord-bot/src/create-discord-bot.ts b/packages/create-discord-bot/src/create-discord-bot.ts
index c6925c84b050..8549bc4b3aa7 100644
--- a/packages/create-discord-bot/src/create-discord-bot.ts
+++ b/packages/create-discord-bot/src/create-discord-bot.ts
@@ -1,11 +1,11 @@
import type { ExecException } from 'node:child_process';
-import { cp, glob, mkdir, stat, readdir, readFile, writeFile } from 'node:fs/promises';
+import { cp, mkdir, stat, readdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { URL } from 'node:url';
import { styleText } from 'node:util';
import type { PackageManager } from './helpers/packageManager.js';
-import { install, isNodePackageManager } from './helpers/packageManager.js';
+import { install } from './helpers/packageManager.js';
import { GUIDE_URL } from './util/constants.js';
interface Options {
@@ -67,39 +67,18 @@ export async function createDiscordBot({ directory, installPackages, typescript,
process.chdir(root);
- const newVSCodeSettings = await readFile('./.vscode/settings.json', {
- encoding: 'utf8',
- }).then((str) => {
- let newStr = str.replace('[REPLACE_ME]', deno || bun ? 'auto' : packageManager);
- if (deno) {
- // @ts-expect-error: This is fine
- newStr = newStr.replaceAll('"[REPLACE_BOOL]"', true);
- }
-
- return newStr;
- });
- await writeFile('./.vscode/settings.json', newVSCodeSettings);
-
- const globIterator = glob('./src/**/*.ts');
- for await (const file of globIterator) {
- const newData = await readFile(file, { encoding: 'utf8' }).then((str) =>
- str.replaceAll('[REPLACE_IMPORT_EXT]', typescript && !isNodePackageManager(packageManager) ? 'ts' : 'js'),
- );
- await writeFile(file, newData);
- }
+ const newVSCodeSettings = await readFile('./.vscode/settings.json', { encoding: 'utf8' });
+ await writeFile(
+ './.vscode/settings.json',
+ newVSCodeSettings.replace(
+ /"npm\.packageManager":\s*"[^"]+"/,
+ `"npm.packageManager": "${deno || bun ? 'auto' : packageManager}"`,
+ ),
+ );
if (!deno) {
- const newPackageJSON = await readFile('./package.json', {
- encoding: 'utf8',
- }).then((str) => {
- let newStr = str.replace('[REPLACE_ME]', directoryName);
- newStr = newStr.replaceAll(
- '[REPLACE_IMPORT_EXT]',
- typescript && !isNodePackageManager(packageManager) ? 'ts' : 'js',
- );
- return newStr;
- });
- await writeFile('./package.json', newPackageJSON);
+ const newPackageJSON = await readFile('./package.json', { encoding: 'utf8' });
+ await writeFile('./package.json', newPackageJSON.replace(/"name":\s*"[^"]+"/, `"name": "${directoryName}"`));
}
if (installPackages) {
diff --git a/packages/create-discord-bot/src/helpers/packageManager.ts b/packages/create-discord-bot/src/helpers/packageManager.ts
index 4d02c679a425..ff5d981500c0 100644
--- a/packages/create-discord-bot/src/helpers/packageManager.ts
+++ b/packages/create-discord-bot/src/helpers/packageManager.ts
@@ -1,12 +1,12 @@
import { execSync } from 'node:child_process';
import process from 'node:process';
import { styleText } from 'node:util';
-import { DEFAULT_PACKAGE_MANAGER, NODE_PACKAGE_MANAGERS } from '../util/constants.js';
+import { DEFAULT_PACKAGE_MANAGER, type PACKAGE_MANAGERS } from '../util/constants.js';
/**
* A union of supported package managers.
*/
-export type PackageManager = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn';
+export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
/**
* Resolves the package manager from `npm_config_user_agent`.
@@ -117,12 +117,3 @@ export function install(packageManager: PackageManager) {
env,
});
}
-
-/**
- * Whether the provided package manager is a Node package manager.
- *
- * @param packageManager - The package manager to check
- */
-export function isNodePackageManager(packageManager: PackageManager): packageManager is 'npm' | 'pnpm' | 'yarn' {
- return NODE_PACKAGE_MANAGERS.includes(packageManager as any);
-}
diff --git a/packages/create-discord-bot/src/util/constants.ts b/packages/create-discord-bot/src/util/constants.ts
index 3193a01c8ec7..828748197a67 100644
--- a/packages/create-discord-bot/src/util/constants.ts
+++ b/packages/create-discord-bot/src/util/constants.ts
@@ -13,11 +13,6 @@ export const DEFAULT_PROJECT_NAME = 'my-bot' as const;
*/
export const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun', 'deno'] as const;
-/**
- * The supported Node.js package managers.
- */
-export const NODE_PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn'] as const;
-
/**
* The URL to the guide.
*/
diff --git a/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js b/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js
new file mode 100644
index 000000000000..5d92eaf15f97
--- /dev/null
+++ b/packages/create-discord-bot/template/Bun/JavaScript/eslint.config.js
@@ -0,0 +1,21 @@
+import common from 'eslint-config-neon/common';
+import node from 'eslint-config-neon/node';
+import prettier from 'eslint-config-neon/prettier';
+
+const config = [
+ {
+ ignores: [],
+ },
+ ...common,
+ ...node,
+ ...prettier,
+ {
+ rules: {
+ 'jsdoc/check-tag-names': 0,
+ 'jsdoc/no-undefined-types': 0,
+ 'jsdoc/valid-types': 0,
+ },
+ },
+];
+
+export default config;
diff --git a/packages/create-discord-bot/template/Bun/JavaScript/package.json b/packages/create-discord-bot/template/Bun/JavaScript/package.json
index eb216348fe6d..21c88e57ef74 100644
--- a/packages/create-discord-bot/template/Bun/JavaScript/package.json
+++ b/packages/create-discord-bot/template/Bun/JavaScript/package.json
@@ -1,24 +1,24 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "[REPLACE_ME]",
+ "name": "@discordjs/template-bun-javascript",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
- "lint": "prettier --check . && eslint --ext .[REPLACE_IMPORT_EXT] --format=pretty src",
- "deploy": "bun run src/util/deploy.[REPLACE_IMPORT_EXT]",
- "format": "prettier --write . && eslint --ext .[REPLACE_IMPORT_EXT] --fix --format=pretty src",
- "start": "bun run src/index.[REPLACE_IMPORT_EXT]"
+ "lint": "prettier --check . && eslint --ext .js --format=pretty src",
+ "deploy": "bun run src/util/deploy.js",
+ "format": "prettier --write . && eslint --ext .js --fix --format=pretty src",
+ "start": "bun run src/index.js"
},
"dependencies": {
- "@discordjs/core": "^2.3.0",
- "discord.js": "^14.24.2"
+ "@discordjs/core": "^2.4.0",
+ "discord.js": "^14.25.1"
},
"devDependencies": {
- "eslint": "^9.38.0",
+ "eslint": "^9.38.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "zod": "^4.1.12"
+ "prettier": "^3.7.4",
+ "zod": "^4.1.13"
}
}
diff --git a/packages/create-discord-bot/template/Bun/JavaScript/src/index.js b/packages/create-discord-bot/template/Bun/JavaScript/src/index.js
new file mode 100644
index 000000000000..b7bd4c885163
--- /dev/null
+++ b/packages/create-discord-bot/template/Bun/JavaScript/src/index.js
@@ -0,0 +1 @@
+console.log();
diff --git a/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js b/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js
new file mode 100644
index 000000000000..041e3ca1079b
--- /dev/null
+++ b/packages/create-discord-bot/template/Bun/TypeScript/eslint.config.js
@@ -0,0 +1,26 @@
+import common from 'eslint-config-neon/common';
+import node from 'eslint-config-neon/node';
+import prettier from 'eslint-config-neon/prettier';
+import typescript from 'eslint-config-neon/typescript';
+
+const config = [
+ {
+ ignores: [],
+ },
+ ...common,
+ ...node,
+ ...typescript,
+ ...prettier,
+ {
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.eslint.json'],
+ },
+ },
+ rules: {
+ 'import/extensions': 0,
+ },
+ },
+];
+
+export default config;
diff --git a/packages/create-discord-bot/template/Bun/TypeScript/package.json b/packages/create-discord-bot/template/Bun/TypeScript/package.json
index 7f736c7993f5..c53c3f8290e2 100644
--- a/packages/create-discord-bot/template/Bun/TypeScript/package.json
+++ b/packages/create-discord-bot/template/Bun/TypeScript/package.json
@@ -1,27 +1,27 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "[REPLACE_ME]",
+ "name": "@discordjs/template-bun-typescript",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
- "lint": "tsc && prettier --check . && eslint --ext .[REPLACE_IMPORT_EXT] --format=pretty src",
- "deploy": "bun run src/util/deploy.[REPLACE_IMPORT_EXT]",
- "format": "prettier --write . && eslint --ext .[REPLACE_IMPORT_EXT] --fix --format=pretty src",
- "start": "bun run src/index.[REPLACE_IMPORT_EXT]"
+ "lint": "tsc && prettier --check . && eslint --ext .ts --format=pretty src",
+ "deploy": "bun run src/util/deploy.ts",
+ "format": "prettier --write . && eslint --ext .ts --fix --format=pretty src",
+ "start": "bun run src/index.ts"
},
"dependencies": {
- "@discordjs/core": "^2.3.0",
- "discord.js": "^14.24.2"
+ "@discordjs/core": "^2.4.0",
+ "discord.js": "^14.25.1"
},
"devDependencies": {
- "@sapphire/ts-config": "^5.0.1",
- "@types/bun": "^1.3.1",
- "eslint": "^9.38.0",
+ "@sapphire/ts-config": "^5.0.3",
+ "@types/bun": "^1.3.3",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
+ "prettier": "^3.7.4",
"typescript": "~5.9.3",
- "zod": "^4.1.12"
+ "zod": "^4.1.13"
}
}
diff --git a/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts b/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts
new file mode 100644
index 000000000000..cb0ff5c3b541
--- /dev/null
+++ b/packages/create-discord-bot/template/Bun/TypeScript/src/index.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.json b/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.json
index 17d3c1864f0b..7e6c323b1500 100644
--- a/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.json
+++ b/packages/create-discord-bot/template/Bun/TypeScript/tsconfig.json
@@ -2,14 +2,14 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": ["@sapphire/ts-config", "@sapphire/ts-config/extra-strict"],
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"declaration": false,
"declarationMap": false,
+ "incremental": false,
"module": "ESNext",
"moduleResolution": "Bundler",
- "target": "ESNext",
- "outDir": "dist",
"noEmit": true,
- "allowImportingTsExtensions": true,
+ "target": "ESNext",
"skipLibCheck": true
}
}
diff --git a/packages/create-discord-bot/template/Deno/.vscode/settings.json b/packages/create-discord-bot/template/Deno/.vscode/settings.json
index 6457249fed9a..eb4101864192 100644
--- a/packages/create-discord-bot/template/Deno/.vscode/settings.json
+++ b/packages/create-discord-bot/template/Deno/.vscode/settings.json
@@ -8,5 +8,5 @@
"editor.trimAutoWhitespace": false,
"files.insertFinalNewline": true,
"files.eol": "\n",
- "deno.enable": "[REPLACE_BOOL]"
+ "deno.enable": true
}
diff --git a/packages/create-discord-bot/template/Deno/src/util/loaders.ts b/packages/create-discord-bot/template/Deno/src/util/loaders.ts
index 9a1d9cbc61f2..b123e3c243c9 100644
--- a/packages/create-discord-bot/template/Deno/src/util/loaders.ts
+++ b/packages/create-discord-bot/template/Deno/src/util/loaders.ts
@@ -1,6 +1,6 @@
import type { PathLike } from 'node:fs';
import { glob, stat } from 'node:fs/promises';
-import { resolve } from 'node:path';
+import { basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Command } from '../commands/index.ts';
import { predicate as commandPredicate } from '../commands/index.ts';
@@ -43,7 +43,7 @@ export async function loadStructures(
// Loop through all the matching files in the directory
for await (const file of glob(pattern)) {
// If the file is index.ts, skip the file
- if (file.endsWith('/index.ts')) {
+ if (basename(file) === 'index.ts') {
continue;
}
diff --git a/packages/create-discord-bot/template/JavaScript/.prettierignore b/packages/create-discord-bot/template/JavaScript/.prettierignore
new file mode 100644
index 000000000000..bd5535a6035b
--- /dev/null
+++ b/packages/create-discord-bot/template/JavaScript/.prettierignore
@@ -0,0 +1 @@
+pnpm-lock.yaml
diff --git a/packages/create-discord-bot/template/JavaScript/.vscode/extensions.json b/packages/create-discord-bot/template/JavaScript/.vscode/extensions.json
index 3d5debb1071a..e4679713f3fe 100644
--- a/packages/create-discord-bot/template/JavaScript/.vscode/extensions.json
+++ b/packages/create-discord-bot/template/JavaScript/.vscode/extensions.json
@@ -2,7 +2,6 @@
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
- "tamasfe.even-better-toml",
"codezombiech.gitignore",
"christian-kohler.npm-intellisense",
"christian-kohler.path-intellisense"
diff --git a/packages/create-discord-bot/template/JavaScript/.vscode/settings.json b/packages/create-discord-bot/template/JavaScript/.vscode/settings.json
index a3ff3551eea2..30619664d1ec 100644
--- a/packages/create-discord-bot/template/JavaScript/.vscode/settings.json
+++ b/packages/create-discord-bot/template/JavaScript/.vscode/settings.json
@@ -9,5 +9,5 @@
"editor.trimAutoWhitespace": false,
"files.insertFinalNewline": true,
"files.eol": "\n",
- "npm.packageManager": "[REPLACE_ME]"
+ "npm.packageManager": "[REPLACE_PACKAGE_MANAGER]"
}
diff --git a/packages/create-discord-bot/template/JavaScript/package.json b/packages/create-discord-bot/template/JavaScript/package.json
index 354aff0dac73..1c60bd8ba738 100644
--- a/packages/create-discord-bot/template/JavaScript/package.json
+++ b/packages/create-discord-bot/template/JavaScript/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "[REPLACE_ME]",
+ "name": "@discordjs/template-javascript",
"version": "0.1.0",
"private": true,
"type": "module",
@@ -11,15 +11,15 @@
"deploy": "node --env-file=.env src/util/deploy.js"
},
"dependencies": {
- "@discordjs/core": "^2.3.0",
- "discord.js": "^14.24.2"
+ "@discordjs/core": "^2.4.0",
+ "discord.js": "^14.25.1"
},
"devDependencies": {
- "eslint": "^9.38.0",
+ "eslint": "^9.38.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "zod": "^4.1.12"
+ "prettier": "^3.7.4",
+ "zod": "^4.1.13"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/create-discord-bot/template/JavaScript/src/util/loaders.js b/packages/create-discord-bot/template/JavaScript/src/util/loaders.js
index 8bde35f3baba..4b37580cba69 100644
--- a/packages/create-discord-bot/template/JavaScript/src/util/loaders.js
+++ b/packages/create-discord-bot/template/JavaScript/src/util/loaders.js
@@ -1,5 +1,5 @@
import { glob, stat } from 'node:fs/promises';
-import { resolve } from 'node:path';
+import { basename, resolve } from 'node:path';
import { fileURLToPath, URL } from 'node:url';
import { predicate as commandPredicate } from '../commands/index.js';
import { predicate as eventPredicate } from '../events/index.js';
@@ -40,7 +40,7 @@ export async function loadStructures(dir, predicate, recursive = true) {
// Loop through all the matching files in the directory
for await (const file of glob(pattern)) {
// If the file is index.js, skip the file
- if (file.endsWith('/index.js')) {
+ if (basename(file) === 'index.js') {
continue;
}
diff --git a/packages/create-discord-bot/template/TypeScript/.prettierignore b/packages/create-discord-bot/template/TypeScript/.prettierignore
index 1521c8b7652b..bd5535a6035b 100644
--- a/packages/create-discord-bot/template/TypeScript/.prettierignore
+++ b/packages/create-discord-bot/template/TypeScript/.prettierignore
@@ -1 +1 @@
-dist
+pnpm-lock.yaml
diff --git a/packages/create-discord-bot/template/TypeScript/.vscode/extensions.json b/packages/create-discord-bot/template/TypeScript/.vscode/extensions.json
index 3d5debb1071a..e4679713f3fe 100644
--- a/packages/create-discord-bot/template/TypeScript/.vscode/extensions.json
+++ b/packages/create-discord-bot/template/TypeScript/.vscode/extensions.json
@@ -2,7 +2,6 @@
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
- "tamasfe.even-better-toml",
"codezombiech.gitignore",
"christian-kohler.npm-intellisense",
"christian-kohler.path-intellisense"
diff --git a/packages/create-discord-bot/template/TypeScript/.vscode/settings.json b/packages/create-discord-bot/template/TypeScript/.vscode/settings.json
index ae55b963ba78..f756167ddcee 100644
--- a/packages/create-discord-bot/template/TypeScript/.vscode/settings.json
+++ b/packages/create-discord-bot/template/TypeScript/.vscode/settings.json
@@ -9,5 +9,5 @@
"editor.trimAutoWhitespace": false,
"files.insertFinalNewline": true,
"files.eol": "\n",
- "npm.packageManager": "[REPLACE_ME]"
+ "npm.packageManager": "[REPLACE_PACKAGE_MANAGER]"
}
diff --git a/packages/create-discord-bot/template/TypeScript/eslint.config.js b/packages/create-discord-bot/template/TypeScript/eslint.config.js
index 777913a13585..041e3ca1079b 100644
--- a/packages/create-discord-bot/template/TypeScript/eslint.config.js
+++ b/packages/create-discord-bot/template/TypeScript/eslint.config.js
@@ -5,7 +5,7 @@ import typescript from 'eslint-config-neon/typescript';
const config = [
{
- ignores: ['**/dist/*'],
+ ignores: [],
},
...common,
...node,
diff --git a/packages/create-discord-bot/template/TypeScript/package.json b/packages/create-discord-bot/template/TypeScript/package.json
index 35b38f805807..83b26ca47234 100644
--- a/packages/create-discord-bot/template/TypeScript/package.json
+++ b/packages/create-discord-bot/template/TypeScript/package.json
@@ -1,29 +1,29 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "[REPLACE_ME]",
+ "name": "@discordjs/template-typescript",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"lint": "prettier --check . && eslint --ext .ts --format=pretty src",
- "deploy": "node --env-file=.env dist/util/deploy.js",
+ "deploy": "node --env-file=.env src/util/deploy.ts",
"format": "prettier --write . && eslint --ext .ts --fix --format=pretty src",
- "start": "node --env-file=.env dist/index.js"
+ "start": "node --env-file=.env src/index.ts"
},
"dependencies": {
- "@discordjs/core": "^2.3.0",
- "discord.js": "^14.24.2"
+ "@discordjs/core": "^2.4.0",
+ "discord.js": "^14.25.1"
},
"devDependencies": {
- "@sapphire/ts-config": "^5.0.1",
- "@types/node": "^22.18.13",
- "eslint": "^9.38.0",
+ "@sapphire/ts-config": "^5.0.3",
+ "@types/node": "^22.19.1",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
+ "prettier": "^3.7.4",
"typescript": "~5.9.3",
- "zod": "^4.1.12"
+ "zod": "^4.1.13"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/create-discord-bot/template/TypeScript/src/commands/index.ts b/packages/create-discord-bot/template/TypeScript/src/commands/index.ts
index 0da41a43a0a7..ba63bf6daf74 100644
--- a/packages/create-discord-bot/template/TypeScript/src/commands/index.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/commands/index.ts
@@ -1,6 +1,6 @@
import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'discord.js';
import { z } from 'zod';
-import type { StructurePredicate } from '../util/loaders.[REPLACE_IMPORT_EXT]';
+import type { StructurePredicate } from '../util/loaders.ts';
/**
* Defines the structure of a command
diff --git a/packages/create-discord-bot/template/TypeScript/src/commands/ping.ts b/packages/create-discord-bot/template/TypeScript/src/commands/ping.ts
index a72cc3a08859..7b30e8273e8b 100644
--- a/packages/create-discord-bot/template/TypeScript/src/commands/ping.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/commands/ping.ts
@@ -1,4 +1,4 @@
-import type { Command } from './index.[REPLACE_IMPORT_EXT]';
+import type { Command } from './index.ts';
export default {
data: {
diff --git a/packages/create-discord-bot/template/TypeScript/src/commands/utility/user.ts b/packages/create-discord-bot/template/TypeScript/src/commands/utility/user.ts
index 121d4cec72b5..34bc5315a64e 100644
--- a/packages/create-discord-bot/template/TypeScript/src/commands/utility/user.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/commands/utility/user.ts
@@ -1,4 +1,4 @@
-import type { Command } from '../index.[REPLACE_IMPORT_EXT]';
+import type { Command } from '../index.ts';
export default {
data: {
diff --git a/packages/create-discord-bot/template/TypeScript/src/events/index.ts b/packages/create-discord-bot/template/TypeScript/src/events/index.ts
index 516ac1edbd9d..ccbfe5c18287 100644
--- a/packages/create-discord-bot/template/TypeScript/src/events/index.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/events/index.ts
@@ -1,6 +1,6 @@
import type { ClientEvents } from 'discord.js';
import { z } from 'zod';
-import type { StructurePredicate } from '../util/loaders.[REPLACE_IMPORT_EXT]';
+import type { StructurePredicate } from '../util/loaders.ts';
/**
* Defines the structure of an event.
diff --git a/packages/create-discord-bot/template/TypeScript/src/events/interactionCreate.ts b/packages/create-discord-bot/template/TypeScript/src/events/interactionCreate.ts
index 4e0f0a90034d..fe37d3d67624 100644
--- a/packages/create-discord-bot/template/TypeScript/src/events/interactionCreate.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/events/interactionCreate.ts
@@ -1,7 +1,7 @@
import { URL } from 'node:url';
import { Events } from 'discord.js';
-import { loadCommands } from '../util/loaders.[REPLACE_IMPORT_EXT]';
-import type { Event } from './index.[REPLACE_IMPORT_EXT]';
+import { loadCommands } from '../util/loaders.ts';
+import type { Event } from './index.ts';
const commands = await loadCommands(new URL('../commands/', import.meta.url));
diff --git a/packages/create-discord-bot/template/TypeScript/src/events/ready.ts b/packages/create-discord-bot/template/TypeScript/src/events/ready.ts
index c5917b92313a..5fd6216f9840 100644
--- a/packages/create-discord-bot/template/TypeScript/src/events/ready.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/events/ready.ts
@@ -1,5 +1,5 @@
import { Events } from 'discord.js';
-import type { Event } from './index.[REPLACE_IMPORT_EXT]';
+import type { Event } from './index.ts';
export default {
name: Events.ClientReady,
diff --git a/packages/create-discord-bot/template/TypeScript/src/index.ts b/packages/create-discord-bot/template/TypeScript/src/index.ts
index 5c6001733ddc..ddb980d950bb 100644
--- a/packages/create-discord-bot/template/TypeScript/src/index.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/index.ts
@@ -1,7 +1,7 @@
import process from 'node:process';
import { URL } from 'node:url';
import { Client, GatewayIntentBits } from 'discord.js';
-import { loadEvents } from './util/loaders.[REPLACE_IMPORT_EXT]';
+import { loadEvents } from './util/loaders.ts';
// Initialize the client
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
diff --git a/packages/create-discord-bot/template/TypeScript/src/util/deploy.ts b/packages/create-discord-bot/template/TypeScript/src/util/deploy.ts
index ee2b429c56da..a5b7ef25a6a8 100644
--- a/packages/create-discord-bot/template/TypeScript/src/util/deploy.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/util/deploy.ts
@@ -2,7 +2,7 @@ import process from 'node:process';
import { URL } from 'node:url';
import { API } from '@discordjs/core/http-only';
import { REST } from 'discord.js';
-import { loadCommands } from './loaders.[REPLACE_IMPORT_EXT]';
+import { loadCommands } from './loaders.ts';
const commands = await loadCommands(new URL('../commands/', import.meta.url));
const commandData = [...commands.values()].map((command) => command.data);
diff --git a/packages/create-discord-bot/template/TypeScript/src/util/loaders.ts b/packages/create-discord-bot/template/TypeScript/src/util/loaders.ts
index 09bad738cfe7..94f8cab10126 100644
--- a/packages/create-discord-bot/template/TypeScript/src/util/loaders.ts
+++ b/packages/create-discord-bot/template/TypeScript/src/util/loaders.ts
@@ -1,11 +1,9 @@
import type { PathLike } from 'node:fs';
import { glob, stat } from 'node:fs/promises';
-import { resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import type { Command } from '../commands/index.[REPLACE_IMPORT_EXT]';
-import { predicate as commandPredicate } from '../commands/index.[REPLACE_IMPORT_EXT]';
-import type { Event } from '../events/index.[REPLACE_IMPORT_EXT]';
-import { predicate as eventPredicate } from '../events/index.[REPLACE_IMPORT_EXT]';
+import { basename, resolve } from 'node:path';
+import { fileURLToPath, URL } from 'node:url';
+import { predicate as commandPredicate, type Command } from '../commands/index.ts';
+import { predicate as eventPredicate, type Event } from '../events/index.ts';
/**
* A predicate to check if the structure is valid
@@ -36,14 +34,14 @@ export async function loadStructures(
// Create an empty array to store the structures
const structures: Structure[] = [];
- // Create a glob pattern to match the .[REPLACE_IMPORT_EXT] files
+ // Create a glob pattern to match the .ts files
const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
- const pattern = resolve(basePath, recursive ? '**/*.[REPLACE_IMPORT_EXT]' : '*.[REPLACE_IMPORT_EXT]');
+ const pattern = resolve(basePath, recursive ? '**/*.ts' : '*.ts');
// Loop through all the matching files in the directory
for await (const file of glob(pattern)) {
- // If the file is index.[REPLACE_IMPORT_EXT], skip the file
- if (file.endsWith('/index.[REPLACE_IMPORT_EXT]')) {
+ // If the file is index.ts, skip the file
+ if (basename(file) === 'index.ts') {
continue;
}
diff --git a/packages/create-discord-bot/template/TypeScript/tsconfig.json b/packages/create-discord-bot/template/TypeScript/tsconfig.json
index 01a98906e8eb..c825672771cf 100644
--- a/packages/create-discord-bot/template/TypeScript/tsconfig.json
+++ b/packages/create-discord-bot/template/TypeScript/tsconfig.json
@@ -2,12 +2,15 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": ["@sapphire/ts-config", "@sapphire/ts-config/extra-strict"],
"compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "erasableSyntaxOnly": true,
"declaration": false,
"declarationMap": false,
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
+ "incremental": false,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
"target": "ESNext",
- "outDir": "dist",
"skipLibCheck": true
}
}
diff --git a/packages/discord.js/.cliff-jumperrc.json b/packages/discord.js/.cliff-jumperrc.json
index b69ebe5790d5..fd83b87ebafc 100644
--- a/packages/discord.js/.cliff-jumperrc.json
+++ b/packages/discord.js/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "discord.js",
"packagePath": "packages/discord.js",
"tagTemplate": "{{new-version}}",
diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json
index d5731082256f..75b7e6879f8d 100644
--- a/packages/discord.js/package.json
+++ b/packages/discord.js/package.json
@@ -74,7 +74,7 @@
"@discordjs/ws": "workspace:^",
"@sapphire/snowflake": "3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.7",
- "discord-api-types": "^0.38.31",
+ "discord-api-types": "^0.38.36",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.12.1",
@@ -85,18 +85,18 @@
"@discordjs/api-extractor": "workspace:^",
"@discordjs/docgen": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsdoc": "^54.7.0",
- "prettier": "^3.6.2",
+ "prettier": "^3.7.4",
"tsd": "^0.33.0",
- "turbo": "^2.5.8",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3"
},
"engines": {
diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js
index ad0a03c6336d..a3820c849c8b 100644
--- a/packages/discord.js/src/client/Client.js
+++ b/packages/discord.js/src/client/Client.js
@@ -389,15 +389,6 @@ class Client extends AsyncEventEmitter {
);
this.ws.on(WebSocketShardEvents.Dispatch, this._handlePacket.bind(this));
- this.ws.on(WebSocketShardEvents.Ready, async data => {
- for (const guild of data.guilds) {
- this.expectedGuilds.add(guild.id);
- }
-
- this.status = Status.WaitingForGuilds;
- await this._checkReady();
- });
-
this.ws.on(WebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency }, shardId) => {
this.emit(Events.Debug, `[WS => Shard ${shardId}] Heartbeat acknowledged, latency of ${latency}ms.`);
this.lastPingTimestamps.set(shardId, heartbeatAt);
@@ -427,7 +418,9 @@ class Client extends AsyncEventEmitter {
PacketHandlers[packet.t](this, packet, shardId);
}
- if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) {
+ if (packet.t === GatewayDispatchEvents.Ready) {
+ await this._checkReady();
+ } else if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) {
this.expectedGuilds.delete(packet.d.id);
await this._checkReady();
}
diff --git a/packages/discord.js/src/client/websocket/handlers/READY.js b/packages/discord.js/src/client/websocket/handlers/READY.js
index bf78a1368de6..393d832d4a79 100644
--- a/packages/discord.js/src/client/websocket/handlers/READY.js
+++ b/packages/discord.js/src/client/websocket/handlers/READY.js
@@ -1,6 +1,7 @@
'use strict';
const { ClientApplication } = require('../../../structures/ClientApplication.js');
+const { Status } = require('../../../util/Status.js');
let ClientUser;
@@ -14,6 +15,7 @@ module.exports = (client, { d: data }, shardId) => {
}
for (const guild of data.guilds) {
+ client.expectedGuilds.add(guild.id);
guild.shardId = shardId;
client.guilds._add(guild);
}
@@ -23,4 +25,6 @@ module.exports = (client, { d: data }, shardId) => {
} else {
client.application = new ClientApplication(client, data.application);
}
+
+ client.status = Status.WaitingForGuilds;
};
diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js
index edb1ad4c644c..23617c058d66 100644
--- a/packages/discord.js/src/index.js
+++ b/packages/discord.js/src/index.js
@@ -111,7 +111,6 @@ exports.ApplicationEmoji = require('./structures/ApplicationEmoji.js').Applicati
exports.ApplicationRoleConnectionMetadata =
require('./structures/ApplicationRoleConnectionMetadata.js').ApplicationRoleConnectionMetadata;
exports.Attachment = require('./structures/Attachment.js').Attachment;
-exports.AttachmentBuilder = require('./structures/AttachmentBuilder.js').AttachmentBuilder;
exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction.js').AutocompleteInteraction;
exports.AutoModerationActionExecution =
require('./structures/AutoModerationActionExecution.js').AutoModerationActionExecution;
diff --git a/packages/discord.js/src/managers/ChannelManager.js b/packages/discord.js/src/managers/ChannelManager.js
index b1d3a183af0c..306715c4c0d9 100644
--- a/packages/discord.js/src/managers/ChannelManager.js
+++ b/packages/discord.js/src/managers/ChannelManager.js
@@ -1,7 +1,7 @@
'use strict';
const process = require('node:process');
-const { lazy } = require('@discordjs/util');
+const { lazy, isFileBodyEncodable, isJSONEncodable } = require('@discordjs/util');
const { Routes } = require('discord-api-types/v10');
const { BaseChannel } = require('../structures/BaseChannel.js');
const { MessagePayload } = require('../structures/MessagePayload.js');
@@ -147,7 +147,7 @@ class ChannelManager extends CachedManager {
* Creates a message in a channel.
*
* @param {TextChannelResolvable} channel The channel to send the message to
- * @param {string|MessagePayload|MessageCreateOptions} options The options to provide
+ * @param {string|MessagePayload|MessageCreateOptions|JSONEncodable|FileBodyEncodable} options The options to provide
* @returns {Promise}
* @example
* // Send a basic message
@@ -174,18 +174,21 @@ class ChannelManager extends CachedManager {
* .catch(console.error);
*/
async createMessage(channel, options) {
- let messagePayload;
+ let payload;
if (options instanceof MessagePayload) {
- messagePayload = options.resolveBody();
+ payload = await options.resolveBody().resolveFiles();
+ } else if (isFileBodyEncodable(options)) {
+ payload = options.toFileBody();
+ } else if (isJSONEncodable(options)) {
+ payload = { body: options.toJSON() };
} else {
- messagePayload = MessagePayload.create(this, options).resolveBody();
+ payload = await MessagePayload.create(this, options).resolveBody().resolveFiles();
}
const resolvedChannelId = this.resolveId(channel);
const resolvedChannel = this.resolve(channel);
- const { body, files } = await messagePayload.resolveFiles();
- const data = await this.client.rest.post(Routes.channelMessages(resolvedChannelId), { body, files });
+ const data = await this.client.rest.post(Routes.channelMessages(resolvedChannelId), payload);
return resolvedChannel?.messages._add(data) ?? new (getMessage())(this.client, data);
}
diff --git a/packages/discord.js/src/managers/MessageManager.js b/packages/discord.js/src/managers/MessageManager.js
index d0a4402b4490..b9e0366b0b05 100644
--- a/packages/discord.js/src/managers/MessageManager.js
+++ b/packages/discord.js/src/managers/MessageManager.js
@@ -2,6 +2,7 @@
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
+const { isFileBodyEncodable, isJSONEncodable } = require('@discordjs/util');
const { Routes } = require('discord-api-types/v10');
const { DiscordjsTypeError, ErrorCodes } = require('../errors/index.js');
const { Message } = require('../structures/Message.js');
@@ -223,21 +224,27 @@ class MessageManager extends CachedManager {
* Edits a message, even if it's not cached.
*
* @param {MessageResolvable} message The message to edit
- * @param {string|MessageEditOptions|MessagePayload} options The options to edit the message
+ * @param {string|MessageEditOptions|MessagePayload|FileBodyEncodable|JSONEncodable} options The options to edit the message
* @returns {Promise}
*/
async edit(message, options) {
const messageId = this.resolveId(message);
if (!messageId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable');
- const { body, files } = await (
- options instanceof MessagePayload
- ? options
- : MessagePayload.create(message instanceof Message ? message : this, options)
- )
- .resolveBody()
- .resolveFiles();
- const data = await this.client.rest.patch(Routes.channelMessage(this.channel.id, messageId), { body, files });
+ let payload;
+ if (options instanceof MessagePayload) {
+ payload = await options.resolveBody().resolveFiles();
+ } else if (isFileBodyEncodable(options)) {
+ payload = options.toFileBody();
+ } else if (isJSONEncodable(options)) {
+ payload = { body: options.toJSON() };
+ } else {
+ payload = await MessagePayload.create(message instanceof Message ? message : this, options)
+ .resolveBody()
+ .resolveFiles();
+ }
+
+ const data = await this.client.rest.patch(Routes.channelMessage(this.channel.id, messageId), payload);
const existing = this.cache.get(messageId);
if (existing) {
diff --git a/packages/discord.js/src/structures/AttachmentBuilder.js b/packages/discord.js/src/structures/AttachmentBuilder.js
deleted file mode 100644
index 3dd8d1e86c89..000000000000
--- a/packages/discord.js/src/structures/AttachmentBuilder.js
+++ /dev/null
@@ -1,185 +0,0 @@
-'use strict';
-
-const { basename, flatten } = require('../util/Util.js');
-
-/**
- * Represents an attachment builder
- */
-class AttachmentBuilder {
- /**
- * @param {BufferResolvable|Stream} attachment The file
- * @param {AttachmentData} [data] Extra data
- */
- constructor(attachment, data = {}) {
- /**
- * The file associated with this attachment.
- *
- * @type {BufferResolvable|Stream}
- */
- this.attachment = attachment;
-
- /**
- * The name of this attachment
- *
- * @type {?string}
- */
- this.name = data.name;
-
- /**
- * The description of the attachment
- *
- * @type {?string}
- */
- this.description = data.description;
-
- /**
- * The title of the attachment
- *
- * @type {?string}
- */
- this.title = data.title;
-
- /**
- * The base64 encoded byte array representing a sampled waveform
- * This is only for voice message attachments.
- *
- * @type {?string}
- */
- this.waveform = data.waveform;
-
- /**
- * The duration of the attachment in seconds
- * This is only for voice message attachments.
- *
- * @type {?number}
- */
- this.duration = data.duration;
- }
-
- /**
- * Sets the description of this attachment.
- *
- * @param {string} description The description of the file
- * @returns {AttachmentBuilder} This attachment
- */
- setDescription(description) {
- this.description = description;
- return this;
- }
-
- /**
- * Sets the file of this attachment.
- *
- * @param {BufferResolvable|Stream} attachment The file
- * @returns {AttachmentBuilder} This attachment
- */
- setFile(attachment) {
- this.attachment = attachment;
- return this;
- }
-
- /**
- * Sets the name of this attachment.
- *
- * @param {string} name The name of the file
- * @returns {AttachmentBuilder} This attachment
- */
- setName(name) {
- this.name = name;
- return this;
- }
-
- /**
- * Sets the title of this attachment.
- *
- * @param {string} title The title of the file
- * @returns {AttachmentBuilder} This attachment
- */
- setTitle(title) {
- this.title = title;
- return this;
- }
-
- /**
- * Sets the waveform of this attachment.
- * This is only for voice message attachments.
- *
- * @param {string} waveform The base64 encoded byte array representing a sampled waveform
- * @returns {AttachmentBuilder} This attachment
- */
- setWaveform(waveform) {
- this.waveform = waveform;
- return this;
- }
-
- /**
- * Sets the duration of this attachment.
- * This is only for voice message attachments.
- *
- * @param {number} duration The duration of the attachment in seconds
- * @returns {AttachmentBuilder} This attachment
- */
- setDuration(duration) {
- this.duration = duration;
- return this;
- }
-
- /**
- * Sets whether this attachment is a spoiler
- *
- * @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler
- * @returns {AttachmentBuilder} This attachment
- */
- setSpoiler(spoiler = true) {
- if (spoiler === this.spoiler) return this;
-
- if (!spoiler) {
- while (this.spoiler) {
- this.name = this.name.slice('SPOILER_'.length);
- }
-
- return this;
- }
-
- this.name = `SPOILER_${this.name}`;
- return this;
- }
-
- /**
- * Whether or not this attachment has been marked as a spoiler
- *
- * @type {boolean}
- * @readonly
- */
- get spoiler() {
- return basename(this.name).startsWith('SPOILER_');
- }
-
- toJSON() {
- return flatten(this);
- }
-
- /**
- * Makes a new builder instance from a preexisting attachment structure.
- *
- * @param {AttachmentBuilder|Attachment|AttachmentPayload} other The builder to construct a new instance from
- * @returns {AttachmentBuilder}
- */
- static from(other) {
- return new AttachmentBuilder(other.attachment, {
- name: other.name,
- description: other.description,
- });
- }
-}
-
-exports.AttachmentBuilder = AttachmentBuilder;
-
-/**
- * @typedef {Object} AttachmentData
- * @property {string} [name] The name of the attachment
- * @property {string} [description] The description of the attachment
- * @property {string} [title] The title of the attachment
- * @property {string} [waveform] The base64 encoded byte array representing a sampled waveform (for voice message attachments)
- * @property {number} [duration] The duration of the attachment in seconds (for voice message attachments)
- */
diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js
index 72e03bb4a27b..1fdb1bff3373 100644
--- a/packages/discord.js/src/structures/Guild.js
+++ b/packages/discord.js/src/structures/Guild.js
@@ -641,7 +641,7 @@ class Guild extends AnonymousGuild {
}
/**
- * The maximum bitrate available for this guild
+ * The maximum bitrate available for a voice channel in this guild
*
* @type {number}
* @readonly
@@ -663,6 +663,16 @@ class Guild extends AnonymousGuild {
}
}
+ /**
+ * The maximum bitrate available for a stage channel in this guild
+ *
+ * @type {number}
+ * @readonly
+ */
+ get maximumStageBitrate() {
+ return 64_000;
+ }
+
/**
* Fetches a collection of integrations to this guild.
* Resolves with a collection mapping integrations by their ids.
diff --git a/packages/discord.js/src/structures/GuildInvite.js b/packages/discord.js/src/structures/GuildInvite.js
index 2ac2c2cbb29a..179aac0b2523 100644
--- a/packages/discord.js/src/structures/GuildInvite.js
+++ b/packages/discord.js/src/structures/GuildInvite.js
@@ -192,7 +192,7 @@ class GuildInvite extends BaseInvite {
if (!guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe);
return Boolean(
this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageChannels, false) ||
- guild.members.me.permissions.has(PermissionFlagsBits.ManageGuild),
+ guild.members.me.permissions.has(PermissionFlagsBits.ManageGuild),
);
}
diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js
index 0879b5231d71..ce778a215c18 100644
--- a/packages/discord.js/src/structures/Message.js
+++ b/packages/discord.js/src/structures/Message.js
@@ -725,8 +725,8 @@ class Message extends Base {
get editable() {
const precheck = Boolean(
this.author.id === this.client.user.id &&
- (!this.guild || this.channel?.viewable) &&
- this.reference?.type !== MessageReferenceType.Forward,
+ (!this.guild || this.channel?.viewable) &&
+ this.reference?.type !== MessageReferenceType.Forward,
);
// Regardless of permissions thread messages cannot be edited if
@@ -837,19 +837,19 @@ class Message extends Base {
const { channel } = this;
return Boolean(
channel?.type === ChannelType.GuildAnnouncement &&
- !this.flags.has(MessageFlags.Crossposted) &&
- this.reference?.type !== MessageReferenceType.Forward &&
- this.type === MessageType.Default &&
- !this.poll &&
- channel.viewable &&
- channel.permissionsFor(this.client.user)?.has(bitfield, false),
+ !this.flags.has(MessageFlags.Crossposted) &&
+ this.reference?.type !== MessageReferenceType.Forward &&
+ this.type === MessageType.Default &&
+ !this.poll &&
+ channel.viewable &&
+ channel.permissionsFor(this.client.user)?.has(bitfield, false),
);
}
/**
* Edits the content of the message.
*
- * @param {string|MessagePayload|MessageEditOptions} options The options to provide
+ * @param {string|MessageEditOptions|MessagePayload|FileBodyEncodable|JSONEncodable} options The options to provide
* @returns {Promise}
* @example
* // Update the content of a message
diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js
index 3fe3687650d7..8806a761da4b 100644
--- a/packages/discord.js/src/structures/MessagePayload.js
+++ b/packages/discord.js/src/structures/MessagePayload.js
@@ -197,10 +197,13 @@ class MessagePayload {
waveform: file.waveform,
duration_secs: file.duration,
}));
+
+ // Only passable during edits
if (Array.isArray(this.options.attachments)) {
- this.options.attachments.push(...(attachments ?? []));
- } else {
- this.options.attachments = attachments;
+ attachments.push(
+ // Note how we don't check for file body encodable, since we aren't expecting file data here
+ ...this.options.attachments.map(attachment => (isJSONEncodable(attachment) ? attachment.toJSON() : attachment)),
+ );
}
let poll;
@@ -237,7 +240,7 @@ class MessagePayload {
: allowedMentions,
flags,
message_reference,
- attachments: this.options.attachments,
+ attachments,
sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
thread_name: threadName,
applied_tags: appliedTags,
diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js
index fb752ab92df8..875ebc4a7cc1 100644
--- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js
+++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js
@@ -88,7 +88,7 @@ class TextBasedChannel {
* @property {Array<(EmbedBuilder|Embed|APIEmbed)>} [embeds] The embeds for the message
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
* (see {@link https://discord.com/developers/docs/resources/message#allowed-mentions-object here} for more details)
- * @property {Array<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files]
+ * @property {Array<(Attachment|AttachmentPayload|BufferResolvable|FileBodyEncodable|Stream)>} [files]
* The files to send with the message.
* @property {Array<(ActionRowBuilder|MessageTopLevelComponent|APIMessageTopLevelComponent)>} [components]
* Action rows containing interactive components for the message (buttons, select menus) and other
@@ -156,7 +156,7 @@ class TextBasedChannel {
/**
* Sends a message to this channel.
*
- * @param {string|MessagePayload|MessageCreateOptions} options The options to provide
+ * @param {string|MessagePayload|MessageCreateOptions|JSONEncodable|FileBodyEncodable} options The options to provide
* @returns {Promise}
* @example
* // Send a basic message
diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js
index 2dc952239b65..ceafa189ecee 100644
--- a/packages/discord.js/src/util/APITypes.js
+++ b/packages/discord.js/src/util/APITypes.js
@@ -683,3 +683,13 @@
* @external WebhookType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/WebhookType}
*/
+
+/**
+ * @external RESTPatchAPIChannelMessageJSONBody
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/RESTPatchAPIChannelMessageJSONBody}
+ */
+
+/**
+ * @external RESTPostAPIChannelMessageJSONBody
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/RESTPostAPIChannelMessageJSONBody}
+ */
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index 8c7fe0d755bb..6957c552f370 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -5,7 +5,7 @@ import { MessagePort, Worker } from 'node:worker_threads';
import { ApplicationCommandOptionAllowedChannelType, MessageActionRowComponentBuilder } from '@discordjs/builders';
import { Collection, ReadonlyCollection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions, EmojiURLOptions } from '@discordjs/rest';
-import { Awaitable, JSONEncodable } from '@discordjs/util';
+import { Awaitable, FileBodyEncodable, JSONEncodable } from '@discordjs/util';
import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
import {
@@ -266,8 +266,9 @@ export type ActionRowComponentData = MessageActionRowComponentData;
export type ActionRowComponent = MessageActionRowComponent;
-export interface ActionRowData>
- extends BaseComponentData {
+export interface ActionRowData<
+ ComponentType extends ActionRowComponentData | JSONEncodable,
+> extends BaseComponentData {
components: readonly ComponentType[];
}
@@ -603,7 +604,8 @@ export class BaseGuildEmoji extends Emoji {
}
export interface BaseGuildTextChannel
- extends TextBasedChannelFields,
+ extends
+ TextBasedChannelFields,
PinnableChannelFields,
WebhookChannelFields,
BulkDeleteMethod,
@@ -630,7 +632,8 @@ export class BaseGuildTextChannel extends GuildChannel {
}
export interface BaseGuildVoiceChannel
- extends TextBasedChannelFields,
+ extends
+ TextBasedChannelFields,
WebhookChannelFields,
BulkDeleteMethod,
SetRateLimitPerUserMethod,
@@ -1282,10 +1285,7 @@ export class PrimaryEntryPointCommandInteraction<
}
export interface DMChannel
- extends TextBasedChannelFields,
- PinnableChannelFields,
- MessageChannelFields,
- SendMethod {}
+ extends TextBasedChannelFields, PinnableChannelFields, MessageChannelFields, SendMethod {}
export class DMChannel extends BaseChannel {
private constructor(client: Client, data?: RawDMChannelData);
public flags: Readonly;
@@ -1453,6 +1453,7 @@ export class Guild extends AnonymousGuild {
public widgetChannelId: Snowflake | null;
public widgetEnabled: boolean | null;
public get maximumBitrate(): number;
+ public get maximumStageBitrate(): number;
public createTemplate(name: string, description?: string): Promise;
public discoverySplashURL(options?: ImageURLOptions): string | null;
public edit(options: GuildEditOptions): Promise;
@@ -2234,7 +2235,12 @@ export class Message extends Base {
): InteractionCollector[ComponentType]>;
public delete(): Promise>>;
public edit(
- content: MessageEditOptions | MessagePayload | string,
+ content:
+ | FileBodyEncodable
+ | JSONEncodable
+ | MessageEditOptions
+ | MessagePayload
+ | string,
): Promise>>;
public equals(message: Message, rawData: unknown): boolean;
public fetchReference(): Promise>>;
@@ -2259,26 +2265,6 @@ export class Message extends Base {
public inGuild(): this is Message;
}
-export class AttachmentBuilder {
- public constructor(attachment: BufferResolvable | Stream, data?: AttachmentData);
- public attachment: BufferResolvable | Stream;
- public description: string | null;
- public name: string | null;
- public title: string | null;
- public waveform: string | null;
- public duration: number | null;
- public get spoiler(): boolean;
- public setDescription(description: string): this;
- public setFile(attachment: BufferResolvable | Stream, name?: string): this;
- public setName(name: string): this;
- public setTitle(title: string): this;
- public setWaveform(waveform: string): this;
- public setDuration(duration: number): this;
- public setSpoiler(spoiler?: boolean): this;
- public toJSON(): unknown;
- public static from(other: JSONEncodable): AttachmentBuilder;
-}
-
export class Attachment {
private constructor(data: APIAttachment);
private readonly attachment: BufferResolvable | Stream;
@@ -2557,14 +2543,13 @@ export interface TextInputModalData extends BaseModalData
- extends BaseModalData<
- | ComponentType.ChannelSelect
- | ComponentType.MentionableSelect
- | ComponentType.RoleSelect
- | ComponentType.StringSelect
- | ComponentType.UserSelect
- > {
+export interface SelectMenuModalData extends BaseModalData<
+ | ComponentType.ChannelSelect
+ | ComponentType.MentionableSelect
+ | ComponentType.RoleSelect
+ | ComponentType.StringSelect
+ | ComponentType.UserSelect
+> {
channels?: ReadonlyCollection<
Snowflake,
CacheTypeReducer
@@ -2659,8 +2644,9 @@ export class ModalComponentResolver {
public getUploadedFiles(customId: string, required?: boolean): ReadonlyCollection | null;
}
-export interface ModalMessageModalSubmitInteraction
- extends ModalSubmitInteraction {
+export interface ModalMessageModalSubmitInteraction<
+ Cached extends CacheType = CacheType,
+> extends ModalSubmitInteraction {
channelId: Snowflake;
inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>;
inGuild(): this is ModalMessageModalSubmitInteraction<'cached' | 'raw'>;
@@ -3513,7 +3499,8 @@ export interface PrivateThreadChannel extends ThreadChannel {
}
export interface ThreadChannel
- extends TextBasedChannelFields,
+ extends
+ TextBasedChannelFields,
PinnableChannelFields,
BulkDeleteMethod,
SetRateLimitPerUserMethod,
@@ -4278,7 +4265,12 @@ export class ChannelManager extends CachedManager, iterable: Iterable);
public createMessage(
channel: Exclude,
- options: MessageCreateOptions | MessagePayload | string,
+ options:
+ | FileBodyEncodable
+ | JSONEncodable
+ | MessageCreateOptions
+ | MessagePayload
+ | string,
): Promise>;
public fetch(id: Snowflake, options?: FetchChannelOptions): Promise;
}
@@ -4630,7 +4622,12 @@ export abstract class MessageManager extends
public delete(message: MessageResolvable): Promise;
public edit(
message: MessageResolvable,
- options: MessageEditOptions | MessagePayload | string,
+ options:
+ | FileBodyEncodable
+ | JSONEncodable
+ | MessageEditOptions
+ | MessagePayload
+ | string,
): Promise>;
public fetch(options: FetchMessageOptions | MessageResolvable): Promise>;
public fetch(options?: FetchMessagesOptions): Promise>>;
@@ -4807,7 +4804,14 @@ export class VoiceStateManager extends CachedManager = abstract new (...args: any[]) => Entity;
export interface SendMethod {
- send(options: MessageCreateOptions | MessagePayload | string): Promise>;
+ send(
+ options:
+ | FileBodyEncodable
+ | JSONEncodable
+ | MessageCreateOptions
+ | MessagePayload
+ | string,
+ ): Promise>;
}
export interface PinnableChannelFields {
@@ -5060,15 +5064,17 @@ export interface ApplicationCommandAutocompleteStringOptionData extends BaseAppl
type: ApplicationCommandOptionType.String;
}
-export interface ApplicationCommandChoicesData
- extends BaseApplicationCommandOptionsData {
+export interface ApplicationCommandChoicesData<
+ Type extends number | string = number | string,
+> extends BaseApplicationCommandOptionsData {
autocomplete?: false;
choices?: readonly ApplicationCommandOptionChoiceData[];
type: CommandOptionChoiceResolvableType;
}
-export interface ApplicationCommandChoicesOption
- extends BaseApplicationCommandOptionsData {
+export interface ApplicationCommandChoicesOption<
+ Type extends number | string = number | string,
+> extends BaseApplicationCommandOptionsData {
autocomplete?: false;
choices?: readonly ApplicationCommandOptionChoiceData[];
type: CommandOptionChoiceResolvableType;
@@ -5251,13 +5257,15 @@ export interface AutoModerationTriggerMetadata {
regexPatterns: readonly string[];
}
-export interface AwaitMessageComponentOptions
- extends CollectorOptions<[Interaction, Collection]> {
+export interface AwaitMessageComponentOptions extends CollectorOptions<
+ [Interaction, Collection]
+> {
componentType?: ComponentType;
}
-export interface AwaitModalSubmitOptions
- extends CollectorOptions<[ModalSubmitInteraction, Collection]> {
+export interface AwaitModalSubmitOptions extends CollectorOptions<
+ [ModalSubmitInteraction, Collection]
+> {
time: number;
}
@@ -5630,8 +5638,9 @@ export interface BaseInteractionResolvedData;
}
-export interface CommandInteractionResolvedData
- extends BaseInteractionResolvedData {
+export interface CommandInteractionResolvedData<
+ Cached extends CacheType = CacheType,
+> extends BaseInteractionResolvedData {
messages?: ReadonlyCollection>;
}
@@ -6270,7 +6279,7 @@ export interface GuildEmojiEditOptions {
export interface GuildStickerCreateOptions {
description?: string | null;
- file: AttachmentPayload | BufferResolvable | JSONEncodable | Stream;
+ file: AttachmentPayload | BufferResolvable | Stream;
name: string;
reason?: string;
tags: string;
@@ -6617,8 +6626,9 @@ export type CollectedMessageInteraction =
ModalSubmitInteraction
>;
-export interface MessageComponentCollectorOptions
- extends AwaitMessageComponentOptions {
+export interface MessageComponentCollectorOptions<
+ Interaction extends CollectedMessageInteraction,
+> extends AwaitMessageComponentOptions {
max?: number;
maxComponents?: number;
maxUsers?: number;
@@ -6657,25 +6667,24 @@ export interface MessageMentionOptions {
export type MessageMentionTypes = 'everyone' | 'roles' | 'users';
-export interface MessageSnapshot
- extends Partialize<
- Message,
- null,
- Exclude<
- keyof Message,
- | 'attachments'
- | 'client'
- | 'components'
- | 'content'
- | 'createdTimestamp'
- | 'editedTimestamp'
- | 'embeds'
- | 'flags'
- | 'mentions'
- | 'stickers'
- | 'type'
- >
- > {}
+export interface MessageSnapshot extends Partialize<
+ Message,
+ null,
+ Exclude<
+ keyof Message,
+ | 'attachments'
+ | 'client'
+ | 'components'
+ | 'content'
+ | 'createdTimestamp'
+ | 'editedTimestamp'
+ | 'embeds'
+ | 'flags'
+ | 'mentions'
+ | 'stickers'
+ | 'type'
+ >
+> {}
export interface BaseMessageOptions {
allowedMentions?: MessageMentionOptions;
@@ -6688,14 +6697,7 @@ export interface BaseMessageOptions {
)[];
content?: string;
embeds?: readonly (APIEmbed | JSONEncodable)[];
- files?: readonly (
- | Attachment
- | AttachmentBuilder
- | AttachmentPayload
- | BufferResolvable
- | JSONEncodable
- | Stream
- )[];
+ files?: readonly (Attachment | AttachmentPayload | BufferResolvable | FileBodyEncodable | Stream)[];
}
export interface MessageOptionsPoll {
@@ -6723,11 +6725,7 @@ export interface MessageOptionsStickers {
}
export interface BaseMessageCreateOptions
- extends BaseMessageOptions,
- MessageOptionsPoll,
- MessageOptionsFlags,
- MessageOptionsTTS,
- MessageOptionsStickers {
+ extends BaseMessageOptions, MessageOptionsPoll, MessageOptionsFlags, MessageOptionsTTS, MessageOptionsStickers {
enforceNonce?: boolean;
nonce?: number | string;
}
@@ -6737,16 +6735,10 @@ export interface MessageCreateOptions extends BaseMessageCreateOptions {
}
export interface GuildForumThreadMessageCreateOptions
- extends BaseMessageOptions,
- MessageOptionsFlags,
- MessageOptionsStickers {}
-
-export interface MessageEditAttachmentData {
- id: Snowflake;
-}
+ extends BaseMessageOptions, MessageOptionsFlags, MessageOptionsStickers {}
export interface MessageEditOptions extends Omit {
- attachments?: readonly (Attachment | MessageEditAttachmentData)[];
+ attachments?: readonly (Attachment | JSONEncodable)[];
content?: string | null;
flags?:
| BitFieldResolvable<
@@ -6942,18 +6934,20 @@ export interface PartialDMChannel extends Partialize {}
-export interface PartialMessage
- extends Partialize, 'pinned' | 'system' | 'tts' | 'type', 'author' | 'cleanContent' | 'content'> {}
+export interface PartialMessage extends Partialize<
+ Message,
+ 'pinned' | 'system' | 'tts' | 'type',
+ 'author' | 'cleanContent' | 'content'
+> {}
export interface PartialMessageReaction extends Partialize {}
-export interface PartialPoll
- extends Partialize<
- Poll,
- 'allowMultiselect' | 'expiresTimestamp' | 'layoutType',
- null,
- 'answers' | 'message' | 'question'
- > {
+export interface PartialPoll extends Partialize<
+ Poll,
+ 'allowMultiselect' | 'expiresTimestamp' | 'layoutType',
+ null,
+ 'answers' | 'message' | 'question'
+> {
// eslint-disable-next-line no-restricted-syntax
answers: Collection;
message: PartialMessage;
@@ -6964,8 +6958,11 @@ export interface PartialPollAnswer extends Partialize {}
+export interface PartialGuildScheduledEvent extends Partialize<
+ GuildScheduledEvent,
+ 'userCount',
+ 'entityType' | 'name' | 'privacyLevel' | 'status'
+> {}
export interface PartialThreadMember extends Partialize {}
@@ -7260,10 +7257,7 @@ export interface WebhookFetchMessageOptions {
}
export interface WebhookMessageCreateOptions
- extends BaseMessageOptions,
- MessageOptionsPoll,
- MessageOptionsFlags,
- MessageOptionsTTS {
+ extends BaseMessageOptions, MessageOptionsPoll, MessageOptionsFlags, MessageOptionsTTS {
appliedTags?: readonly Snowflake[];
avatarURL?: string;
threadId?: Snowflake;
diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts
index 7afce5f9a2ba..7969242ea084 100644
--- a/packages/discord.js/typings/index.test-d.ts
+++ b/packages/discord.js/typings/index.test-d.ts
@@ -203,7 +203,6 @@ import type {
} from './index.js';
import {
ActionRowBuilder,
- AttachmentBuilder,
ChannelSelectMenuBuilder,
Client,
Collection,
@@ -230,6 +229,7 @@ import {
UserSelectMenuComponent,
UserSelectMenuInteraction,
Webhook,
+ MessageBuilder,
} from './index.js';
// Test type transformation:
@@ -453,15 +453,9 @@ client.on('messageCreate', async message => {
assertIsMessage(client.channels.createMessage(channel, {}));
assertIsMessage(client.channels.createMessage(channel, { embeds: [] }));
- const attachment = new AttachmentBuilder('file.png');
const embed = new EmbedBuilder();
- assertIsMessage(channel.send({ files: [attachment] }));
assertIsMessage(channel.send({ embeds: [embed] }));
- assertIsMessage(channel.send({ embeds: [embed], files: [attachment] }));
-
- assertIsMessage(client.channels.createMessage(channel, { files: [attachment] }));
assertIsMessage(client.channels.createMessage(channel, { embeds: [embed] }));
- assertIsMessage(client.channels.createMessage(channel, { embeds: [embed], files: [attachment] }));
if (message.inGuild()) {
expectAssignable>(message);
@@ -3034,14 +3028,11 @@ await guildScheduledEventManager.edit(snowflake, { recurrenceRule: null });
});
}
-await textChannel.send({
- files: [
- new AttachmentBuilder('https://example.com/voice-message.ogg')
- .setDuration(2)
- .setWaveform('AFUqPDw3Eg2hh4+gopOYj4xthU4='),
- ],
- flags: MessageFlags.IsVoiceMessage,
-});
+await textChannel.send(
+ new MessageBuilder()
+ .setContent(':)')
+ .addAttachments(attachment => attachment.setId(1).setFileData(':)').setFilename('smiley.txt')),
+);
await textChannel.send({
files: [
diff --git a/packages/docgen/package.json b/packages/docgen/package.json
index fd5e79655a69..8b11b68d16dc 100644
--- a/packages/docgen/package.json
+++ b/packages/docgen/package.json
@@ -66,18 +66,17 @@
"typedoc": "^0.25.13"
},
"devDependencies": {
- "@favware/cliff-jumper": "^4.1.0",
"@types/jsdoc-to-markdown": "^7.0.6",
- "@types/node": "^22.18.13",
+ "@types/node": "^22.19.1",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3"
},
"engines": {
diff --git a/packages/formatters/.cliff-jumperrc.json b/packages/formatters/.cliff-jumperrc.json
index 7476e855d236..3f5603b0cbfb 100644
--- a/packages/formatters/.cliff-jumperrc.json
+++ b/packages/formatters/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "formatters",
"org": "discordjs",
"packagePath": "packages/formatters",
diff --git a/packages/formatters/package.json b/packages/formatters/package.json
index 60a7889d1bde..87d654852265 100644
--- a/packages/formatters/package.json
+++ b/packages/formatters/package.json
@@ -55,25 +55,25 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
- "discord-api-types": "^0.38.31"
+ "discord-api-types": "^0.38.36"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/next/.cliff-jumperrc.json b/packages/next/.cliff-jumperrc.json
index 7ce9fb903fe1..c90aba9d167f 100644
--- a/packages/next/.cliff-jumperrc.json
+++ b/packages/next/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "next",
"org": "discordjs",
"packagePath": "packages/next",
diff --git a/packages/next/package.json b/packages/next/package.json
index a802264e0223..201a1809c04a 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -72,25 +72,25 @@
"@discordjs/rest": "workspace:^",
"@discordjs/util": "workspace:^",
"@discordjs/ws": "workspace:^",
- "discord-api-types": "^0.38.31"
+ "discord-api-types": "^0.38.36"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/proxy-container/Dockerfile b/packages/proxy-container/Dockerfile
index effa735b9348..c8974a6ee3d0 100644
--- a/packages/proxy-container/Dockerfile
+++ b/packages/proxy-container/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:22-alpine AS base
+FROM node:24-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
@@ -12,14 +12,14 @@ RUN corepack install
FROM base AS builder
-RUN pnpm install --frozen-lockfile --ignore-scripts
-RUN pnpm exec turbo run build --filter='@discordjs/proxy-container...'
+RUN pnpm --filter='@discordjs/proxy-container...' install --frozen-lockfile --ignore-scripts
+RUN pnpm exec turbo run build --filter='@discordjs/proxy-container...' --output-logs=full
FROM builder AS pruned
RUN pnpm --filter='@discordjs/proxy-container' --prod deploy --legacy pruned
-FROM node:22-alpine AS proxy
+FROM node:24-alpine AS proxy
WORKDIR /usr/proxy-container
diff --git a/packages/proxy-container/package.json b/packages/proxy-container/package.json
index 27ed352ac585..6b37a435fa59 100644
--- a/packages/proxy-container/package.json
+++ b/packages/proxy-container/package.json
@@ -50,16 +50,16 @@
"tslib": "^2.8.1"
},
"devDependencies": {
- "@types/node": "^22.18.13",
+ "@types/node": "^24.10.1",
"cross-env": "^10.1.0",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "terser": "^5.44.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "terser": "^5.44.1",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3"
},
"engines": {
diff --git a/packages/proxy/.cliff-jumperrc.json b/packages/proxy/.cliff-jumperrc.json
index bd4ebbb5c745..2e4fd413a1d7 100644
--- a/packages/proxy/.cliff-jumperrc.json
+++ b/packages/proxy/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "proxy",
"org": "discordjs",
"packagePath": "packages/proxy",
diff --git a/packages/proxy/__tests__/proxyRequests.test.ts b/packages/proxy/__tests__/proxyRequests.test.ts
index 23bc8f94eea6..9fb4ac179a46 100644
--- a/packages/proxy/__tests__/proxyRequests.test.ts
+++ b/packages/proxy/__tests__/proxyRequests.test.ts
@@ -24,6 +24,7 @@ beforeEach(() => {
setGlobalDispatcher(mockAgent); // enabled the mock client to intercept requests
mockPool = mockAgent.get('https://discord.com');
+ api.setAgent(mockAgent);
});
afterEach(async () => {
diff --git a/packages/proxy/package.json b/packages/proxy/package.json
index 827df26b3937..d7965897d9c8 100644
--- a/packages/proxy/package.json
+++ b/packages/proxy/package.json
@@ -73,22 +73,22 @@
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
"@types/supertest": "^6.0.3",
- "@vitest/coverage-v8": "^3.2.4",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
+ "prettier": "^3.7.4",
"supertest": "^7.1.4",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/rest/.cliff-jumperrc.json b/packages/rest/.cliff-jumperrc.json
index 61866a1398d0..46fc1fecd14e 100644
--- a/packages/rest/.cliff-jumperrc.json
+++ b/packages/rest/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "rest",
"org": "discordjs",
"packagePath": "packages/rest",
diff --git a/packages/rest/__tests__/BurstHandler.test.ts b/packages/rest/__tests__/BurstHandler.test.ts
index b56523e60452..cdd7212c4fae 100644
--- a/packages/rest/__tests__/BurstHandler.test.ts
+++ b/packages/rest/__tests__/BurstHandler.test.ts
@@ -1,6 +1,5 @@
/* eslint-disable id-length */
/* eslint-disable promise/prefer-await-to-then */
-// @ts-nocheck
import { performance } from 'node:perf_hooks';
import { MockAgent, setGlobalDispatcher } from 'undici';
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
@@ -137,7 +136,7 @@ test('Handle unexpected 429', async () => {
});
expect(await unexpectedLimit).toStrictEqual({ test: true });
- expect(performance.now()).toBeGreaterThanOrEqual(previous + 1_000);
+ expect(firstResolvedTime!).toBeGreaterThanOrEqual(previous + 1_000);
});
test('server responding too slow', async () => {
diff --git a/packages/rest/__tests__/REST.test.ts b/packages/rest/__tests__/REST.test.ts
index 2f451699d4b7..54aa3016a01b 100644
--- a/packages/rest/__tests__/REST.test.ts
+++ b/packages/rest/__tests__/REST.test.ts
@@ -1,17 +1,15 @@
-import { Buffer, File as NativeFile } from 'node:buffer';
+import { Buffer, File } from 'node:buffer';
import { URLSearchParams } from 'node:url';
import { DiscordSnowflake } from '@sapphire/snowflake';
import type { Snowflake } from 'discord-api-types/v10';
import { Routes } from 'discord-api-types/v10';
import { type FormData, fetch } from 'undici';
-import { File as UndiciFile, MockAgent, setGlobalDispatcher } from 'undici';
+import { MockAgent, setGlobalDispatcher } from 'undici';
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor.js';
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
import { REST } from '../src/index.js';
import { genPath } from './util.js';
-const File = NativeFile ?? UndiciFile;
-
const newSnowflake: Snowflake = DiscordSnowflake.generate().toString();
const api = new REST().setToken('A-Very-Fake-Token');
@@ -37,6 +35,8 @@ beforeEach(() => {
setGlobalDispatcher(mockAgent); // enabled the mock client to intercept requests
mockPool = mockAgent.get('https://discord.com');
+ api.setAgent(mockAgent);
+ fetchApi.setAgent(mockAgent);
});
afterEach(async () => {
diff --git a/packages/rest/__tests__/RequestHandler.test.ts b/packages/rest/__tests__/RequestHandler.test.ts
index 6471a1addbc6..16f02769ccc9 100644
--- a/packages/rest/__tests__/RequestHandler.test.ts
+++ b/packages/rest/__tests__/RequestHandler.test.ts
@@ -481,6 +481,8 @@ test('perm server outage', async () => {
test('server responding too slow', async () => {
const api2 = new REST({ timeout: 1 }).setToken('A-Very-Really-Real-Token');
+ api2.setAgent(mockAgent);
+
mockPool
.intercept({
path: genPath('/slow'),
diff --git a/packages/rest/__tests__/RequestManager.test.ts b/packages/rest/__tests__/RequestManager.test.ts
index e0a6b6ced3a8..c158663dedd8 100644
--- a/packages/rest/__tests__/RequestManager.test.ts
+++ b/packages/rest/__tests__/RequestManager.test.ts
@@ -15,6 +15,7 @@ beforeEach(() => {
setGlobalDispatcher(mockAgent);
mockPool = mockAgent.get('https://discord.com');
+ api.setAgent(mockAgent);
});
afterEach(async () => {
diff --git a/packages/rest/__tests__/UndiciRequest.test.ts b/packages/rest/__tests__/UndiciRequest.test.ts
index 95e0f4b8d139..814458c72a27 100644
--- a/packages/rest/__tests__/UndiciRequest.test.ts
+++ b/packages/rest/__tests__/UndiciRequest.test.ts
@@ -28,6 +28,7 @@ beforeEach(() => {
setGlobalDispatcher(mockAgent); // enabled the mock client to intercept requests
mockPool = mockAgent.get('https://discord.com');
+ api.setAgent(mockAgent);
});
afterEach(async () => {
@@ -81,7 +82,7 @@ test('resolveBody', async () => {
const fd = new globalThis.FormData();
fd.append('key', 'value');
- const resolved = await resolveBody(fd);
+ const resolved = await resolveBody(fd as UndiciFormData);
expect(resolved).toBeInstanceOf(UndiciFormData);
expect([...(resolved as UndiciFormData).entries()]).toStrictEqual([['key', 'value']]);
diff --git a/packages/rest/package.json b/packages/rest/package.json
index ce655b97b059..91157ceac5da 100644
--- a/packages/rest/package.json
+++ b/packages/rest/package.json
@@ -88,7 +88,7 @@
"@sapphire/async-queue": "^1.5.5",
"@sapphire/snowflake": "^3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.7",
- "discord-api-types": "^0.38.31",
+ "discord-api-types": "^0.38.36",
"magic-bytes.js": "^1.12.1",
"tslib": "^2.8.1",
"undici": "7.16.0",
@@ -97,20 +97,20 @@
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/rest/src/lib/utils/types.ts b/packages/rest/src/lib/utils/types.ts
index fa29be36277a..d8d62365b65c 100644
--- a/packages/rest/src/lib/utils/types.ts
+++ b/packages/rest/src/lib/utils/types.ts
@@ -260,8 +260,10 @@ export interface APIRequest {
route: string;
}
-export interface ResponseLike
- extends Pick {
+export interface ResponseLike extends Pick<
+ Response,
+ 'arrayBuffer' | 'bodyUsed' | 'headers' | 'json' | 'ok' | 'status' | 'statusText' | 'text'
+> {
body: Readable | ReadableStream | null;
}
diff --git a/packages/rest/src/strategies/undiciRequest.ts b/packages/rest/src/strategies/undiciRequest.ts
index 5eee90c5e279..bafd0502897d 100644
--- a/packages/rest/src/strategies/undiciRequest.ts
+++ b/packages/rest/src/strategies/undiciRequest.ts
@@ -1,12 +1,14 @@
import { STATUS_CODES } from 'node:http';
import { URLSearchParams } from 'node:url';
import { types } from 'node:util';
-import { type RequestInit, request, Headers, FormData as UndiciFormData } from 'undici';
+import { type RequestInit, request, Headers, FormData as UndiciFormData, Agent } from 'undici';
import type { HeaderRecord } from 'undici/types/header.js';
import type { ResponseLike } from '../shared.js';
export type RequestOptions = Exclude[1], undefined>;
+let localAgent: Agent | null = null;
+
export async function makeRequest(url: string, init: RequestInit): Promise {
// The cast is necessary because `headers` and `method` are narrower types in `undici.request`
// our request path guarantees they are of acceptable type for `undici.request`
@@ -14,6 +16,15 @@ export async function makeRequest(url: string, init: RequestInit): Promise=22.12.0"
diff --git a/packages/structures/.cliff-jumperrc.json b/packages/structures/.cliff-jumperrc.json
index 62fb98f7c930..3855cfce320d 100644
--- a/packages/structures/.cliff-jumperrc.json
+++ b/packages/structures/.cliff-jumperrc.json
@@ -1,4 +1,5 @@
{
+ "$schema": "./node_modules/@favware/cliff-jumper/assets/cliff-jumper.schema.json",
"name": "structures",
"org": "discordjs",
"packagePath": "packages/structures",
diff --git a/packages/structures/__tests__/channels.test.ts b/packages/structures/__tests__/channels.test.ts
index 647f796ea2c7..f81474c6520c 100644
--- a/packages/structures/__tests__/channels.test.ts
+++ b/packages/structures/__tests__/channels.test.ts
@@ -38,7 +38,7 @@ import {
TextChannel,
ThreadMetadata,
VoiceChannel,
-} from '../src/index.js';
+} from '../src/channels/index.js';
import { kData } from '../src/utils/symbols.js';
describe('text channel', () => {
diff --git a/packages/structures/__tests__/invite.test.ts b/packages/structures/__tests__/invite.test.ts
index ec8407c05ca3..9cf1307c71f9 100644
--- a/packages/structures/__tests__/invite.test.ts
+++ b/packages/structures/__tests__/invite.test.ts
@@ -1,7 +1,8 @@
import type { APIExtendedInvite, APIInvite } from 'discord-api-types/v10';
import { InviteTargetType, InviteType } from 'discord-api-types/v10';
import { describe, expect, test } from 'vitest';
-import { Invite } from '../src/index.js';
+import { Invite } from '../src/invites/Invite.js';
+import { dateToDiscordISOTimestamp } from '../src/utils/optimization.js';
import { kPatch } from '../src/utils/symbols.js';
describe('Invite', () => {
@@ -22,7 +23,7 @@ describe('Invite', () => {
const dataExtended: Omit = {
...data,
- created_at: '2020-10-10T13:50:17.209Z',
+ created_at: '2020-10-10T13:50:17.209000+00:00',
max_age: 12,
max_uses: 34,
temporary: false,
@@ -54,7 +55,7 @@ describe('Invite', () => {
const instance = new Invite(dataExtended);
expect(instance.type).toBe(data.type);
expect(instance.code).toBe(dataExtended.code);
- expect(instance.createdAt?.toISOString()).toBe(dataExtended.created_at);
+ expect(dateToDiscordISOTimestamp(instance.createdAt!)).toBe(dataExtended.created_at);
expect(instance.createdTimestamp).toBe(Date.parse(dataExtended.created_at));
expect(instance.maxAge).toBe(dataExtended.max_age);
expect(instance.maxUses).toBe(dataExtended.max_uses);
@@ -63,10 +64,10 @@ describe('Invite', () => {
expect(instance.targetType).toBe(dataExtended.target_type);
expect(instance.temporary).toBe(dataExtended.temporary);
expect(instance.uses).toBe(dataExtended.uses);
- expect(instance.expiresTimestamp).toStrictEqual(Date.parse('2020-10-10T13:50:29.209Z'));
- expect(instance.expiresAt).toStrictEqual(new Date('2020-10-10T13:50:29.209Z'));
+ expect(instance.expiresTimestamp).toStrictEqual(Date.parse('2020-10-10T13:50:29.209000+00:00'));
+ expect(instance.expiresAt).toStrictEqual(new Date('2020-10-10T13:50:29.209000+00:00'));
expect(instance.url).toBe('https://discord.gg/123');
- expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
+ expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209000+00:00' });
});
test('Invite with omitted properties', () => {
@@ -79,8 +80,8 @@ describe('Invite', () => {
});
test('Invite with expiration', () => {
- const instance = new Invite({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
- expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209Z' });
+ const instance = new Invite({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209000+00:00' });
+ expect(instance.toJSON()).toEqual({ ...dataExtended, expires_at: '2020-10-10T13:50:29.209000+00:00' });
});
test('Patching Invite works in place', () => {
diff --git a/packages/structures/__tests__/message.test.ts b/packages/structures/__tests__/message.test.ts
new file mode 100644
index 000000000000..5c739f7bd785
--- /dev/null
+++ b/packages/structures/__tests__/message.test.ts
@@ -0,0 +1,478 @@
+import { DiscordSnowflake } from '@sapphire/snowflake';
+import type {
+ APIActionRowComponent,
+ APIButtonComponent,
+ APIChannelSelectComponent,
+ APIContainerComponent,
+ APIFileComponent,
+ APIMediaGalleryComponent,
+ APIMentionableSelectComponent,
+ APIMessage,
+ APIRoleSelectComponent,
+ APISectionComponent,
+ APISeparatorComponent,
+ APIStringSelectComponent,
+ APIUser,
+ APIUserSelectComponent,
+} from 'discord-api-types/v10';
+import {
+ MessageReferenceType,
+ MessageType,
+ MessageFlags,
+ ComponentType,
+ ButtonStyle,
+ SeparatorSpacingSize,
+ ChannelType,
+ SelectMenuDefaultValueType,
+} from 'discord-api-types/v10';
+import { describe, expect, test } from 'vitest';
+import { Attachment } from '../src/messages/Attachment.js';
+import { Message } from '../src/messages/Message.js';
+import { ContainerComponent } from '../src/messages/components/ContainerComponent.js';
+import { Embed } from '../src/messages/embeds/Embed.js';
+import { User } from '../src/users/User.js';
+import { dateToDiscordISOTimestamp } from '../src/utils/optimization.js';
+
+const user: APIUser = {
+ username: 'user',
+ avatar: 'abcd123',
+ global_name: 'User',
+ discriminator: '0',
+ id: '3',
+};
+
+describe('message with embeds and attachments', () => {
+ const timestamp = '2025-10-09T17:48:20.192000+00:00';
+ const data: APIMessage = {
+ id: DiscordSnowflake.generate({ timestamp: Date.parse(timestamp) }).toString(),
+ type: MessageType.Default,
+ position: 10,
+ channel_id: '2',
+ author: user,
+ attachments: [
+ {
+ filename: 'file.txt',
+ description: 'describe attachment',
+ id: '0',
+ proxy_url: 'https://media.example.com/attachment/123.txt',
+ size: 5,
+ url: 'https://example.com/attachment/123.txt',
+ },
+ ],
+ content: 'something <&5> <&6>',
+ edited_timestamp: '2025-10-09T17:50:20.292000+00:00',
+ embeds: [
+ {
+ author: {
+ name: 'embed author',
+ icon_url: 'https://discord.js.org/static/logo.svg',
+ },
+ color: 255,
+ description: 'describe me',
+ fields: [
+ {
+ name: 'field name',
+ value: 'field value',
+ inline: false,
+ },
+ ],
+ footer: {
+ text: 'footer',
+ },
+ image: {
+ url: 'https://discord.js.org/static/logo.svg',
+ },
+ thumbnail: {
+ url: 'https://discord.js.org/static/logo.svg',
+ },
+ title: 'Title',
+ timestamp: '2025-10-19T21:39:40.193000+00:00',
+ },
+ ],
+ mention_everyone: false,
+ mention_roles: ['5', '6'],
+ mentions: [user],
+ pinned: false,
+ timestamp,
+ tts: false,
+ flags: MessageFlags.SuppressNotifications,
+ };
+
+ test('Message has all properties', () => {
+ const instance = new Message(data);
+ expect(instance.id).toBe(data.id);
+ expect(instance.channelId).toBe(data.channel_id);
+ expect(instance.position).toBe(data.position);
+ expect(instance.content).toBe(data.content);
+ expect(instance.createdTimestamp).toBe(Date.parse(data.timestamp));
+ expect(dateToDiscordISOTimestamp(instance.createdAt!)).toBe(data.timestamp);
+ expect(instance.flags?.toJSON()).toBe(data.flags);
+ expect(instance.editedTimestamp).toBe(Date.parse(data.edited_timestamp!));
+ expect(dateToDiscordISOTimestamp(instance.editedAt!)).toBe(data.edited_timestamp);
+ expect(instance.nonce).toBe(data.nonce);
+ expect(instance.pinned).toBe(data.pinned);
+ expect(instance.tts).toBe(data.tts);
+ expect(instance.webhookId).toBe(data.webhook_id);
+ expect(instance.type).toBe(MessageType.Default);
+ expect(instance.toJSON()).toEqual(data);
+ });
+
+ test('Attachment sub-structure', () => {
+ const instances = data.attachments?.map((attachment) => new Attachment(attachment));
+ expect(instances?.map((attachment) => attachment.toJSON())).toEqual(data.attachments);
+ expect(instances?.[0]?.description).toBe(data.attachments?.[0]?.description);
+ expect(instances?.[0]?.filename).toBe(data.attachments?.[0]?.filename);
+ expect(instances?.[0]?.id).toBe(data.attachments?.[0]?.id);
+ expect(instances?.[0]?.size).toBe(data.attachments?.[0]?.size);
+ expect(instances?.[0]?.url).toBe(data.attachments?.[0]?.url);
+ expect(instances?.[0]?.proxyURL).toBe(data.attachments?.[0]?.proxy_url);
+ });
+
+ test('Embed sub-structure', () => {
+ const instances = data.embeds?.map((embed) => new Embed(embed));
+ expect(instances?.map((embed) => embed.toJSON())).toEqual(data.embeds);
+ expect(instances?.[0]?.description).toBe(data.embeds?.[0]?.description);
+ expect(instances?.[0]?.color).toBe(data.embeds?.[0]?.color);
+ expect(instances?.[0]?.timestamp).toBe(Date.parse(data.embeds![0]!.timestamp!));
+ expect(instances?.[0]?.title).toBe(data.embeds?.[0]?.title);
+ expect(instances?.[0]?.url).toBe(data.embeds?.[0]?.url);
+ expect(instances?.[0]?.type).toBe(data.embeds?.[0]?.type);
+ });
+
+ test('User sub-structure', () => {
+ const instance = new User(data.author);
+ const instances = data.mentions.map((user) => new User(user));
+ expect(instance.toJSON()).toEqual(data.author);
+ expect(instances.map((user) => user.toJSON())).toEqual(data.mentions);
+ expect(instance.avatar).toBe(data.author.avatar);
+ expect(instance.discriminator).toBe(data.author.discriminator);
+ expect(instance.displayName).toBe(data.author.global_name);
+ expect(instance.globalName).toBe(data.author.global_name);
+ expect(instance.id).toBe(data.author.id);
+ expect(instance.username).toBe(data.author.username);
+ });
+});
+
+describe('message with components', () => {
+ const timestamp = '2025-10-10T15:48:20.192000+00:00';
+ const buttonRow: APIActionRowComponent = {
+ type: ComponentType.ActionRow,
+ id: 5,
+ components: [
+ {
+ type: ComponentType.Button,
+ style: ButtonStyle.Danger,
+ custom_id: 'danger',
+ disabled: false,
+ emoji: {
+ animated: false,
+ id: '12345',
+ name: 'emoji',
+ },
+ id: 6,
+ label: 'Danger button',
+ },
+ {
+ type: ComponentType.Button,
+ style: ButtonStyle.Link,
+ url: 'https://discord.js.org/',
+ disabled: false,
+ id: 7,
+ label: 'DJS',
+ },
+ {
+ type: ComponentType.Button,
+ style: ButtonStyle.Premium,
+ sku_id: '9876',
+ disabled: false,
+ id: 8,
+ },
+ ],
+ };
+ const file: APIFileComponent = {
+ type: ComponentType.File,
+ file: {
+ url: 'attachment://file.txt',
+ attachment_id: '0',
+ content_type: 'text/plain',
+ flags: 0,
+ },
+ id: 9,
+ spoiler: true,
+ };
+ const mediaGallery: APIMediaGalleryComponent = {
+ type: ComponentType.MediaGallery,
+ items: [
+ {
+ media: {
+ url: 'https://discord.js.org/static/logo.svg',
+ content_type: 'image/svg+xml',
+ height: 50,
+ width: 50,
+ },
+ description: 'Logo',
+ spoiler: false,
+ },
+ ],
+ id: 10,
+ };
+ const section: APISectionComponent = {
+ type: ComponentType.Section,
+ accessory: {
+ type: ComponentType.Thumbnail,
+ media: {
+ url: 'https://discord.js.org/static/logo.svg',
+ },
+ description: 'Logo thumbnail',
+ id: 13,
+ spoiler: false,
+ },
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: 'Text',
+ id: 14,
+ },
+ ],
+ id: 12,
+ };
+ const separator: APISeparatorComponent = {
+ type: ComponentType.Separator,
+ divider: true,
+ id: 15,
+ spacing: SeparatorSpacingSize.Large,
+ };
+ const channelRow: APIActionRowComponent = {
+ type: ComponentType.ActionRow,
+ id: 16,
+ components: [
+ {
+ type: ComponentType.ChannelSelect,
+ custom_id: 'channel',
+ channel_types: [ChannelType.GuildCategory, ChannelType.GuildText],
+ default_values: [
+ {
+ id: '123456789012345678',
+ type: SelectMenuDefaultValueType.Channel,
+ },
+ {
+ id: '123456789012345679',
+ type: SelectMenuDefaultValueType.Channel,
+ },
+ ],
+ disabled: false,
+ id: 17,
+ max_values: 2,
+ min_values: 0,
+ placeholder: '(none)',
+ required: false,
+ },
+ ],
+ };
+ const mentionRow: APIActionRowComponent = {
+ type: ComponentType.ActionRow,
+ id: 18,
+ components: [
+ {
+ type: ComponentType.MentionableSelect,
+ custom_id: 'mention',
+ default_values: [
+ {
+ id: '123456789012345678',
+ type: SelectMenuDefaultValueType.User,
+ },
+ {
+ id: '123456789012345679',
+ type: SelectMenuDefaultValueType.Role,
+ },
+ ],
+ disabled: false,
+ id: 19,
+ max_values: 2,
+ min_values: 0,
+ placeholder: '(none)',
+ required: false,
+ },
+ ],
+ };
+ const roleRow: APIActionRowComponent = {
+ type: ComponentType.ActionRow,
+ id: 20,
+ components: [
+ {
+ type: ComponentType.RoleSelect,
+ custom_id: 'role',
+ default_values: [
+ {
+ id: '123456789012345678',
+ type: SelectMenuDefaultValueType.Role,
+ },
+ {
+ id: '123456789012345679',
+ type: SelectMenuDefaultValueType.Role,
+ },
+ ],
+ disabled: false,
+ id: 21,
+ max_values: 2,
+ min_values: 0,
+ placeholder: '(none)',
+ required: false,
+ },
+ ],
+ };
+ const userRow: APIActionRowComponent = {
+ type: ComponentType.ActionRow,
+ id: 22,
+ components: [
+ {
+ type: ComponentType.UserSelect,
+ custom_id: 'user',
+ default_values: [
+ {
+ id: '123456789012345678',
+ type: SelectMenuDefaultValueType.User,
+ },
+ {
+ id: '123456789012345679',
+ type: SelectMenuDefaultValueType.User,
+ },
+ ],
+ disabled: false,
+ id: 23,
+ max_values: 2,
+ min_values: 0,
+ placeholder: '(none)',
+ required: false,
+ },
+ ],
+ };
+ const stringRow: APIActionRowComponent = {
+ type: ComponentType.ActionRow,
+ id: 24,
+ components: [
+ {
+ type: ComponentType.StringSelect,
+ custom_id: 'string',
+ options: [
+ {
+ label: 'one',
+ value: '1',
+ default: true,
+ },
+ {
+ label: 'two',
+ value: '2',
+ default: false,
+ },
+ {
+ label: 'three',
+ value: '3',
+ description: 'third',
+ emoji: {
+ id: '3333333333333333333',
+ name: '3',
+ animated: false,
+ },
+ },
+ ],
+ disabled: false,
+ id: 25,
+ max_values: 2,
+ min_values: 0,
+ placeholder: '(none)',
+ required: false,
+ },
+ ],
+ };
+ const container: APIContainerComponent = {
+ type: ComponentType.Container,
+ accent_color: 255,
+ id: 4,
+ components: [
+ buttonRow,
+ file,
+ mediaGallery,
+ section,
+ separator,
+ channelRow,
+ mentionRow,
+ roleRow,
+ userRow,
+ stringRow,
+ ],
+ spoiler: true,
+ };
+ const data: APIMessage = {
+ id: DiscordSnowflake.generate({ timestamp: Date.parse(timestamp) }).toString(),
+ type: MessageType.Reply,
+ position: 15,
+ channel_id: '2',
+ author: user,
+ attachments: [
+ {
+ filename: 'file.txt',
+ description: 'describe attachment',
+ id: '0',
+ proxy_url: 'https://media.example.com/attachment/123.txt',
+ size: 5,
+ url: 'https://example.com/attachment/123.txt',
+ },
+ ],
+ content: '',
+ edited_timestamp: '2025-10-10T15:50:20.292000+00:00',
+ embeds: [],
+ components: [container],
+ message_reference: {
+ channel_id: '505050505050505050',
+ message_id: '606060606060606060',
+ guild_id: '707070707070707070',
+ type: MessageReferenceType.Default,
+ },
+ mention_everyone: false,
+ mention_roles: ['5', '6'],
+ mentions: [user],
+ pinned: false,
+ timestamp,
+ tts: false,
+ flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral,
+ };
+
+ test('Message has all properties', () => {
+ const instance = new Message(data);
+ expect(instance.id).toBe(data.id);
+ expect(instance.channelId).toBe(data.channel_id);
+ expect(instance.position).toBe(data.position);
+ expect(instance.content).toBe(data.content);
+ expect(instance.createdTimestamp).toBe(Date.parse(data.timestamp));
+ expect(dateToDiscordISOTimestamp(instance.createdAt!)).toBe(data.timestamp);
+ expect(instance.flags?.toJSON()).toBe(data.flags);
+ expect(instance.editedTimestamp).toBe(Date.parse(data.edited_timestamp!));
+ expect(dateToDiscordISOTimestamp(instance.editedAt!)).toBe(data.edited_timestamp);
+ expect(instance.nonce).toBe(data.nonce);
+ expect(instance.pinned).toBe(data.pinned);
+ expect(instance.tts).toBe(data.tts);
+ expect(instance.webhookId).toBe(data.webhook_id);
+ expect(instance.type).toBe(MessageType.Reply);
+ expect(instance.toJSON()).toEqual(data);
+ });
+
+ test('Attachment sub-structure', () => {
+ const instances = data.attachments?.map((attachment) => new Attachment(attachment));
+ expect(instances?.map((attachment) => attachment.toJSON())).toEqual(data.attachments);
+ expect(instances?.[0]?.description).toBe(data.attachments?.[0]?.description);
+ expect(instances?.[0]?.filename).toBe(data.attachments?.[0]?.filename);
+ expect(instances?.[0]?.id).toBe(data.attachments?.[0]?.id);
+ expect(instances?.[0]?.size).toBe(data.attachments?.[0]?.size);
+ expect(instances?.[0]?.url).toBe(data.attachments?.[0]?.url);
+ expect(instances?.[0]?.proxyURL).toBe(data.attachments?.[0]?.proxy_url);
+ });
+
+ test('Component sub-structures', () => {
+ const containerInstance = new ContainerComponent(data.components?.[0] as APIContainerComponent);
+ expect(containerInstance.toJSON()).toEqual(container);
+ expect(containerInstance.type).toBe(container.type);
+ expect(containerInstance.id).toBe(container.id);
+ expect(containerInstance.spoiler).toBe(container.spoiler);
+ });
+});
diff --git a/packages/structures/__tests__/types/Mixin.test-d.ts b/packages/structures/__tests__/types/Mixin.test-d.ts
index 59ef4878fca7..d0b0527b188a 100644
--- a/packages/structures/__tests__/types/Mixin.test-d.ts
+++ b/packages/structures/__tests__/types/Mixin.test-d.ts
@@ -1,4 +1,3 @@
-import { expectNotType, expectType } from 'tsd';
import { expectTypeOf } from 'vitest';
import type { MixinTypes } from '../../src/MixinTypes.d.ts';
import type { kMixinConstruct } from '../../src/utils/symbols.js';
@@ -16,22 +15,26 @@ declare const extendsBothOmitBoth: Omit<
keyof Base | typeof kMixinConstruct
>;
-expectType>(extendsNoOmit);
-expectType, [MixinProperty1<'property1'>]>>(extendsOmitProperty1);
-expectNotType>(extendsOmitProperty1);
-expectNotType, [MixinProperty1<'property1'>]>>(extendsNoOmit);
+expectTypeOf(extendsNoOmit).toEqualTypeOf>();
+expectTypeOf(extendsOmitProperty1).toEqualTypeOf, [MixinProperty1<'property1'>]>>();
+expectTypeOf(extendsOmitProperty1).not.toEqualTypeOf>();
+expectTypeOf(extendsNoOmit).not.toEqualTypeOf, [MixinProperty1<'property1'>]>>();
-expectType>(extendsBothNoOmit);
+expectTypeOf(extendsBothNoOmit).toEqualTypeOf>();
// Since MixinProperty2 doesn't utilize the type of property1 in kData, this works and is ok
-expectType, [MixinProperty1<'property1'>, MixinProperty2]>>(extendsBothOmitProperty1);
-expectNotType>(extendsBothOmitProperty1);
+expectTypeOf(extendsBothOmitProperty1).toEqualTypeOf<
+ MixinTypes, [MixinProperty1<'property1'>, MixinProperty2]>
+>();
+expectTypeOf(extendsBothOmitProperty1).not.toEqualTypeOf>();
// Since MixinProperty2 doesn't utilize the type of property1 in kData, this works and is ok
-expectNotType, [MixinProperty1<'property1'>, MixinProperty2]>>(extendsBothNoOmit);
+expectTypeOf(extendsBothNoOmit).not.toEqualTypeOf<
+ MixinTypes, [MixinProperty1<'property1'>, MixinProperty2]>
+>();
// Earlier mixins in the list must specify all properties because of the way merging works
-expectType<
+expectTypeOf(extendsBothOmitBoth).toEqualTypeOf<
MixinTypes, [MixinProperty1<'property1' | 'property2'>, MixinProperty2<'property2'>]>
->(extendsBothOmitBoth);
+>();
expectTypeOf, [MixinProperty1]>>().toBeNever();
// @ts-expect-error Shouldn't be able to assign non identical omits
diff --git a/packages/structures/__tests__/types/channels.test-d.ts b/packages/structures/__tests__/types/channels.test-d.ts
index bbb662ab33ef..c84d0f63b7e0 100644
--- a/packages/structures/__tests__/types/channels.test-d.ts
+++ b/packages/structures/__tests__/types/channels.test-d.ts
@@ -1,79 +1,81 @@
import type { ChannelType, GuildChannelType, GuildTextChannelType, ThreadChannelType } from 'discord-api-types/v10';
-import { expectNever, expectType } from 'tsd';
-import type { Channel } from '../../src/index.js';
+import { expectTypeOf } from 'vitest';
+import type { Channel } from '../../src/channels/Channel.js';
declare const channel: Channel;
if (channel.isGuildBased()) {
- expectType(channel.guildId);
- expectType(channel.type);
+ expectTypeOf(channel.guildId).toBeString();
+ expectTypeOf(channel.type).toEqualTypeOf();
if (channel.isDMBased()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isPermissionCapable()) {
- expectType>(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf<
+ Exclude
+ >();
}
if (channel.isTextBased()) {
- expectType(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf();
}
if (channel.isWebhookCapable()) {
- expectType>(
- channel.type,
- );
+ expectTypeOf(channel.type).toEqualTypeOf<
+ ChannelType.GuildForum | ChannelType.GuildMedia | Exclude
+ >();
}
if (channel.isThread()) {
- expectType(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf();
}
if (channel.isThreadOnly()) {
- expectType(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf();
}
if (channel.isVoiceBased()) {
- expectType(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf();
if (!channel.isTextBased()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (!channel.isWebhookCapable()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
}
}
if (channel.isDMBased()) {
- expectType(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf();
if (channel.isGuildBased()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isPermissionCapable()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isWebhookCapable()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isVoiceBased()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isThread()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isThreadOnly()) {
- expectNever(channel);
+ expectTypeOf(channel).toBeNever();
}
if (channel.isTextBased()) {
- expectType(channel.type);
+ expectTypeOf(channel.type).toEqualTypeOf();
}
}
diff --git a/packages/structures/package.json b/packages/structures/package.json
index 0f9c585b534c..8a7724434f99 100644
--- a/packages/structures/package.json
+++ b/packages/structures/package.json
@@ -63,27 +63,26 @@
"dependencies": {
"@discordjs/formatters": "workspace:^",
"@sapphire/snowflake": "^3.5.5",
- "discord-api-types": "^0.38.31"
+ "discord-api-types": "^0.38.36"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@discordjs/scripts": "workspace:^",
- "@favware/cliff-jumper": "^4.1.0",
- "@types/node": "^22.18.13",
- "@vitest/coverage-v8": "^3.2.4",
+ "@favware/cliff-jumper": "^6.0.0",
+ "@types/node": "^22.19.1",
+ "@vitest/coverage-v8": "^4.0.15",
"cpy-cli": "^6.0.0",
"cross-env": "^10.1.0",
"esbuild-plugin-version-injector": "^1.2.1",
- "eslint": "^9.38.0",
+ "eslint": "^9.39.1",
"eslint-config-neon": "^0.2.9",
"eslint-formatter-compact": "^9.0.1",
"eslint-formatter-pretty": "^7.0.0",
- "prettier": "^3.6.2",
- "tsd": "^0.33.0",
- "tsup": "^8.5.0",
- "turbo": "^2.5.8",
+ "prettier": "^3.7.4",
+ "tsup": "^8.5.1",
+ "turbo": "^2.6.3",
"typescript": "~5.9.3",
- "vitest": "^3.2.4"
+ "vitest": "^4.0.15"
},
"engines": {
"node": ">=22.12.0"
@@ -91,8 +90,5 @@
"publishConfig": {
"access": "public",
"provenance": true
- },
- "tsd": {
- "directory": "__tests__/types"
}
}
diff --git a/packages/structures/src/bitfields/AttachmentFlagsBitField.ts b/packages/structures/src/bitfields/AttachmentFlagsBitField.ts
new file mode 100644
index 000000000000..a4db45687de1
--- /dev/null
+++ b/packages/structures/src/bitfields/AttachmentFlagsBitField.ts
@@ -0,0 +1,16 @@
+import { AttachmentFlags } from 'discord-api-types/v10';
+import { BitField } from './BitField.js';
+
+/**
+ * Data structure that makes it easy to interact with a {@link Attachment#flags} bitfield.
+ */
+export class AttachmentFlagsBitField extends BitField {
+ /**
+ * Numeric attachment flags.
+ */
+ public static override readonly Flags = AttachmentFlags;
+
+ public override toJSON() {
+ return super.toJSON(true);
+ }
+}
diff --git a/packages/structures/src/bitfields/MessageFlagsBitField.ts b/packages/structures/src/bitfields/MessageFlagsBitField.ts
new file mode 100644
index 000000000000..233baf55989c
--- /dev/null
+++ b/packages/structures/src/bitfields/MessageFlagsBitField.ts
@@ -0,0 +1,16 @@
+import { MessageFlags } from 'discord-api-types/v10';
+import { BitField } from './BitField.js';
+
+/**
+ * Data structure that makes it easy to interact with a {@link Message#flags} bitfield.
+ */
+export class MessageFlagsBitField extends BitField {
+ /**
+ * Numeric message flags.
+ */
+ public static override readonly Flags = MessageFlags;
+
+ public override toJSON() {
+ return super.toJSON(true);
+ }
+}
diff --git a/packages/structures/src/bitfields/index.ts b/packages/structures/src/bitfields/index.ts
index 011821c8a4a1..9598b8090b84 100644
--- a/packages/structures/src/bitfields/index.ts
+++ b/packages/structures/src/bitfields/index.ts
@@ -1,4 +1,6 @@
export * from './BitField.js';
+export * from './AttachmentFlagsBitField.js';
export * from './ChannelFlagsBitField.js';
+export * from './MessageFlagsBitField.js';
export * from './PermissionsBitField.js';
diff --git a/packages/structures/src/channels/AnnouncementChannel.ts b/packages/structures/src/channels/AnnouncementChannel.ts
index ccf12515586b..1d0ebcd6a7a4 100644
--- a/packages/structures/src/channels/AnnouncementChannel.ts
+++ b/packages/structures/src/channels/AnnouncementChannel.ts
@@ -10,18 +10,17 @@ import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
-export interface AnnouncementChannel
- extends MixinTypes<
- Channel,
- [
- TextChannelMixin,
- ChannelParentMixin,
- ChannelPermissionMixin,
- ChannelPinMixin,
- ChannelSlowmodeMixin,
- ChannelTopicMixin,
- ]
- > {}
+export interface AnnouncementChannel extends MixinTypes<
+ Channel,
+ [
+ TextChannelMixin,
+ ChannelParentMixin,
+ ChannelPermissionMixin,
+ ChannelPinMixin,
+ ChannelSlowmodeMixin,
+ ChannelTopicMixin,
+ ]
+> {}
/**
* Sample Implementation of a structure for announcement channels, usable by direct end consumers.
diff --git a/packages/structures/src/channels/AnnouncementThreadChannel.ts b/packages/structures/src/channels/AnnouncementThreadChannel.ts
index 418516c9b938..daf0a05e9c43 100644
--- a/packages/structures/src/channels/AnnouncementThreadChannel.ts
+++ b/packages/structures/src/channels/AnnouncementThreadChannel.ts
@@ -11,19 +11,20 @@ import { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
-export interface AnnouncementThreadChannel
- extends MixinTypes<
- Channel,
- [
- TextChannelMixin,
- ChannelOwnerMixin,
- ChannelParentMixin,
- ChannelPinMixin,
- ChannelSlowmodeMixin,
- GuildChannelMixin,
- ThreadChannelMixin,
- ]
- > {}
+export interface AnnouncementThreadChannel<
+ Omitted extends keyof APIAnnouncementThreadChannel | '' = '',
+> extends MixinTypes<
+ Channel,
+ [
+ TextChannelMixin,
+ ChannelOwnerMixin,
+ ChannelParentMixin,
+ ChannelPinMixin,
+ ChannelSlowmodeMixin,
+ GuildChannelMixin,
+ ThreadChannelMixin,
+ ]
+> {}
/**
* Sample Implementation of a structure for announcement threads, usable by direct end consumers.
diff --git a/packages/structures/src/channels/CategoryChannel.ts b/packages/structures/src/channels/CategoryChannel.ts
index 56b1196358ab..dd891954dfd3 100644
--- a/packages/structures/src/channels/CategoryChannel.ts
+++ b/packages/structures/src/channels/CategoryChannel.ts
@@ -6,11 +6,10 @@ import { Channel } from './Channel.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
-export interface CategoryChannel
- extends MixinTypes<
- Channel,
- [ChannelPermissionMixin, GuildChannelMixin]
- > {}
+export interface CategoryChannel extends MixinTypes<
+ Channel,
+ [ChannelPermissionMixin, GuildChannelMixin]
+> {}
/**
* Sample Implementation of a structure for category channels, usable by direct end consumers.
diff --git a/packages/structures/src/channels/Channel.ts b/packages/structures/src/channels/Channel.ts
index 2ea00eacdc2d..54701059c2bb 100644
--- a/packages/structures/src/channels/Channel.ts
+++ b/packages/structures/src/channels/Channel.ts
@@ -2,7 +2,7 @@ import { DiscordSnowflake } from '@sapphire/snowflake';
import type { APIChannel, APIPartialChannel, ChannelType, ChannelFlags } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { ChannelFlagsBitField } from '../bitfields/ChannelFlagsBitField.js';
-import { kData, kPatch } from '../utils/symbols.js';
+import { kData } from '../utils/symbols.js';
import { isIdSet } from '../utils/type-guards.js';
import type { Partialize } from '../utils/types.js';
import type { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
@@ -50,15 +50,6 @@ export class Channel<
super(data as ChannelDataType);
}
- /**
- * {@inheritDoc Structure.[kPatch]}
- *
- * @internal
- */
- public override [kPatch](data: Partial>) {
- return super[kPatch](data);
- }
-
/**
* The id of the channel
*/
diff --git a/packages/structures/src/channels/DMChannel.ts b/packages/structures/src/channels/DMChannel.ts
index c679b2caa6b9..c85a085bc9d2 100644
--- a/packages/structures/src/channels/DMChannel.ts
+++ b/packages/structures/src/channels/DMChannel.ts
@@ -7,11 +7,10 @@ import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { DMChannelMixin } from './mixins/DMChannelMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
-export interface DMChannel
- extends MixinTypes<
- Channel,
- [DMChannelMixin, TextChannelMixin, ChannelPinMixin]
- > {}
+export interface DMChannel extends MixinTypes<
+ Channel,
+ [DMChannelMixin