feat(guide): port legacy guide (#10938)

* feat: initial attempt at porting legacy guide

* feat: completion of legacy guide backport

* chore: lockfile shenanigans

* fix: handle svgs

* fix: replace svg with mermaid integration

* chore: format

* chore: remove unnecssary bullet

* chore: cleanup code highlights

* chore: explicit return

* chore: move display components after interactive components in sidebar

* chore: voice

* top link should be installation
* add docs link to sidebar

* feat: subguide-based accent styles

* chore: don't list faq twice

* chore: mention display components in interactive components

* fix: remove unoccs/order rule from guide

* chore: redirect to legacy guide instead of /guide root

* refactor: use `<kbd>`

* refactor: more kbd use

* Update apps/guide/content/docs/legacy/app-creation/handling-events.mdx

Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com>

* chore: fix typos

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* chore: fix typos

* chore: fix links regarding secret stores across coding platforms

* chore: fix typo

* chore: link node method directly

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* chore: typos

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* chore: typo

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* fix: prevent v14 changes from being listed twice

* chore: prefer relative links

* chore: missed link conversion

* chore: missed link conversion

* chore: fix link

* chore: remove legacy code highlight markers

* chore: rephrase and extend contributing guidelines

* feat(setup): suggest cli flag over dotenv package

* chore: move introduction in sidebar

better navigation experience if the 'next page' in intro refers to getting started vs. updating/faq

* fix: replace outdated link

* fix: update voice dependencies

* chore: update node install instructions

* fix: list in missing access callout

* chore: match bun env file format

* chore: restore ffmpeg disclaimer

* fix: lockfile conflict

* chore: action row typo

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* chore: no longer use at-next for pino

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com>
Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
This commit is contained in:
Souji
2025-07-08 15:01:50 +02:00
committed by GitHub
parent ee3ca6f7c6
commit bc6005f446
136 changed files with 11847 additions and 48 deletions

View File

@@ -0,0 +1,101 @@
---
title: Additional Information
---
<Callout>This page is a follow-up and bases its code on [the previous page](./).</Callout>
Here we will topic some extra topics about sharding that might have raised concerns.
## Legend
- `manager` is an instance of `ShardingManager`, e.g. `const manager = new ShardingManager(file, options);`
- `client.shard` refers to the current shard.
## Shard messages
For shards to communicate, they have to send messages to one another, as they each have another process. You must wait for each shard to finish spawning before you can listen to their events, otherwise `ShardingManager#shards` will be an empty `Collection`. You can listen for these messages on the individual shards by adding the following lines in your `index.js` file:
```js title="index.js"
manager
.spawn()
.then((shards) => {
shards.forEach((shard) => {
shard.on('message', (message) => {
console.log(`Shard[${shard.id}] : ${message._eval} : ${message._result}`);
});
});
})
.catch(console.error);
```
As the property names imply, the `_eval` property is what the shard is attempting to evaluate, and the `_result` property is the output of said evaluation. However, these properties are only guaranteed if a _shard_ is sending a message. There will also be an `_error` property, should the evaluation have thrown an error.
You can also send messages via `process.send('hello')`, which would not contain the same information. This is why the `.message` property's type is declared as `*` in the `Shard#message` documentation.
## Specific shards
There might be times where you want to target a specific shard. An example would be to kill a specific shard that isn't working as intended. You can achieve this by taking the following snippet (in a command, preferably):
<Callout>
`ShardClientUtil#ids` can hold multiple ids. If you use the default sharding manager, the `.ids` array will only have
one entry.
</Callout>
```js
client.shard.broadcastEval((c) => {
if (c.shard.ids.includes(0)) process.exit();
});
```
If you're using something like [PM2](http://pm2.keymetrics.io/) or [Forever](https://github.com/foreverjs/forever), this is an easy way to restart a specific shard. Remember, `ShardClientUtil#broadcastEval` sends a message to **all** shards, so you have to check if it's on the shard you want.
## `ShardingManager#shardArgs` and `ShardingManager#execArgv`
Consider the following example of creating a new `ShardingManager` instance:
```js
const manager = new ShardingManager('./bot.js', {
execArgv: ['--trace-warnings'],
shardArgs: ['--ansi', '--color'],
token: 'your-token-goes-here',
});
```
The `execArgv` property is what you would usually pass to Node without sharding, e.g.:
```sh
node --trace-warnings bot.js
```
You can find a list of command-line options for Node [here](https://nodejs.org/api/cli.html).
The `shardArgs` property is what you would usually pass to your bot without sharding, e.g.:
```sh
node bot.js --ansi --color
```
You can access them later as usual via `process.argv`, which contains an array of executables, your main file, and the command-line arguments used to execute the script.
## Eval arguments
There may come the point where you will want to pass arguments from the outer scope into a `.broadcastEval()` call.
```js
function funcName(c, { arg }) {
// ...
}
client.shard.broadcastEval(funcName, { context: { arg: 'arg' } });
```
The `BroadcastEvalOptions` typedef was introduced in discord.js v13 as the second parameter in `.broadcastEval()`. It accepts two properties: `shard` and `context`. The `context` property will be sent as the second argument to your function.
In this small snippet, an argument is passed to the `funcName` function through this parameter.
The function will receive the arguments as an object as the second parameter.
<Callout>
The `context` option only accepts properties which are JSON-serializable. This means you cannot pass complex data
types in the context directly. For example, if you sent a `User` instance, the function would receive the raw data
object.
</Callout>

View File

@@ -0,0 +1,165 @@
---
title: Advanced Sharding
---
<Callout type="warn">
This page is a follow-up and bases its code on [the previous page](./additional-information), which assumes knowledge
of arguments and passing functions.
</Callout>
## Sending messages across shards
Let's start with the basic usage of shards. At some point in bot development, you might have wanted to send a message to another channel, which may or may not necessarily be on the same guild, which means it may or may not be on the same shard. To achieve this, you will need to go back to your friend `.broadcastEval()` and try every shard for the desired channel. Suppose you have the following code in your `interactionCreate` event:
```js title="bot.js"
client.on(Events.InteractionCreate, (interaction) => {
// ...
// [!code focus:9]
if (commandName === 'send') {
const id = interaction.options.getString('destination');
const channel = client.channels.cache.get(id);
if (!channel) return interaction.reply('I could not find such a channel.');
channel.send('Hello!');
return interaction.reply(`I have sent a message to channel: \`${id}\`!`);
}
});
```
This will never work for a channel that lies on another shard. So, let's remedy this.
<Callout>
`ShardClientUtil#ids` can hold multiple ids. If you use the default sharding manager, the `.ids` array will only have
one entry.
</Callout>
```js title="bot.js"
if (commandName === 'send') {
const id = interaction.options.getString('destination');
return client.shard
.broadcastEval(
async (c, { channelId }) => {
const channel = c.channels.cache.get(channelId);
if (channel) {
await channel.send(`This is a message from shard ${c.shard.ids.join(',')}!`);
return true;
}
return false;
},
{ context: { channelId: id } },
)
.then(console.log);
}
```
If all is well, you should notice an output like `[false, true, false, false]`. If it is not clear why `true` and `false` are hanging around, the last expression of the eval statement will be returned. You will want this if you want any feedback from the results. Now that you have observed said results, you can adjust the command to give yourself proper feedback, like so:
```js title="bot.js"
return client.shard
.broadcastEval((c) => {
// ...
})
.then((sentArray) => {
// Search for a non falsy value before providing feedback
if (!sentArray.includes(true)) {
return message.reply('I could not find such a channel.');
}
return message.reply(`I have sent a message to channel: \`${id}\`!`);
});
```
And that's it for this section! You have successfully communicated across all of your shards.
## Using functions continued
If you remember, there was a brief mention of passing functions through `.broadcastEval()`, but no super clear description of exactly how to go about it. Well, fret not, for this section will cover it! Suppose you have the following code in your `interactionCreate` event:
```js title="bot.js"
client.on(Events.InteractionCreate, (interaction) => {
// ...
// [!code focus:5]
if (commandName === 'emoji') {
const emojiId = interaction.options.getString('emoji');
const emoji = client.emojis.cache.get(emojiId);
return interaction.reply(`I have found an emoji ${emoji}!`);
}
});
```
The aforementioned code will essentially search through `client.emojis.cache` for the provided id, which will be given provided by the `emoji` option. However, with sharding, you might notice it doesn't search through all the client's emojis. As mentioned in an earlier section of this guide, the different shards partition the client and its cache. Emojis derive from guilds meaning each shard will have the emojis from all guilds for that shard. The solution is to use `.broadcastEval()` to search all the shards for the desired emoji.
Let's start with a basic function, which will try to grab an emoji from the current client and return it.
```js
function findEmoji(c, { nameOrId }) {
return c.emojis.cache.get(nameOrId) || c.emojis.cache.find((e) => e.name.toLowerCase() === nameOrId.toLowerCase());
}
```
Next, you need to call the function in your command properly. If you recall from [this section](./additional-information#eval-arguments), it is shown there how to pass a function and arguments correctly.
```js
client.on(Events.InteractionCreate, (interaction) => {
// ...
// [!code :5]
if (commandName === 'emoji') {
const emojiNameOrId = interaction.options.getString('emoji');
return client.shard.broadcastEval(findEmoji, { context: { nameOrId: emojiNameOrId } }).then(console.log);
}
});
```
Now, run this code, and you will surely get a result that looks like the following:
```js
[
{
guild: {
members: {},
// ...
id: '222078108977594368',
name: 'discord.js Official',
icon: '6e4b4d1a0c7187f9fd5d4976c50ac96e',
// ...
emojis: {},
},
id: '383735055509356544',
name: 'duckSmug',
requiresColons: true,
managed: false,
animated: false,
_roles: [],
},
];
```
While this result isn't _necessarily_ bad or incorrect, it's simply a raw object that got `JSON.parse()`'d and `JSON.stringify()`'d over, so all of the circular references are gone. More importantly, The object is no longer a true `GuildEmoji` object as provided by discord.js. _This means none of the convenience methods usually provided to you are available._ If this is a problem for you, you will want to handle the item _inside_ the `broadcastEval`. Conveniently, the `findEmoji` function will be run, so you should execute your relevant methods there, before the object leaves the context.
```js
function findEmoji(c, { nameOrId }) {
const emoji =
c.emojis.cache.get(nameOrId) || c.emojis.cache.find((e) => e.name.toLowerCase() === nameOrId.toLowerCase());
if (!emoji) return null;
// If you wanted to delete the emoji with discord.js, this is where you would do it. Otherwise, don't include this code.
emoji.delete();
return emoji;
}
```
With all that said and done, usually you'll want to display the result, so here is how you can go about doing that:
```js
return client.shard.broadcastEval(findEmoji, { context: { nameOrId: emojiNameOrId } }).then((emojiArray) => {
// Locate a non falsy result, which will be the emoji in question
const foundEmoji = emojiArray.find((emoji) => emoji);
if (!foundEmoji) return message.reply('I could not find such an emoji.');
return message.reply(
`I have found the ${foundEmoji.animated ? `<${foundEmoji.identifier}>` : `<:${foundEmoji.identifier}> emoji!`}!`,
);
});
```
And that's all! The emoji should have pretty-printed in a message, as you'd expect.

View File

@@ -0,0 +1,209 @@
---
title: App Sharding
---
## When to shard
Before you dive into this section, please note that sharding may not be necessary for you. Sharding is only **required at 2,500 guilds** — at that point, Discord will not allow your bot to login without sharding. With that in mind, you should consider this when your bot is around 2,000 guilds, which should be enough time to get this working. Contrary to popular belief, sharding itself is very simple. It can be complicated depending on your bot's needs, however.
If your bot is in a total of 2,000 or more servers, then please continue with this guide. Otherwise, it may be a good idea to wait until then.
<Callout>
Sharding is only relevant if you app uses gateway events. For webhook callbacks, this is completely irrelevant!
</Callout>
## How does sharding work?
As an application grows large, a developer may find it necessary to split their process to run parallel to maximize efficiency. On a much larger scale of things, the developer might notice their process slow down, amongst other problems.
[Check out the official Discord documentation on the topic.](https://discord.com/developers/docs/topics/gateway#sharding)
<Callout type="warn">
This guide only explains the basics of sharding using the built-in `ShardingManager`, which can run shards as separate processes or threads on a single machine.
If you need to scale beyond that (e.g., running shards on multiple machines/containers), you can still do it with discord.js by passing appropriate options to the Client constructor. Nevertheless, you will be on your own regarding managing shards and sharing information between them.
</Callout>
<Callout>
Apart from `ShardingManager`, discord.js also supports a sharding mode known as Internal sharding. Internal sharding creates multiple websocket connections from the same process, and does not require major code changes. To enable it, simply pass `shards: 'auto'` as ClientOptions to the Client constructor.
However, **internal sharding is not ideal for bigger bots due to high memory usage** of the single main process and will not be further discussed in this guide.
</Callout>
## Sharding file
First, you'll need to have a file that you'll be launching from now on, rather than your original `index.js` file. It's highly recommended renaming that to `bot.js` and naming this new file to `index.js` instead. Copy & paste the following snippet into your new `index.js` file.
```js title="index.js" lineNumbers
const { ShardingManager } = require('discord.js');
const manager = new ShardingManager('./bot.js', { token: 'your-token-goes-here' });
manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`));
manager.spawn();
```
The above code utilizes the discord.js sharding manager to spawn the recommended amount of shards for your bot. The recommended amount should be approximately 1,000 guilds per shard. Note that you have to attach the event listener to `shardCreate` before calling `.spawn()` to prevent a race condition possibly preventing shard 0 from logging the successful launch. Even though you provide the token here, you will still need to send it over to the main bot file in `client.login()`, so don't forget to do that.
<Callout>
You can find the methods available for the ShardingManager class `ShardingManager`. Though, you may not be making much
use of this section, unlike the next feature we will explore, which you may learn about by clicking [this
link](./additional-information).
</Callout>
## Getting started
You will most likely have to change some code to get your newly sharded bot to work. If your bot is very basic, then you're in luck! We assume you probably have some form of a `stats` command, by which you can quickly view your bot's statistics, such as its server count. We will use it as an example that needs to adapt to running with shards.
In this code, you likely have the snippet `client.guilds.cache.size`, which counts the number of _cached_ guilds attached to that client. Since sharding will launch multiple processes, each process (each shard) will now have its subset collection of guilds it is responsible for. This means that your original code will not function as you might expect.
Here is some sample code for a `stats` command, without sharding taken into consideration:
```js title="bot.js" lineNumbers
const { Client, Events, GatewayIntentBits } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on(Events.InteractionCreate, (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
if (commandName === 'stats') {
return interaction.reply(`Server count: ${client.guilds.cache.size}.`);
}
});
client.login('your-token-goes-here');
```
Let's say your bot is in a total of 3,600 guilds. Using the recommended shard count, you might end up at four shards, each containing approximately 900 guilds. If a guild is on a specific shard (shard #2, for example) and receives this command, the guild count will be close to 900, which is not the "correct" number of guilds for your bot. Let's take a look at how to fix that.
## FetchClientValues
One of the most common sharding utility methods you'll be using is `ShardClientUtil#fetchClientValues`. This method retrieves a property on the Client object of all shards.
Take the following snippet of code:
```js
client.shard.fetchClientValues('guilds.cache.size').then(console.log); // [!code word:fetchClientValues]
```
If you run it, you will notice an output like `[898, 901, 900, 901]`. You will be correct in assuming that that's the total number of guilds per shard stored in an array in the Promise. This probably isn't the ideal output for guild count, so let's use [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) to provide a better output.
<Callout>
It's highly recommended for you to visit [the
documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) to
understand how the `reduce()` method works, as you will probably find great use of it in sharding.
</Callout>
In this case, this method iterates through the array and adds each current value to the total amount:
```js
client.shard
.fetchClientValues('guilds.cache.size')
.then((results) => {
console.log(`${results.reduce((acc, guildCount) => acc + guildCount, 0)} total guilds`); // [!code word:reduce]
})
.catch(console.error);
```
While it's a bit unattractive to have more nesting in your commands, it is necessary when not using `async`/`await`. Now, the code at the top should look something like the below:
```js title="bot.js" lineNumbers=5
client.on(Events.InteractionCreate, (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
// [!code focus:9]
if (commandName === 'stats') {
return interaction.reply(`Server count: ${client.guilds.cache.size}.`); // [!code --]
return client.shard // [!code ++:6]
.fetchClientValues('guilds.cache.size')
.then((results) => {
return interaction.reply(`Server count: ${results.reduce((acc, guildCount) => acc + guildCount, 0)}`);
})
.catch(console.error);
}
});
// ...
```
## BroadcastEval
Next, check out another handy sharding method known as `ShardClientUtil#broadcastEval`. This method makes all of the shards evaluate a given method, which receives a `client` and a `context` argument. The `client` argument refers to the Client object of the shard evaluating it. You can read about the `context` argument [here](./sharding/additional-information#eval-arguments).
```js
client.shard
.broadcastEval((c) =>
c.guilds.cache // [!code word:broadcastEval]
.reduce((acc, guild) => acc + guild.memberCount, 0),
)
.then(console.log);
```
This will run the code given to `broadcastEval` on each shard and return the results to the Promise as an array, once again. You should see something like `[9001, 16658, 13337, 15687]` logged. The code sent to each shard adds up the `memberCount` property of every guild that shard is handling and returns it, so each shard's total guild member count. Of course, if you want to total up the member count of _every_ shard, you can do the same thing again on the Promise results.
```js
client.shard
.broadcastEval((c) => c.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)) // [!code word:boradcastEval]
.then((results) => {
return interaction.reply(`Total member count: ${results.reduce((acc, memberCount) => acc + memberCount, 0)}`);
})
.catch(console.error);
```
## Putting them together
You'd likely want to output both pieces of information in the stats command. You can combine these two results with [Promise.all()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all):
```js
const promises = [
client.shard.fetchClientValues('guilds.cache.size'),
client.shard.broadcastEval((c) => c.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)),
];
Promise.all(promises) // [!code word:all]
.then((results) => {
const totalGuilds = results[0].reduce((acc, guildCount) => acc + guildCount, 0);
const totalMembers = results[1].reduce((acc, memberCount) => acc + memberCount, 0);
return interaction.reply(`Server count: ${totalGuilds}\nMember count: ${totalMembers}`);
})
.catch(console.error);
```
`Promise.all()` runs every Promise you pass inside an array in parallel and waits for each to finish before returning their results simultaneously. The result is an array that corresponds with the array of Promises you passso the first result element will be from the first Promise. With that, your stats command should look something like this:
```js title="bot.js" lineNumbers=5
client.on(Events.InteractionCreate, (interaction) => {
// ...
// [!code focus:21]
if (commandName === 'stats') {
return client.shard // [!code --:6]
.fetchClientValues('guilds.cache.size')
.then((results) => {
return interaction.reply(`Server count: ${results.reduce((acc, guildCount) => acc + guildCount, 0)}`);
})
.catch(console.error);
// [!code ++:12]
const promises = [
client.shard.fetchClientValues('guilds.cache.size'),
lient.shard.broadcastEval((c) => c.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)),
];
return Promise.all(promises)
.then((results) => {
const totalGuilds = results[0].reduce((acc, guildCount) => acc + guildCount, 0);
const totalMembers = results[1].reduce((acc, memberCount) => acc + memberCount, 0);
return interaction.reply(`Server count: ${totalGuilds}\nMember count: ${totalMembers}`);
})
.catch(console.error);
}
});
```
The next section contains additional changes you might want to consider.