4.1.0 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.
Highlights
Adds to 4.0. Upgrading from 4.0, below, lists what may change for a 4.0 bot (docs).
One way to answer.
respond(),@Defer()and presenters, in a theme's colours and emojis (docs).A pipeline around every handler. Global guards, interceptors, filters, pipes,
@Cooldownand observers, in one order (docs).Message command patterns. Typed params, flags, aliases, usage replies and a built-in
!help(docs).Sharding, providers and lifecycle. Sharding in one process or a process per shard, with
ShardContext.call, async providers,onReady/onShutdownandapp.stop()(docs).Localisation and testing. Typed catalogs, and
MeoCordTestingModulewith Discord-shaped mocks (docs).
Upgrading from 4.0
- Breaking
Requirements. Node.js 22.13+, discord.js 14.27.0+ and dotenv 18.0.5+:
npm install discord.js@^14.27.0 dotenv@^18.0.5. Most 4.0 bots and tests run without edits (guide). - Breaking
Class guards cover inherited and autocomplete handlers. Check guards that read command-only data or reply on denial; an autocomplete denial closes the menu (guide).
- Breaking
Base class stages cover subclasses. A base controller's guards, interceptors, filters and cooldowns run first on its subclasses' handlers;
@Controller({ inheritStages: false })opts out (guide). - Breaking
applyDecorators(A, B)applies in stack order. Guards run in the order listed; reverse them to keep 4.0's order (guide). - Breaking
Message keywords ignore case, and one handler runs. They match word by word; set
caseSensitive: trueto match case. Two handlers on one keyword stop the bot, and an appprefixapplies to keywords; use{ prefix: false }for bare messages (guide). - Breaking
Bot reactions are ignored, the bot's own included. Add
{ bots: true }to a@ReactionHandlerthat needs them, and recheck counts that subtracted the bot's own reactions (guide). - Breaking
Duplicate routes stop the bot. Two component handlers matching the same customIds, two handlers of one command, or two builders of one command (4.0 warned) are refused, naming both. Registering a base controller with its subclass is refused the same way (guide).
- Breaking
A re-declared inherited route takes the subclass's options. Its own builder or
@MessageHandler/@ReactionHandleroptions apply; to keep the base's, don't re-declare it (guide). - Breaking
Errors after a reply or deferral are answered. A deferred reply becomes the error, and a replied command gets a private follow-up; an exception filter replaces it (guide).
- Breaking
Themefollows the app's theme, with new defaults, as do MeoCord's own answers:Theme.errorColoris#E3606D, not#DC3545, and the success, info and warning colours change too. Set@MeoCord({ theme: { colors } })to keep 4.0's (guide). - Breaking
MeoCord's metadata keys start with
meocord:.SetMetadatarefuses only those and the DI keys (inversify's injectable flag,design:paramtypes); read MeoCord's metadata withinspectHandler(guide). Activities rotate in order, only when set. Without
activities, MeoCord leaves the presence alone; with them, it starts at ready.Reactions use the cached message.
reaction.messageis fetched only when held by id alone; callreaction.message.fetch()for fresh data.customId params arrive decoded.
%2Fand%25reach the handler as/and%; drop your own decoding.Logs and exit codes.
[DEBUG]prints only in development unlesslogLevelorMEOCORD_LOG_LEVEL=debugasks;info()/verbose()print[INFO]/[VERBOSE], objects asconsole.logdoes, and a signal after a failed login exits 1. Skip it in a 4.0main.tscatch withif (!isExplainedError(error))frommeocord/common, or a refused token or intent logs twice (docs).
Upgrading builds and the CLI
- Breaking
A mistyped config option stops
build,startandregister, listing every problem; unknown options only warn (guide). - Breaking
Config and assets load beside
dist/main.js..envstill loads from the working directory, so set it there when starting elsewhere. Rebuild (guide). - Breaking
NODE_ENVfollows the mode.build --prodcompilesmeocord.config.tsas production, andstart --devruns the bot as development whatever the shell sets. On Bun, start production withNODE_ENV=production, or.env.developmentloads first, with a warning (guide). No
evaldevtool. Development builds usecheap-module-source-map, andeval-*becomes its non-eval form with a warning. SetsourceMappedStacks: falsefor trackers that apply uploaded source maps (docs).- Breaking
meocord/eslintflags unawaited promises.await,returnorvoidthem, or turn the rule off (guide). - Breaking
The generated rate-limit guard never limited. Move its map to module level, or use
@Cooldown(guide). - Breaking
What a 4.0 app can copy. New apps read every
.envfile and type asset imports; add theswcincludefor coverage of untested files. An app created for pnpm addsreflect-metadata,@types/nodeand apnpm-workspace.yaml, and one installed with npm 11.16+ addsallowScriptstopackage.json(guide).
Upgrading your tests
- Breaking
Mocks have distinct snowflake ids. Pass
{ user: first.user }to share a user. A mock withoutguildIdis a DM, a given channel setschannelId,guildIdandguild, and a member given tocreateChatInputOptionsfills aUserparam with theUser, where 4.0 passed theGuildMember(guide). - Breaking
Handler arguments are type-checked. Select menu choices and plain-string customId params that never matched now fail to compile (guide).
Changed messages. Load-time refusals start with
Class.method:, and a@Commandgiven the wrong interaction names the handler, not "Invalid interaction type passed to @Command". A modal mock'sisFromMessage()needs amessage, and autocompleterespond()rejects over 25 choices.Option mocks resolve as Discord's.
getMember()returns the member,nullin a DM. A getter of another type,getInteger()on a fraction (usegetNumber()),getChannel()on a channel type the option doesn't allow, orgetSubcommand()with no subcommand throws discord.js's error, where 4.0's mock returnednull(getSubcommand(false)reads none asnull); only a user or member read as a role, or the reverse, staysnull.Load
.envin tests yourself, in the vitest setup or withdotenv, if a suite relied on an earlier build for it.
Responses and presenters
respond(interaction)answers any interaction: acknowledge, send, edit, follow up or report an error as its state allows, in user-installed apps too (docs).@Defer()acknowledges before guards run, then locks the clicked message's controls under a loading view until the handler answers;mode: 'auto'defers only when needed (docs).Presenters draw MeoCord's own answers:
@MeoCord({ presenter })styles loading, error and help views, for messages too, async or not, with files; a failed drawing still answers (docs).Unanswered handlers are named in a development warning, which
@MeoCord({ warnUnanswered })toggles.
Handlers and the call pipeline
One fixed pipeline runs every handler: guards, then interceptors around validation, pipes, cooldowns and the handler, inside exception filters (docs).
Guards can be global, in
@MeoCord({ guards }); they read typed facts throughExecutionContextandcreateMetadata, and throwGuardDeniedErrorto say why (docs).Interceptors wrap a handler, for timing, logging, caching or mapping errors (docs).
Exception filters and
UserErrordecide what the user sees when a call throws; aUserErroranswers privately, or in a reply that doesn't ping (docs).@Validateand pipes check input with any Standard Schema library and reshape it (docs).Typed options: each
{ provide, params }is checked against the params its stage declares, andMeoCordOptionsnames what@MeoCordtakes.Observers hear each call start and settle, with its outcome and duration (docs).
@Onand@Oncehandle any discord.js event, on a controller or a service, through the same pipeline (docs).HandlerRegistrylists every handler with its metadata (docs).Startup checks name dead handlers, shadowing handlers or builders, and missing intents or partials before login; a refused app gets one line naming
Class.methodand exits 1.
Theming
Themes name colours, emojis and button styles by role, set with
@MeoCord({ theme }),@UseThemeor per server and user withthemeForandThemeCache;useTheme()reads them, and apps add roles (docs).ThemeResolverclasses look themes up with the app's services:themeFortakes one, resolved from the container, cached as the functions are (docs).respond()fills in colour: an answer without one takesprimary, error views takewarningordanger, and{ fill: false }sends as written.
Cooldowns
@Cooldownlimits a handler per user, channel, server, everyone or abyvalue; cooldowns stack, count only calls that pass guards and validation, and show the wait as a Discord timestamp (docs).Cooldown stores: memory by default,
ShardedCooldownStoreacross shards,RedisCooldownStoreon Redis or Valkey, or your own, checked withtestCooldownStore;cooldownStoreFailuredecides calls while one is down (docs).
Message commands
Patterns:
@MessageHandler('roll {sides:int} {note...?}')matches after a prefix or mention, with typed params such asint,memberor the app's own, flags and lists, checked at compile time (docs).Starts:
messages: { prefix, mention, caseSensitive }sets how commands start;mention: 'only'needs no MessageContent intent.Answers: a misfit gets its usage;
aliases,descriptionandscopedescribe a command,helpadds!help, anddmOnErroranddmOnCooldownDM the author (docs).Fetched after guards: named members, users and channels are fetched only once guards pass, uncached members together; a role is read from the cache.
Components and routing
Typed customId params such as
{count:int}arrive as values, androute()builds matching ids, checked at compile time (docs).Select choices and modal fields arrive in params, with resolved users, members, roles and channels, and file uploads as
Attachments.Context menu handlers are typed from the builder's
setType()(docs).Reactions match a custom emoji by id as well as by name (docs).
Sharding
shardingruns shards in one process or, withmode: 'process', one each; the manager registers once, restarts exited shards, stops when all would fail, and shuts shards down through their hooks (docs).ShardContext.callruns a service method in every shard, each result typed as the JSON it arrives as, in tests too.
Providers and lifecycle
Providers supply values, classes and async factories under any token,
createTokenincluded, injected with@Inject;factoryProvidertypes a factory (docs).OnReadyandOnShutdownrun in dependency order on controllers, services, providers and the cooldown store, withinshutdownTimeout(docs).app.stop()shuts the bot down from code, as a signal does.
Testing
invokeanddispatchrun a handler, or route an interaction, message or reaction, through the pipeline;getResponseshows what was sent (docs).MeoCordTestingModule.fromApp(App)wires the whole app as the bot does, withoverride*();init({ ready: true }),close()andemit()run hooks and events (docs).Mocks behave like discord.js, with members, roles, permissions, channels and locales;
createMockMember,createMockMessageand the rest take properties, a message itsauthor, andcreateMockInteractionDiscord'sauthorizingIntegrationOwnersmap (docs).Checks:
inspectHandler,resolveRoute,expectCompleteCatalog,testCooldownStoreandresetAllMocks().
Localisation
createTranslatorchecks keys,{params}and plurals against the default catalog at compile time;t.localizations()fills builders,t.for()translates replies, and@MeoCord({ i18n })injects it (docs).MeoCord's own texts translate through a
meocordgroup in the app's catalog.
The CLI and builds
Command registration goes global, per server or to a dev server, at startup or with
meocord registerover REST; unchanged dev commands skip resending unless--force-register(docs).meocord createcommits the lockfile, addsnpm start, writes samples whose specs test what they answer, and makes its first commit by running git directly, so MeoCord no longer installssimple-git.meocord generateadds observers, filters, interceptors and pipes, a customId per component, and specs that test the answer.meocord start --devrestarts on source, config,tsconfig.jsonand dev.envchanges, one bot at a time, keeping the last good build while code doesn't compile (docs)..envfiles: a new app reads.env.<mode>.local,.env.local,.env.<mode>and.envon every runtime.Self-contained builds pack native addons from any package manager and run on Bun too;
optionalExternalscovers optional packages (docs).Source-mapped stacks and
logLevel: traces name your source lines in production too, andlogLevelorMEOCORD_LOG_LEVELsets what prints (docs).dist/cli.jsondescribes every CLI command and option as data.
Deprecations
- Breaking
Names removed in 5.0. Each still works and its JSDoc names the replacement. A new app's ESLint config sets
@typescript-eslint/no-deprecatedto warn outside specs, which finds them; add it to a 4.0 app's config.Theme: useuseTheme().colorsand@MeoCord({ theme }); reading or setting a colour warns once (guide).SetMetadataand string metadata keys: usecreateMetadataandExecutionContext.get(decorator); each warns once (guide).ReactionHandlerOptions: renamedReactionEvent(guide).MetadataKey,CommandMetadata,AutocompleteMetadata: internal; drop the import (guide).
@Autocomplete<void>loses its type parameter in 5.0. Lint misses it: search for@Autocomplete<and write@Autocomplete(…).- Breaking
Retrying
start()after a failed login warns, logging in again with its handlers, and rejects in 5.0. Make a new app withMeoCordFactory.createper attempt; retrying after a provider failure stays supported (guide). - Breaking
@Controller,@Service,@Guard,@CommandBuilderor@MeoCordon a method applies nothing, as in 4.0, and warns; 5.0 refuses it. Move it to the class (guide). - Breaking
@MessageHandler('')warns, and 5.0 refuses it. Write@MessageHandler()(guide). - Breaking
Handlers that never run warn at startup, and 5.0 refuses to start:
- Breaking
Changes in 5.0, warned now:
Fixes to 4.0 behaviour
Running the bot
- Retrying
app.start()after a failed login no longer attaches every handler twice. node dist/main.jshas the config's.envvalues before the app's modules run. Rebuild.- A refused privileged intent, or a refused or empty token, is explained in one line with where to fix it.
- A builder that can't be built is named with its command in the registration error.
- A builder on a subcommand path is named with the fix, not "Invalid string format"; one naming its own command still works, with a warning.
- One SIGINT and SIGTERM listener per process: many apps no longer trigger
MaxListenersExceededWarning. - A signal during login stops the bot at once, with "Bot has shut down".
- A missing or broken built config is reported once, with the file, reason and fix.
- Reactions in uncached DMs reach their handlers again on discord.js 14.26.2 and later.
- Retrying
Routing and dispatch
- A click a discord.js collector answers no longer gets "Command not found!".
- A user and a message context menu of one name each reach their own handler.
- Overlapping component patterns are warned about at startup, not the first click.
- A subclass controller no longer adds its handlers to its base class.
Dependency injection
- A subclass with its own constructor gets its own dependencies.
@inject(Token)on an interface-typed parameter works, not "missing metadata on type Object".- An injection cycle is refused naming it, not "Circular dependency found: (No dependency trace)".
- A constructor parameter with no runtime type is refused naming the class and parameter, not inversify's
emitDecoratorMetadataerror. - A class with no decorator whose constructor injects, guards, interceptors, filters and pipes included, is refused as the app starts naming the class and the decorator to add, not inversify's missing-metadata error, which a guard gave only at its first call.
- A guard in
servicesorproviders, or injected into a service, reads each call's ownparams; one with a setter param or a sealed instance shares them, with a warning. - A guarded method called directly on any class the app runs, a provider's class included, or on a subclass of one, runs its guards, not "Cannot read properties of undefined (reading 'get')"; on an instance no app made, it says to inject the class.
Handler types
- A handler may return a value or take fewer parameters, not "Unable to resolve signature of method decorator".
applyDecoratorspasses on what a wrapping decorator returns.
Builds
- Production builds keep class names, where a clash renamed
Shoptoshop_controller_Shop. - Imported files land in
dist/assetsunder their own names; a WebAssembly module takes a content hash. - On Windows,
new URL('./file', import.meta.url)gives afile:URL, not ac:pathfileURLToPathrefused withERR_INVALID_URL_SCHEME. Rebuild. - A
tsconfig.jsonwithextends,files,typeRoots, comments orbaseUrlpaths builds as TypeScript reads it, is never rewritten, and concurrent builds no longer clash. - Self-contained builds run under Bun and pack each package's installed dependency versions, npm-nested and pnpm-store dependencies, and per-platform binaries, fixing "Cannot find module". Rebuild.
- Production builds keep class names, where a clash renamed
The CLI
meocord startforwards SIGINT and SIGTERM, so Docker, pm2 and systemd stop the bot cleanly.start --devruns one bot at a time, restarts it through its own shutdown on Windows, starts it again on the next rebuild after it exits on its own, which one Ctrl+C then stops, and keeps it running when a save doesn't compile or anrsbuildhook throws.start --dev --buildbuilds once, andstart --devexits 1 when watching can't start.createkeeps the app when git can't commit, joins an enclosing Git repository, quotes any app name, and refuses a name with no letters or digits, notDirectory "" already exists.- A created app passes
lintandteston pnpm and installs on pnpm 11+ and npm 11.16+ without warnings;createwarns on Node.js 22.0 to 22.12. generateworks on Windows, writes lint-clean files formatted in one ESLint run, and refuses a name outsidesrc/, with\separating folders on Windows.buildandstart --proddon't clear the screen,start --devkeeps scrollback, and no command writes screen-clearing codes into piped output.--helpno longer prints "No available choices.".- The CLI runs at the filesystem root, not "Cannot locate the "MeoCord" package directory".
require('meocord/package.json')resolves, andmeocord/eslintignorescoverage/.
Logging
Loggerprints any value, aSymbolincluded, and colours a line only when its own stream is a terminal.- Log lines escape what a user sent, and shorten long message text, with its length.
- A project that loads meocord with
require(), as CommonJS code or Jest in CommonJS mode does, logs again: everyLoggermethod threwchalk.bold is not a function. Built bots were not affected. - The CLI and tests no longer read the app's name or
.envfrom a staledist.
Testing
- Mocks have an
'en-US'locale,createdTimestampandcreatedAt, a workinginGuild()and resolving promise methods. - A mock interaction without a
clientgets one fromcreateMockClient, andgetAttachment()returns theAttachmentgiven, ornull. createMockChanneltakesThreadChannel, stubsthreads.createon text, announcement, forum and media channels, and gives a subclass its base's managers.
- Mocks have an
Security
Loggerno longer writes the bot token to logs (GHSA-62w2-fp4p-4jq8). In 4.0.0, a discord.js object a bot logged throughLogger, such as an interaction, a message or the client, printed with every property, the token included. 4.0.1 fixed it, and 4.1.0 has the fix:Loggerprints objects asconsole.logdoes and replaces the token with[redacted]. Coming from 4.0.1, nothing changes. Coming from 4.0.0, if a bot logged such objects and others can read its logs, reset the token in the Discord Developer Portal.