4.2.1 changelog
Published
Entries marked Breaking change a working bot; the migration guide says what to do about them. Every release of the line is in the changelog.
Patch Changes
#577
2b4be88Thanks @l7aromeo! - Catalog checks read a catalog's own keys and shapes, and a refused builder says what is wrong with a name:expectCompleteCatalogreads a group with anotherkey, such asreasons: { spam, offTopic, other }, as a group, as the translator's types do. It no longer fails a correct catalog for "lacking" plural forms, and it reports a key such a group misses in a locale.expectCompleteCatalogreads only a catalog's own keys, so a missing key namedconstructorortoStringis reported. It also reports a translation of another shape than the default's, a text for a plural or the reverse, asn should be a plural, as the default is.- A key with a
.in its name, such as'ban.done', is never found, because a lookup reads it as a path.createTranslatornow warns about each one, naming the locale and the key, andMessageKey,StringMessageKeyandLocalizationKeyleave it out, so using one fails to compile. Nest it as a group:ban: { done: … }. LocaleCatalog<C>takes ameocordgroup with your translations of MeoCord's own texts, ascreateTranslatordoes.- A builder refused for a name gives Discord's rule for one: letters, numbers,
-and_, lowercase where the script has case, without spaces, up to 32 characters, in every locale.
A catalog test that passed with a gap of these kinds can now fail and name it. See Localisation.
#620
4e395acThanks @l7aromeo! - A testing module asked todispatch,invokeoremitafterclose()now warns once that it is closed and its services are shut down, or shutting down, including whileclose()is still running. It still runs the call, as before. A misplacedclose(), such as one inafterEachthat runs before a test's last call, shows instead of passing quietly on that state. See Testing.#605
33d724bThanks @l7aromeo! -@Commandwarns, naming the handler, about a component pattern no customId can match: one that is empty, or whose shortest customId is over Discord's 100 characters. Such a handler never runs; the bot still starts.route('').build()throws a RangeError, as it does for an id over 100 characters, rather than returning an empty customId Discord refuses.#595
cdb8dcfThanks @l7aromeo! -meocord start --devchecks a savedmeocord.config.tsas startup does: a config with options of the wrong type, or one that fails to load, is reported once, with every problem, and the running bot is left as it is, rather than starting with that config and failing later. A config that exports only named values is refused with "it must export an object as its default export", naming what it exports, instead of a warning for each export. A config that throws as it loads, such as for a variable it requires, is reported with the line inmeocord.config.tsit threw at, and a built bot whose config throws says to check the environment it reads, rather than to rebuild.#599
e3c727aThanks @l7aromeo! - A controller listed twice in@MeoCord({ controllers })orMeoCordTestingModule.create({ controllers })is one controller: its@MessageHandler()listeners run once per message, and its reaction handlers once per reaction, where they ran twice. Commands and components were already routed once.#598
585c076Thanks @l7aromeo! - A cooldown'sbymay return any string, one holding half of an emoji included, such as text cut short withslice. Such a value made the call fail with "URIError: URI malformed" before the handler ran; it now counts under a key of its own. Every value that worked before keeps its key, so running cooldowns carry on across the upgrade.#598
585c076Thanks @l7aromeo! - WithdmOnCooldown, a cooldown counted per server, per channel or globally tells each author it refuses once per wait, asper: 'user'already did. Only the first author refused in a wait was told, and the rest were refused without a word.#579
4aba54dThanks @l7aromeo! -@Cooldownnow warns, naming the handler, when it is given abypassthat is not a function, such astrue, which fails every call it counts, or asecondsthat is not a number, such as'5', which it reads as one. Both get past the types only from JavaScript or through a cast. They still decorate as before, and the next major version (5.0) refuses them; givebypassa function andsecondsa number to silence the warning. Abypassoffalseornullstill means none.#581
150e174Thanks @l7aromeo! -testCooldownStorenow catches three ways a store can break the cooldown contract:- a
releasethat frees the key's other calls too, which resets a user's cooldown whenever@Cooldowngives back a call; - a batch refusal that names the first cooldown to refuse rather than the longest wait, which tells users to retry too soon;
- a wait counted from the oldest call in the window, rather than from the oldest of the newest
usescalls, after a limit is lowered.
A store that keeps the contract passes as before. A store that gets one of these wrong now fails its own suite run, naming the case. See Cooldown stores.
- a
#588
27b03a5Thanks @l7aromeo! -meocord createfolds accented letters into the app's folder and package name instead of dropping them: "Café Bot" givescafe-bot, where it gavecaf-bot, and "Straße" givesstrasse. A name with no Latin letters or digits, such as one written only in another script, is refused with a message that says it needs Latin letters or digits.#612
9cb5e9eThanks @l7aromeo! -respond()keeps track of which message is the answer, so it no longer edits or deletes a reply that is gone, which Discord refused with 10008 (Unknown Message):- After
delete(),send(),edit(), an error answer andfollowUp()send a follow-up, which becomes the answer that latersend(),edit()anddelete()act on. A follow-up that takes the place of an edit is private when the deleted answer was, unless the payload setsflags.messageisundefineduntil something is sent, and a seconddelete()with nothing sent since throws. - After a private
followUp()on a public deferral, which deletes the deferral, the follow-up is the answer:send()andedit()edit it,delete()deletes it, andmessageis it.
A test that expected
editReplyin these sequences now sees afollowUp, or an edit of the follow-up by its id.- After
#571
e9fc8bcThanks @l7aromeo! -meocord start --dev,register --devandbuild --devrun as development whateverNODE_ENVthe shell holds, as their help and the CLI guide say. WithNODE_ENV=productionset in the shell,start --devloaded the config from the production.envfiles, so a token kept only in.env.developmentwas reported missing, andregister --devregistered the commands as production does rather than tocommands.developmentGuild.--prodstill keeps aNODE_ENVthe shell sets.#583
0b4be9cThanks @l7aromeo! - A direct call on a controller,module.get(Controller).method(interaction), runs its guards in the testing module that made the instance. With two modules compiled from one controller, a direct call on the first ran the second module's guard stubs, services and container, since the last compiled module was the only one recorded on the class. A bot has one container, so nothing changes there. See Testing.#585
5cfb77dThanks @l7aromeo! - A built bot now loads its packages after its config, and so after.env: a package that readsprocess.envas it loads, in a build withoutbundleDependencies, gets the values.envsets, as a self-contained build already gave it. A package kept out of the bundle loads where the code importing it runs: an ESMimportis a dynamicimport(), and a CommonJSrequirestays arequireat its call site. A package listed inexternalsthat a dependency requires inside atrytherefore no longer stops the bot at startup when it is missing; an ESM import of one still does.A self-contained build started on another platform stops with MeoCord's message naming both platforms, before any native addon loads, instead of the addon's own error. The message is now printed before the bot's logger is set up, as a plain
[ERROR] [MeoCord]line.A build without
bundleDependenciesremoves thenode_modules,package.jsonandmeocord.platform.jsona previous self-contained build left indist, so it no longer runs packages from that stale copy. An external named as a file inside a package, such asexternals: ['lodash/fp.js'], now packs its package.#584
f2a9bf4Thanks @l7aromeo! -emit's JSDoc, andEmitResult.ran's, now say what a test sees from a guard'sGuardDeniedErrorand a handler'sUserError: they rejectemitas any error does, because the bot's event fallback doesn't run in a testing module. On a bot, that fallback skips a refused event, and logs aUserError, answering it as a reply when the event carries a message. Check them withrejects. See Invoke and dispatch.#600
4729b10Thanks @l7aromeo! - A primary entry point command is now registered only globally, as Discord requires. Discord refuses a guild update that holds one (400, code 50222), and before, that refusal took every other command in the update with it.- A production run sends the entry point in the global update, even with
commands.guildsset, and ignores aguildsrestriction on its builder, with a warning. Withcommands.guilds, that global update holds the entry point and replaces any global commands left from an earlier configuration. - A development run with
developmentGuild, andmeocord register --guild, leave the entry point out of the guild update with a warning, so the rest of the commands register. They send nothing global, as a global update from such a run would delete the application's other global commands. A production run ormeocord registerregisters the entry point.
See Slash commands.
- A production run sends the entry point in the global update, even with
#575
f8c2a51Thanks @l7aromeo! - Help rendered from your catalog'smeocord.helptexts now keeps its lists apart in every language, on Node and Bun alike: params join with·and aliases with,, as in MeoCord's English. Before, zh-CN ran them together, ja and ru joined them with a space, and de joined the last two with "und". MeoCord's own English help is unchanged. A test that pins translated help sees the new separators. See Message commands.#608
772dbd4Thanks @l7aromeo! - In a built bot,import.meta.urlnames the running bundle indist, asimport.meta.dirnameandimport.meta.filenamealready do. It named the source file on the machine that built the bot, so code finding its files from it worked only there, and the bundle carried the build machine's source path.new URL('./file', import.meta.url)assets keep resolving beside the bundle, andimport.meta.env.MODEis still written in.#589
921ff91Thanks @l7aromeo! -invokechecks a handler gets the arguments it declares when it builds none for it, as for a reaction handler given its reaction without itsReactionEvent. The call used to reach the handler and fail inside it, withCannot destructure property 'user' of 'undefined'. Now:- under
useStrictMocks(),invokerefuses the call before the handler runs, naming the fix:Stars.star takes 2 arguments, and invoke was given 1: pass its ReactionEvent after the reaction, as { user, action }. - by default,
invokewarns once with the same words and calls the handler as before, so a handler that never reads its second argument keeps running.
A handler behind
@UseGuard, or behind@Command's check, keeps thelengthandnameit was declared with. See Testing.- under
#596
90034c7Thanks @l7aromeo! - Every service the app makes now gets its lifecycle hooks once, in the bot and inMeoCordTestingModule:- A service that only a guard, interceptor, filter, pipe or the presenter injects is made as the bot comes online, and its
onReadyandonShutdownrun, as@Servicedocuments. Before, it never got its hooks, and it was made anew for every call that reached its stage. It is now the one shared instance@Servicepromises. A constructor that throws in such a service is reported as the bot comes online instead of failing each call. A class that injects the call'sExecutionContextis still made for each call, without hooks. If you worked around the missing hooks by calling an init method from the guard, or by listing the service inservices, you can remove that. - One instance that two tokens reach, such as a factory alias of a service or one value provided twice, runs its
onReadyandonShutdownonce instead of twice.
- A service that only a guard, interceptor, filter, pipe or the presenter injects is made as the bot comes online, and its
#610
44a9a8eThanks @l7aromeo! -@Commandandroute()warn about a customId pattern with a brace pair that isn't a param, such asprofile/{café}: a param's name is ASCII letters, digits and_, so the pair is matched as literal text and a click onprofile/123never reaches the handler. The pattern matches as before; rename the param, such as to{cafe}, to capture the segment.#573
c88e7a2Thanks @l7aromeo! -t.localizations()keeps the default locale's wording where Discord would show another locale's. Discord shows en-US users the en-GB value, en-GB users the en-US one, and es-419 users the es-ES one when their own locale has none. So when your default is en-US, en-GB or es-419 and its partner has a translation, the result now includes the default's own message too. A bot with such a pair re-registers its commands once, and a test that pinst.localizations()for one sees the default's entry.An empty translation,
'', which translation tools export for an untranslated string, now counts as missing:t.localizations()leaves it out, so Discord shows the default there, and a bot with one now starts instead of stopping at startup;- a reply falls back to a related locale or the default, where it was empty;
expectCompleteCatalogreports it as missing.
See Localisation.
#591
31760bfThanks @l7aromeo! - Undermeocord start --devwith process sharding (sharding: { mode: 'process', development: true }), a shard that can't log in is reported as a failed login again: watch mode says the bot could not log in and starts it again on the next change insrcor.env, where it reported a plain exit with code 1. Each app now decides once, as it is created, whether its process is a shard, before discord.js'sShardingManagerwrites the variable that marks one into the manager's own process. Nothing to change in your bot.#602
85ff84aThanks @l7aromeo! - AMemoryCooldownStorethat nothing uses any more is garbage-collected once its calls have expired. Its sweep timer held it for as long as the process ran, so a test run kept one store per testing module that dispatched a cooldown. The sweep now stops when it leaves the store empty and starts again on its next call; counting is unchanged.#603
eee2287Thanks @l7aromeo! -@MeoCord({ providers })now warns about a provider forThemeCacheorExecutionContext, tokens MeoCord binds itself: aThemeCacheprovider silently replaced the app's theme cache. The bot still starts; the next major version (5.0) refuses such a provider. Remove it. ATranslatorprovider withouti18nstays supported, andMeoCordTestingModule's providers are unchanged.#609
0ef1597Thanks @l7aromeo! - A mock thread'screatedTimestampandcreatedAtare read from its id, as another channel's are, in both modes.createdTimestampread a stub andcreatedAtan Invalid Date.Under
useStrictMocks(), a user, server or channel made with a generated id was created when the mock was made, as a message and an interaction already were. That coverscreateMockUser(),createMockGuild(),createMockChannel()and an interaction's own user, so a test of an account-age check gets a new account.SnowflakeUtil.timestampFrom(id)still reads the id's own time.In default mode their creation time is still their generated id's, a fixed day in 2025. Reading it warns once, saying to give the mock an id or a
createdTimestamp, or to calluseStrictMocks(). An id the test gives decides the time in both modes. The next major version (5.0) reads the time the mock was made without the call.#594
111acddThanks @l7aromeo! - Mocks give the values discord.js always sets where they gave stubs without methods, and the warnings about placeholders name the right reader.- Calls that threw now work, in both modes.
String(interaction)on a slash command writes/settings email address:a@b.c, as discord.js does. An interaction made withoutoptionshas an empty resolver.message.mentions.has(user, { ignoreRepliedUser: true })answers.mentions.parsedUsersandmentions.crosspostedChannelsare Collections.- These are empty collections and bitfields:
message.reactions,message.messageSnapshots, a reaction'susers, a member's and a user'sflags, an interaction'sentitlements, and a server'spresences. A reaction'susers.fetch()resolves an empty Collection. - A manager's lookups that read discord.js's own cache, such as
guild.presences.resolve(id)and a thread'smembers.resolve(id), find what itscacheholds, elsenull. - So in default mode too, these fields hold discord.js's types instead of stubs. A test that relied on a stub's shape there sees the real value.
- Under
useStrictMocks(), data reads as discord.js gives it.- A reaction's
meisfalse,mentions.repliedUserisnull, and a modal'smessageisnull. - A message's
editedAtcomes fromeditedTimestamp. A member'spresenceis the one its server caches, elsenull. A server'sverifiedcomes from itsfeatures.systemChanneland a channel'sparentare the channels their ids name, elsenull. - A voice or stage channel's
fullis computed. - A reaction made without a message has a whole one of its own, so a dispatched reaction is neither warned about nor fetched.
- A reaction's
- In default mode, values are unchanged. Reading one of those values, or a voice channel's
full, logs a warning once. The warning says what to set, or to calluseStrictMocks().- When MeoCord's dispatcher reads a reaction's placeholder
partial, the warning says so. It also says the reaction was fetched because of it, and to setreaction.partial = falseandreaction.message.partial = false. mentions.has()readsrepliedUserwithout a warning, since that placeholder matches no user.
- When MeoCord's dispatcher reads a reaction's placeholder
- Under
useStrictMocks(), a test that assertedreaction.fetchormessage.fetchwas called during dispatch changes. A whole mock reaction is no longer fetched. The next major version (5.0) reads these values as strict mocks do.
- Calls that threw now work, in both modes.
#597
eef4c72Thanks @l7aromeo! - A mock autocomplete's focused option reads as Discord sends it: a string.getFocused(),getFocused(true).valueand the option's entry inoptions.datanow agree, anddatamarks itfocused.- With no value given, as before anything is typed, the focused option reads
''instead ofnull, in both modes. A test that checked fornullchanges; discord.js's types rulenullout. - The focused option is now in
options.data, as Discord sends it, so with no value given an autocomplete handler's params are{ query: '' }rather than{}, in both modes. A test assertingtoEqual({})there changes. - A number given for it reads as its digits under
useStrictMocks(), keeping its option type. In default mode it stays the number and warns once when read. The next major version (5.0) gives the string without the call.
- With no value given, as before anything is typed, the focused option reads
#617
9e10a4eThanks @l7aromeo! - A mock interaction holds each message it sends as it was sent, as Discord does:followUp()resolves to the follow-up it sent, with the content, embeds, components and flags thatfetchReply(id)reads back, as discord.js resolves it to the message Discord returns. It used to resolve to a message holding only the id, with a stubcontentand flags of 0, so code reading whether a follow-up was private saw a different value than against Discord.reply(),update()andfollowUp()build what they send when they are called, so a builder changed afterwards no longer changes whatfetchReply()reads. Reading a follow-up no longer uses up a generated id.- Under
useStrictMocks(), an answer whose components or embeds discord.js refuses to build, such as a button with no label, rejects at the call with discord.js's error and leaves the interaction as it was. In default mode it still resolves, and it warns once. The next major version (5.0) rejects it without the call.
#638
08b990aThanks @l7aromeo! - Every id a mock generates comes from one count, so a test run gives the same ids each time. A server'sownerId, a role made without an id, an interaction'scommandIdandapplicationId, a thread'sownerIdand a channel'sguildIdfallback were time-based snowflakes, which differed between runs and sorted apart from every other mock id. A test that matched one of them against a time-based snowflake reads a counter id instead.A role made with
createMockInteraction(Role)and no id gets its id when it is made, as a user does. ItscreatedTimestampis still the time the mock was made, in both modes.In default mode, a reply read again, through
fetchReply()or aseditReply()resolves it, has the same author, server and channel each time, where each read made new ones. A test comparing two reads of the same reply sees the same objects, and reading it again uses up no ids.#607
542cd81Thanks @l7aromeo! - UnderuseStrictMocks(), a mock interaction is in the place itsmessage,guild,memberorguildIdgives it, as Discord sends it:- An interaction given a
messageis in the message's channel and server. A button built on a defaultcreateMockMessage()is in that message's server, no longer a DM. When the test also gives the interaction achannel,guildorguildId, a message whose servercreateMockMessage()made itself moves to that place instead. - An interaction given a
guild, or amemberof a server, is in that server, with itsguildId. - An interaction given a
guildIdalone is in a server the bot isn't in. Itsguildandchannelarenull, and itsmemberis a raw member for its user. - A
channel,guildorguildIdthat is somewhere other than a message the test placed, by giving it achannelorguild, is refused, naming both.
In default mode, placement is unchanged. Where strict mocks would place the interaction elsewhere, reading its
guildId,guild,channelId,channelormemberwarns once, saying what to give or to calluseStrictMocks(), and so doinGuild(),inCachedGuild(),memberPermissionsandappPermissions, each under its own name, where strict mocks would answer otherwise. The same goes for the message, where strict mocks would move one whose placecreateMockMessage()made. The next major version (5.0) places interactions this way without the call.- An interaction given a
#618
41f908fThanks @l7aromeo! - UnderuseStrictMocks(), a mock interaction'smemberPermissionsapplies its channel's permission overwrites, as Discord computes it, so a guard that checks it denies in a test where it denies in production. The overwrites go on top ofmember.permissions, so a value a test sets there still decides the base. As in discord.js:- an Administrator or the server's owner has every permission,
PermissionsBitField.All; - a thread takes its parent's overwrites;
- a raw member's permissions are kept as given.
In default mode
memberPermissionsis still the member's own permissions. Where strict mocks would read otherwise, because of the channel's overwrites or an Administrator's or owner's every permission, reading it warns once, naming the permissions strict mocks read without or with. It, andappPermissions, also warn once where the server's @everyone role has no permissions set, since strict mocks give @everyone the permissions Discord gives it in a new server; setguild.roles.everyone.permissionsto read the same in both modes. The next major version (5.0) applies the overwrites without the call.- an Administrator or the server's owner has every permission,
#613
6f54ab0Thanks @l7aromeo! - A mock message'scomponentsandembedsare discord.js's classes, as a real message holds them.createMockMessage({ components, embeds }), and the messageseditReply()andfetchReply()return, used to keep each one as a bare{ toJSON }, somessage.components[0].typeandmessage.embeds[0].titlereadundefined.Each one is now built from its JSON at the time of the call, whether it was given as a builder or as API JSON:
- an action row is an
ActionRowholding aButtonComponent, a select menu component, and so on; - Components V2 are
ContainerComponent,TextDisplayComponentand the rest; - an embed is an
Embed; - a type discord.js doesn't know is a plain
Component, as discord.js builds it.
A discord.js instance you give is kept as it is.
toJSON()still returns the same JSON, so no passing test changes.- an action row is an
#636
379932fThanks @l7aromeo! - What a mock computes for itself no longer calls a method a test may stub, or caches anything a test can see.- A mock message's
memberreads its server's member cache, as discord.js does, without callingguild.members.resolve(). A test'smembers.resolve.mockReturnValueOnce()is left for its own code. - A mock interaction's
appPermissionscomputes the bot's permissions with discord.js's own computation, without callingchannel.permissionsFor(). Reading it no longer caches the bot's member inguild.members.cache. In a thread, it is the bot's permissions in the thread's parent, as Discord gives them, where it was empty. - A mock interaction's
channelis the channel its server caches underchannelIdeach time it is read, so a channel a test caches there after a first read is the one read.
- A mock message's
#590
54d76f9Thanks @l7aromeo! - A mock interaction keeps its reply state, its original response and discord.js's errors whatever a test sets its answers to do.- A test's own value. An answer given a value with
mockResolvedValue,mockResolvedValueOnceormockImplementationnow replies or defers as a real one would:reply,deferReply,followUp,editReply,showModal,updateanddeferUpdate.getResponse(interaction).statethen reads'replied'or'deferred'where it read'unanswered'. One that rejects still changes nothing.- A second answer after such an answer, which discord.js refuses, still runs in default mode, with a warning.
- Under
useStrictMocks()it is refused, as discord.js refuses it. The next major version (5.0) refuses it without the call.
- The original response.
fetchReply()reads back whatreply()orupdate()sent instead of an empty message. AfterdeleteReply(), fetching, editing or deleting the original response rejects with 10008 (Unknown Message), as Discord answers. So does fetching it before any answer. Follow-ups stay reachable by their id:editReply({ message: id }),fetchReply(id)anddeleteReply(id)act on the follow-up, whereeditReplyedited the original before. - Flags.
flagsgiven as an array, a name or aMessageFlagsBitFieldmakes a reply ephemeral, as in discord.js; only a number did before.nullflags are none. A bigint or an unknown name rejects with discord.js's RangeError, where a bigint read as ephemeral before. - Errors. A second reply, an answer before any reply and a second autocomplete
respond()throw discord.js'sDiscordjsError, with the codesInteractionAlreadyRepliedandInteractionNotReplied. They keep their messages, so a test matching the message as a string or a regex keeps passing. One comparing withnew Error(message)sees the new name and code.
- A test's own value. An answer given a value with
#582
3279286Thanks @l7aromeo! - meocord's mocks now work with each test runner's own mock functions and matchers.- Under
useMockFn(jest.fn), a method of acreateMock()double that has no implementation set returnsundefined, as jest's mocks do. It no longer overflows the stack. Nested methods, and methods whose name starts with_, stay mocks as before. - jest's call matchers, such as
toHaveBeenCalled, read acreateMock()member as a mock, whether or notuseMockFnis set. They took it for a jasmine spy and failed withCannot read properties of undefined (reading 'map').- To get this, a nested member named
callsno longer makesallandcountfor itself:x.y.calls.allandx.y.calls.countreadundefineduntil a test sets them. - Every other name under it, such as
x.y.calls.foo, is still a nested mock, and acallsmember at the root,x.calls.all, is a mock as before.
- To get this, a nested member named
- Without
useMockFn, Vitest'stoHaveResolved,toHaveResolvedWith,toHaveResolvedTimes,toHaveLastResolvedWith,toHaveNthResolvedWith,toHaveBeenCalledBeforeandtoHaveBeenCalledAfterwork on meocord's own mocks.mockgainssettledResults,invocationCallOrderandcontextsfor them. The new fields stay out ofmock's keys, so a test comparingmockas a whole sees the same shape. Call order is counted across meocord's own mocks, so compare two of meocord's mocks, or two of the runner's, rather than one of each. - A meocord mock whose implementation returns a promise hands back a promise that settles the same way, not the same object.
expect(fn()).toBe(promise)no longer holds;toEqual,toHaveReturnedWithand awaiting the result are unaffected. A property set on the given promise, such as acancelmethod, isn't carried over. useMockFn(mock.fn)under node:test gives meocord's refusal, which says to leave meocord's own mock function in place, with node's error as itscause, instead of node's internal TypeError.
- Under
#625
99d9339Thanks @l7aromeo! - Messages a mock sends or fetches are where they belong and hold what was sent:- A message from a channel's
send(), a message'sreply()orforward(), or an interaction'sfollowUp(),editReply()orfetchReply()holds the content, embeds and components of the call that made it. It is in the channel it was sent in and that channel's server. Before, it sat in a generated channel of a generated server, with a stubcontent. - A message's
edit()changes that message and resolves to it, where it resolved to a new one.crosspost()resolves to the message itself. - A message fetched with
messages.fetch()is in the channel it was fetched from, with an author other than the bot. - A role made with
roles.create()or fetched withroles.fetch()is in its server. A role, channel, thread or application command a manager'screate()makes is in that manager's cache, as discord.js caches it.bans.create()resolves to the member, user or id it was given and caches nothing, as discord.js's does.
Under
useStrictMocks(), a message the bot sends is itsclient.user's, so itseditablereadstrue. One sent in a DM is in that DM:inGuild()isfalse, andguildandmemberarenull. In default mode these keep today's values and warn once when read: the author of a message the bot sent, and the server of one sent in a DM. The next major version (5.0) reads them as strict mocks do.- A message from a channel's
#623
46e974bThanks @l7aromeo! - A mock message context menu's target is itstargetMessage, andtargetIdreads that message's id. Without atargetMessage, the menu gets a generated id and a message made for it, as a user context menu already did. That message is in the menu's channel and its server, or in the user's DM channel for a menu used in a DM. SettingtargetMessageafter the mock is made movestargetIdwith it, and settingtargetIdmakes a message for the new id. AtargetMessagewith no id gets a generated one. If atargetIdis given that differs from thetargetMessage's id,useStrictMocks()refuses it, naming both. In default mode it warns once and reads the message's id.A user or an attachment made with
createMockInteraction(User)orcreateMockInteraction(Attachment)has a snowflakeid, from the same count as every other mock's. Before, the id was a stub that stringified to[object Object]for every one of them: two users shared a cooldown key, a mention read<@[object Object]>, andcreateModalFieldskeyed uploads by a stub. A user made this way gets its creation time the same waycreateMockUser()'s does: the time the mock was made underuseStrictMocks(), and its id's time, with a warning, in default mode.#593
449bfd4Thanks @l7aromeo! -respond().original.componentsis typed as the message's top-level components,readonly APIMessageTopLevelComponent[], rather thanreadonly unknown[], so a handler can hand them back tosend(),editReply()or a builder without a cast, as it already can theembedsbeside them. Reading them compiles as before. A cast to an unrelated type, such asRecord<string, unknown>[], now fails to compile; drop it. Code that builds its ownResponseState, such as a test double, withunknown[]components inoriginalmust type them as Discord'sAPIMessageTopLevelComponent[].#576
9cf754fThanks @l7aromeo! - A customId pattern that repeats a param name, such ascard/{id}/{id}, or starts one with a digit, such ascard/{1}, is refused in words that name the param:Invalid pattern "card/{id}/{id}": {id} appears twice; give each param its own name.These patterns were already refused, with the regular expression engine's error and the compiled regex in place of the pattern, so only the message changes, for@Commandandroute()alike.#574
793df0fThanks @l7aromeo! - A command, an option or a modal field named like a member ofObject.prototype, such asconstructor,__proto__ortoString, now routes like any other name. Declaring@Command('constructor', …)used to throwmetas.findIndex is not a function, and an undeclared command of such a name, say one left registered, failed with an error log instead of "Command not found!". An option or a modal field named__proto__now reaches the handler as an own param, and a customId param namedconstructoris no longer reported as colliding with a field that doesn't exist. Nothing to change in your code.#614
f1346aeThanks @l7aromeo! -createMockInteraction's types now refuse achannelbeside a raw member (createMockRawMember()), as the mock already did when built. A raw member is from a server the bot isn't in, where discord.js caches no channel. That covers a server's channel,channel: null, a DM channel, and a channel besideguild: null. These calls compiled and then threw "leave the channel out" at runtime.{ guildId, member: createMockRawMember() }still gives a raw-server interaction.#624
405004fThanks @l7aromeo! - A service whoseonReadyis still running when the bot stops, throughapp.stop()or a signal, now gets itsonShutdownonce thatonReadyfinishes, so what it opened, such as a connection pool, is closed. Shutdown waits for it within the sameshutdownTimeout, leaving theonShutdownhooks their reserve. AnonReadystill running then is named in a warning, and shutdown goes on without it. AnonReadythat callsapp.stop()itself isn't waited for.A testing module's
close()duringinit({ ready: true })waits for theonReadyhooks within itsshutdownTimeoutin the same way, warning about one that never settles rather than hanging, and anonReadythat callsclose()no longer waits for itself.#601
d368e8dThanks @l7aromeo! - Command registration keeps in step with what Discord holds:- A development start no longer skips a scope as unchanged when the commands were removed, changed or registered again since, from this checkout or another: it checks the scope with one listing first, and sends again when any command's version differs from what it last sent. A scope that
clearOthercleared, or that a production run wrote, from the same checkout is sent again without the check. Before, only--force-registerbrought such commands back. meocord register --guildwith a blank guild, as an unset--guild "$DEV_GUILD_ID"gives, stops with an error and exits 1, where it registered tocommands.guildsor globally. That matches whatcommands.guildsalready does for an unset id.- The warning about commands left in another scope says why they are kept during a development-guild run, instead of advising
commands.clearOther, which doesn't apply there. - The registered-commands table lists only subcommands and subcommand groups under Sub-commands, not a command's plain options.
- A development start no longer skips a scope as unchanged when the commands were removed, changed or registered again since, from this checkout or another: it checks the scope with one listing first, and sends again when any command's version differs from what it last sent. A scope that
#572
dc7cdb9Thanks @l7aromeo! - Under process sharding, a call to every shard now reaches each one for any shard count:ShardContext.broadcastEval, and discord.js'sclient.shard.broadcastEval()andclient.shard.fetchClientValues(). They were refused with "Shards are still being spawned." unless the bot ran exactly 4 shards. Nothing to change in your bot.#619
3517d1aThanks @l7aromeo! - A process that loads meocord both as an ES module and throughrequire(), such as an ES module bot with a CommonJS helper, a jest CommonJSsetupFilesbeside ES module specs, ornode --require ./setup.cjs, now behaves as one:UserError,GuardDeniedError,ValidationErrorand meocord's other errors from either build are answered as what they are, andinstanceofholds across the two. An app's own subclass still matches only its own instances.- A handler's
@UseThemetheme, the app's translator for meocord's own texts, and its presenter apply whichever build answers. respond()keeps one answer for an interaction, so asend()after adelete()from the other build follows up rather than editing the deleted reply, andgetResponse()reads a mock's answers whichever build made the mock or answered it.useStrictMocks()anduseMockFn()called in a setup file in one format apply to mocks made in the other, and the two never give two mocks one id.
The two builds share this state only within one installed version. It now outlasts
vi.resetModules(),jest.resetModules()and jest'sresetModulesconfig, where it was made afresh: mock ids keep counting, anduseStrictMocks()anduseMockFn()called in a setup file stay in effect for every test.#587
f0a9ff3Thanks @l7aromeo! - Shutdown now waits for the calls under way before theonShutdownhooks, in every bot, and stops taking events first:- A handler that awaits
app.stop(), as an owner-only shutdown command does, no longer holds the shutdown up untilshutdownTimeout. It isn't waited for. - No interaction, message, reaction or
@On/@Onceevent starts a call once shutdown has begun, and an@On/@Oncehandler already running is waited for, as a dispatched call is. Before, a bot whose cooldown store has noonShutdownkept dispatching while its hooks ran. - An
@On('error')handler keeps receiving the client's errors until the client is destroyed. shutdownTimeoutstill bounds the whole shutdown. The calls are waited for no longer than the timeout less a reserve kept for the hooks: a quarter of it, at least 1 second and never more than half. A call still running then is named in a warning, and the hooks run in the time left.
A
stop()while the bot starts now waits for the providers being made, at mostshutdownTimeout, and makes nothing more after it, so no service is constructed once the bot has shut down. A factory still pending aftershutdownTimeoutis named in a warning.MeoCordTestingModule.close()duringinit()likewise waits for the providersinit()is making, and closes them.- A handler that awaits
#616
8ea2acfThanks @l7aromeo! -overrideGuard,overrideInterceptorandoverrideFilterreplace a stage that is also a provider. A guard, interceptor or filter listed inproviders, the app's or the testing module's, made every call fail withAmbiguous bindings found for serviceonce it was overridden. Now the stub stands in for it, and wins over anoverrideProviderof the same class. It runs theonReadyandonShutdowna provider would; a stage applied only with@UseGuard,@UseInterceptoror@UseFilterruns none, as on the bot.compile()also checks each stub for its stage's method,canActivate,interceptorcatch, on the stub or its prototype. UnderuseStrictMocks(), a stub without it is refused withoverrideGuard(G).useValue(…) has no canActivate method.By default,compile()warns with the same words, once per stub and method, and builds the module, so a test that never reaches the stage passes as before. See Testing.#604
f30b621Thanks @l7aromeo! - A theme emoji may be the England, Scotland or Wales flag, as theming documents for flags, where the bot stopped at startup calling it "not an emoji".#606
e0d3300Thanks @l7aromeo! - The warning for athemeForresult the theme can't use shows each value as other log lines show outside text: in double quotes, with control characters and line breaks escaped, and cut short after 200 characters. A role name in it is escaped too. A malformed saved value no longer spans several lines. The startup refusal of a theme written in code reads as before.#592
b0bcb45Thanks @l7aromeo! -meocord.config.tsand the build readtsconfig.json'spathsastscdoes, whatever TypeScript version the project uses: paths inherited throughextends, paths relative tobaseUrl, and${configDir}inpaths,include,excludeandfiles. A config that imports through such a path now loads, rather than failing with "Cannot find module", and a${configDir}path no longer breaks the build.#586
6fe54a9Thanks @l7aromeo! - AUserError,GuardDeniedErrororValidationErrorwhose message is empty, or longer than Discord takes, is answered as the user's own outcome again, instead of failing as a bot fault with the generic danger-coloured answer and two error logs. The message is fitted before any presenter sees it, your own included:- at most 4096 characters for an embed and 4000 for a Components V2 Text Display, title line included, cut to end in
…; - an empty or whitespace-only message becomes MeoCord's generic error text.
A message command's plain-text reply is cut to 2000 characters, its warning emoji included.
PresentedError.messagedocuments the limit; a presenter that writes the message as its text needs no change.- at most 4096 characters for an embed and 4000 for a Components V2 Text Display, title line included, cut to end in
#488
b29a23cThanks @l7aromeo! - AUserErrorthrown from a message command or an@Onmessage event is answered with a reply that pings no one its text mentions, as MeoCord's other replies to messages already do. Users, roles,@everyoneand@herein the error's message are shown as written. Nothing changes for a bot whose error messages hold no mentions.