4.1.0-beta.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.
Minor Changes
#66
5b49d3eThanks @l7aromeo! - Choose where commands are registered, and register without starting the bot.commandsinmeocord.config.ts:guildsregisters to guilds instead of globally,developmentGuildsends everything to one test guild understart --dev,register: falseleaves registration tomeocord register, andclearOtherremoves commands left in a scope no longer used, which are otherwise reported as a warning; a development run sending todevelopmentGuildonly warns, since production may share the application.@CommandBuilder(type, { guilds })keeps one command in its own guilds, for staff commands.meocord register [--build] [--dev] [--guild <id>]registers over REST and exits, without logging in, and exits non-zero on failure.- In development, a scope whose commands are unchanged since the last start is not sent again;
meocord start --dev --force-registersends it anyway. - Commands are read from the controllers' prototypes, so registering constructs no controller.
With no
commandssetting, an existing bot still registers every command globally at each start. One thing does change: a builder whosetoJSON()throws, such as a slash command missing its description, now stops that start's registration. You see an error naming the builder, and no commands are sent, where before the rest were registered and the broken one was dropped. A bulk update without it would delete it from Discord. Fix the builder and the next start registers everything.New applications get
developmentGuildwired toDEV_GUILD_IDin.env.#76
6bfe0e3Thanks @l7aromeo! - Limit how often a handler runs with@Cooldown.@Cooldown({ seconds, uses, per, bypass })on an interaction or message handler, or a controller, allowsusescalls withinseconds(a sliding window), counted per'user','guild','channel'or'global'. Stack several for layered limits: each call counts against every one of them in order, so a call a later limit blocks has still used the earlier ones; put the shortest window first.bypassexempts callers such as owners. Counts are kept under the class name, so two same-named classes are refused at startup when either has a cooldown or a@Oncehandler.- It is counted after guards, validation and pipes, so a denied call or bad input spends nothing. A blocked call throws
CooldownError(frommeocord/common), which the built-in fallback answers only to the caller withcooldownMessage(): "Slow down: try again in 12s." - Calls are counted in memory by default.
@MeoCord({ cooldownStore })takes a class extendingCooldownStore, such as one on Redis, to share the count across shards and processes; with process sharding and the in-memory store, the bot warns that'user'and'global'cooldowns count per shard. inspectHandler(...).cooldownslists a handler's cooldowns.
#81
017ec6cThanks @l7aromeo! - Add@Defer(), which acknowledges an interaction for its handler in two steps: a deferred reply (or an invisible deferred update, for a component) before guards run, so slow guards and handlers never miss Discord's three seconds; then, once guards, validation and pipes allow the call, a lock on the component's message — its controls disabled, the clicked button showing the loading emoji, the presenter's loading view added.respond(interaction).send()withoutcomponentsputs the message back as it was, including buttons that were disabled on purpose, and a handler that never answers has it put back when it returns. A guard that returnsfalseleaves nothing behind, and one that throwsGuardDeniedErroris answered privately.Options:
ephemeral,disable('all','clicked'or'none'),mode: 'auto'to acknowledge only when the handler has not answered afteraftermilliseconds (1500 by default, and never later than 2.5 seconds after the interaction was created), andsuppressNotifications.@Deferon a message, reaction, event or autocomplete handler throws.#69
f00309fThanks @l7aromeo! - Add exception filters, which decide what happens when a handler, its interceptors or its guards throw.@Catch(ErrorType, ...)marks a class implementingExceptionFilter:catch(error, context)receives the error and the call'sExecutionContext. With no types, it handles every error.@UseFilter(...)applies filters to a method or a controller, and@MeoCord({ filters })to every handler. The method's filters are tried first, then the controller's, then global ones; within one level, the first whose@Catchmatches.{ provide, params }passes options, read withcontext.getParams(). One instance serves every call.- An interaction no handler matches raises
CommandNotFoundError, which global filters receive with no handler in the context. GuardDeniedError, thrown from a guard, denies with a message the built-in fallback shows only to the user who made the call. Returningfalsestill denies silently.- An error no filter handles goes to the built-in fallback, which logs it and answers the user: "An error occurred while executing the command.", or "Command not found!".
- Testing:
overrideFilter(Class).useValue(stub),inspectHandler(...).filters, andMeoCordTestingModule.create({ app })includes global filters.invokeresolves witherrorset when a filter handled one, and rejects with an error no filter handles, since the fallback does not run in tests. meocord generate filter <name>(aliasf) writes a filter, the error it handles, and a spec.
#60
f3ea0dfThanks @l7aromeo! - Add typed handler metadata andExecutionContext, so guards can read what they guard.createMetadata<T>(description)inmeocord/commonmakes a typed decorator for a controller or a handler, with a unique key.ExecutionContextinmeocord/commondescribes the handler call being run. A guard receives it by constructor injection and reads metadata withcontext.get(Roles), where the handler's value wins over the controller's. It also gives the handler's arguments, the controller and method, the call type, and the guard's{ provide, params }throughgetParams().SetMetadatakeys are read withcontext.get('roles').createExecutionContext(Controller, 'method', { args, params })inmeocord/testingbuilds that context for a guard's unit test.meocord generate guardnow generates a guard that reads acreateMetadatadecorator throughExecutionContext, with a spec usingcreateExecutionContext.
Existing guards and tests work unchanged: guards run in the same order, once each, whether a handler is dispatched or called directly in a test. A controller or service, which is shared across calls, cannot inject
ExecutionContext; the bot andMeoCordTestingModulerefuse to start with a clear error rather than handing one call's context to another.#110
5d45fc4Thanks @l7aromeo! -isExplainedError(error)inmeocord/commontells whether MeoCord has already logged what went wrong with an errorapp.start()rejects with, and what to do about it, such as a privileged intent Discord refused. New apps'main.tsuses it to skip logging such an error a second time with its stack trace; an existing app can do the same:TypeScript bootstrap().catch(error => { if (!isExplainedError(error)) logger.error('Error during startup:', error) })#68
1eb5f7fThanks @l7aromeo! - Add gateway event handlers and handler discovery.@On(event)and@Once(event)frommeocord/decoratorhandle any discord.js client event on a controller or service, with the handler's parameters typed fromClientEvents. Event handlers run through the same pipeline as commands, so guards, interceptors and exception filters apply; an error no filter handles is logged with the event and handler, without stopping the bot. At startup MeoCord warns about intents and partials the handlers need thatclientOptionslacks, for@MessageHandlerand@ReactionHandlertoo.HandlerRegistryfrommeocord/corelists every registered handler — commands (one entry per subcommand path), components, modals, autocomplete, message, reaction and event handlers — with the metadata declared on it. Inject it into a service to build a/helpcommand or generated docs.TestingModule.emit(event, ...args)sends an event to a testing module's handlers through the same pipeline, andMeoCordTestingModulebindsHandlerRegistry.- A guard's
canActivatenow also receives an event handler's arguments, so its first parameter accepts any value, and@UseGuardno longer throws when a method's first argument is not an interaction, message or reaction. - Global guards and interceptors from
@MeoCord({ guards, interceptors })also run on event handlers.@Guard({ types })and@Interceptor({ types })limit a guard or interceptor to the context types it is written for, at every level, and a subclass inherits them unless it declares its own. An empty list, or'autocomplete'for an interceptor, which never runs there, throws when the class is decorated. At startup MeoCord names each global one withouttypesthat will also run on events. With no stage that applies to a call, no execution context is built.
#65
939e206Thanks @l7aromeo! - Add global guards:@MeoCord({ guards })runs guards before every dispatched handler — commands, components, modals, autocomplete, message and reaction handlers — ahead of the controller's and the method's own guards. Entries take the same forms as@UseGuard, a guard class or{ provide, params }, and guards that injectExecutionContextreceive it. A controller method called directly still runs only its own guards.For tests,
MeoCordTestingModule.create({ app: App, ... })reads the application's global guards, somodule.invokeruns them first, andinspectHandler(Controller, 'method', { app: App })lists them first. Controllers and providers are still listed as before.#78
99a8bd4Thanks @l7aromeo! - Addrespond(interaction), one place to answer an interaction, and presenters to style MeoCord's answers.respond(interaction)inmeocord/commonreturns the interaction's response state, typed as theResponseStateinterface:acknowledge(),send(),edit(),followUp(),delete(),modal()anderror(). Each picks the Discord call from where the answer stands — reply, update or edit — re-read from the interaction on every call, so answers made directly with discord.js still count. Flags are computed per call, so an ephemeral follow-up never leaks into the next message, and one sent while a public deferred reply is still empty stays private instead of becoming that reply; Components V2 edits keep their flag; re-sent Discord attachment images are pointed atattachment://.- Answers go through the interaction's own methods, which work wherever a user-installed app is used. The channel is used only once the interaction's token has expired and the bot is present.
getInstallContext(interaction)inmeocord/commonreports where an interaction happened and whether the bot is there. error(error, { message, visibility })shows an error and never throws. The built-in fallback now answers through it, so an error on a private (ephemeral) component message is added to that message rather than sent separately.@MeoCord({ presenter })takes aResponsePresenterthat styles the error and loading views. Without one, errors look as before, and the loading view is "⏳ Working on it…" in the newTheme.primaryColor.- Interceptors and filters reach the state as
context.response. - Testing:
getResponse(interaction)reports whatrespond()sent;createDiscordError(code)builds the error discord.js throws; mock messages carry real, emptyflags,components,embedsandattachments; mockshowModal()and a modal submission'sdeferUpdate()answer the interaction as the real ones do.
#67
6c47f09Thanks @l7aromeo! - Add interceptors, which run around a handler once its guards allow the call — for timing, logging, caching or mapping errors.@Interceptor()marks a class implementingInterceptorInterface:intercept(context, next)receives the call'sExecutionContextand continues withnext.handle(), which resolves to what the handler returns. An interceptor can act before and after the handler, skip it, or replace the error it throws.@UseInterceptor(...)applies interceptors to a method or a controller, including inherited handlers, and@MeoCord({ interceptors })to every handler. Global interceptors are outermost, then the controller's, then the method's.{ provide, params }passes options, read withcontext.getParams().- One instance serves every call. An interceptor that injects
ExecutionContextis refused at startup. - Interceptors run for dispatched handlers and under
TestingModule.invoke; a controller method called directly runs its guards but no interceptors, and autocomplete handlers run none. - Testing:
overrideInterceptor(Class).useValue(stub),inspectHandler(...).interceptors, andMeoCordTestingModule.create({ app })includes global interceptors.invokeresolves{ ran: false }when an interceptor skips the handler. meocord generate interceptor <name>(aliasi) writes an interceptor and its spec.
#64
37170f3Thanks @l7aromeo! - Add lifecycle hooks. A controller or service that implementsOnReadyfrommeocord/interfacehasonReady(client, { primary })called once the bot is ready; one that implementsOnShutdownhasonShutdown()called on SIGINT or SIGTERM, before the client is destroyed. Hooks run on every controller and service the app binds, including services no handler has used yet.onReadyhooks run one at a time in dependency order, each class after the classes it injects, and never wait for command registration;onShutdownhooks run in reverse order. A hook that throws is logged and the next one still runs. A signal that arrives whileonReadyhooks are running starts no furtheronReadyand shuts down only the classes whoseonReadyfinished, and those without one. Shutdown waits for theonShutdownhooks up to the newshutdownTimeoutoption inmeocord.config.ts(10 seconds by default), then destroys the client and exits 0.A process now adds one SIGINT and one SIGTERM listener however many apps it starts, so a test suite that creates many apps no longer triggers Node's
MaxListenersExceededWarning.#73
42b62a1Thanks @l7aromeo! - Translate commands and replies from typed catalogs.createTranslator({ default, locales })inmeocord/commonbuilds a translator from one catalog per discord.jsLocale. Keys,{name}params and plural forms ({ one, other, … }, chosen throughIntl.PluralRules) are type-checked against the default catalog, whichdefineCatalog(...)oras constkeeps literal; other locales may leave messages out, and fall back to a locale of the same language, then the default.t.default(key)andt.localizations(key)fill command builders;t.for(interaction),t.for(interaction, { public: true }),t.forGuild(guild)andt.locale(locale)translate replies.@MeoCord({ i18n: t })injects it asTranslator.expectCompleteCataloginmeocord/testingreports missing messages and plural forms per locale.- Registration refuses localised names and descriptions Discord would reject, listing each field, and a builder that throws while building now names itself and the command.
Nothing changes for a bot that does not use it.
#84
b21b0edThanks @l7aromeo! -createMockInteractionacceptsauthorizingIntegrationOwnersas the plain map Discord sends —{ [ApplicationIntegrationType.UserInstall]: userId }— and builds theAuthorizingIntegrationOwnersobject discord.js would, so testing a user-installed command no longer needsas never.#75
b11d77bThanks @l7aromeo! - AddoptionalExternalstomeocord.config.ts, for packages a dependency tries to load and runs without, such assupports-color, whichdebugprobes for inside atryand which axios brings in. WithbundleDependencieson, such a package made every build warn, and listing it inexternalsmade the bot fail at startup when it was missing, because an external becomes an import that runs before the bot's code. A name listed inoptionalExternalsstays arequirewhere the dependency calls it, so a missing package is caught by the dependency, and it is copied intodist/node_moduleswhen it is installed. The build warns when a name is also inexternals. discord.js's optional accelerators,zlib-sync,bufferutilandutf-8-validate, are handled the same way, as before.#72
d9ad59bThanks @l7aromeo! - Add sharding, configured byshardinginmeocord.config.ts.sharding: { shards: 'auto' }(or a number) runs every shard in one process, in one client, with nothing else changing. Unset,clientOptions.shardsworks as before.mode: 'process'runs each shard in its own process.meocord start,node dist/main.js, bun and process managers such as pm2 all start a manager that registers the commands once, spawns the shards from the built bundle, restarts a shard that exits with a growing delay, stops everything with exit code 1 when a shard's token is invalid or Discord refuses its intents, and on SIGINT or SIGTERM shuts every shard down through itsonShutdownhooks before killing any left aftershutdownTimeoutplus five seconds. Undermeocord start --devevery shard runs in one process unlesssharding.developmentistrue.ShardContextfrommeocord/coregives the shards of the current process and calls a service method in every shard withcall(Service, 'method', ...args), one result per process. Each process runs the class passed in, or, for a call from another process, the class of that name, so with process sharding the bot refuses to start when two controllers or services share a name.onReady'sprimaryistrueonly in the process running shard 0.MeoCordFactory.create()now returns the newMeoCordApplicationtype, with the samestart()andregisterCommands()as before.- The generated
meocord.config.tsshows theshardingoption, commented out.
#63
9e27912Thanks @l7aromeo! - AddTestingModule.invokeandinspectHandlertomeocord/testing, for testing a handler the way the bot runs it.module.invoke(Controller, 'method', ...args)runs the handler through the same pipeline dispatch uses, starting with its guards, class guards first and each once. Guards resolve from the testing module, sooverrideGuardstubs apply and guards that injectExecutionContextreceive it. It resolves to{ ran },falsewhen a guard denied the call, and the method name and arguments are type-checked against the handler. An interaction dispatch could not route to the handler, such as a customId its pattern does not match, is rejected before anything runs.inspectHandler(Controller, 'method')reports the guards that run for a handler, in order, and reads its metadata asExecutionContextdoes, without building a module.
Calling a controller method directly in a test still runs its guards, as before.
#70
ade2be6Thanks @l7aromeo! - Validate a handler's input, and transform it with pipes.@Validate(schema)checks an interaction handler's input with any Standard Schema library (zod, valibot, arktype and others) before it runs; a handler takes one, and a second throws when decorated. The handler receives the schema's output, and its second parameter is type-checked against it. Invalid input throws aValidationError(frommeocord/common) listing each issue, which the built-in fallback answers privately with that list; an exception filter can phrase it otherwise.- Pipes turn one validated value into what the handler works with:
@Validate(schema, { pipes: { uid: AccountPipe } })keeps the handler fully typed, and@UsePipe(key, ...pipes)works on its own or beside@Validate, where the value it produces is markedPiped<T>(frommeocord/interface). Mark a class@Pipe()and implementPipeInterface. meocord generate pipe <name>(aliaspi) writes a pipe and its spec.- A modal handler's second argument now also carries the submitted fields, keyed by customId, next to the customId params; a param wins over a field of the same name, with a warning in development.
TestingModule.invokebuilds the params from the interaction when a test passes none, andcreateModalFieldsgives a mock modal its fields.
Existing handlers keep working: modal handlers receive extra keys, and nothing is validated until you add
@Validate.
Patch Changes
- Breaking
#65
5df8fa3Thanks @l7aromeo! - A class-level@UseGuardnow also guards the controller's@Autocompletehandlers, including inherited ones, as it does commands, components, message and reaction handlers. Global guards from@MeoCord({ guards })run there too. A guard sees anAutocompleteInteractionandExecutionContext.getType() === 'autocomplete', and must not reply; when a guard denies, the menu is closed with an empty list instead of being left loading.This changes which guards run for autocomplete. If a class guard assumes a command interaction or replies on denial, see Class guards now cover autocomplete handlers.
#80
cc2fb29Thanks @l7aromeo! - The CLI stops sooner and says what to do when something is wrong.build,startandregistercheckmeocord.config.tsfirst. One that fails to load stops them, naming the file and line, instead of building on. Options of the wrong type, such assharding.mode: 'bogus'oroptionalExternals: 'sharp', stop them with a list of every problem, where before some built silently and others failed with an internal error. An option MeoCord does not know is reported as a warning.start --prodwithout--buildchecks the built config the same way, and says when there is no config at all.- The compiled config is written only once it has built, so a failed build no longer leaves a broken
dist/meocord.config.mjsfor every later command to trip over. meocord generaterefuses names that leave its folder (.., a leading/, a drive letter), which could write outsidesrc/or the project, and asks to be run from a project's root. On Windows,\separates folders in a name.meocord createrefuses a name with no letters or digits, which was reported asDirectory "" already exists.- A missing token is reported with where it comes from:
discordTokeninmeocord.config.ts, which a new app reads fromDISCORD_TOKENin.env. start --dev --buildbuilds once, as the watcher does, instead of twice.
#95
86301f7Thanks @l7aromeo! -meocord start --devrestarts the bot on the next rebuild after it exited on its own, such as after an error at startup, and Ctrl+C then stops the watcher at once instead of waiting for a second Ctrl+C. The bot started throughnpm runfrom a script bun runs is launched on node again, rather than handed to npm.#105
8dc19f4Thanks @l7aromeo! -meocord --helpand each command's help no longer print "No available choices." for arguments that have no choices, andmeocord create --helpdescribes its<app-name>argument instead of saying "No description provided".#103
35d2276Thanks @l7aromeo! -meocord startandmeocord registerpass SIGINT and SIGTERM on to the bot. A signal sent to the CLI alone, as Docker, pm2 and systemd send one, used to stop the CLI and leave the bot running, or, with SIGINT, not stop it at all; the bot now shuts down through its own shutdown path and the CLI exits with its code. One Ctrl+C that reaches a process twice within a second counts once, so it no longer force-kills the shards in process sharding; a second signal after that still stops everything at once.#93
18db29eThanks @l7aromeo! - The warning about two componentcustomIdpatterns that can match the same id, such asa/{x}/canda/b/{y}, is logged when the bot starts, as the README describes, rather than at the first button, select menu or modal interaction. The routes are built once at startup and reused by every interaction.#69
f00309fThanks @l7aromeo! - A command that throws after deferring its reply is now answered instead of left showing "thinking…" until it times out: the deferred reply is edited into the error message. A command or component that throws after it already replied now gets a private follow-up with the error, where it used to get nothing. Buttons, select menus and modals submitted from a public message are answered with a private follow-up, never by editing the message the user clicked; on a private (ephemeral) message, the error is added to that message. Unanswered interactions are answered exactly as before.If you want a different answer in these cases — a different text, no answer, or a log to an error service — register an exception filter with
@Catch()in@MeoCord({ filters }): filters run before this built-in answer and replace it.#56
c2c09d7Thanks @l7aromeo! - Update@rsbuild/coreto 2.2.9, and generate new applications withprettier3.9.9.#59
7cfea8dThanks @l7aromeo! -meocord/eslintignorescoverage/. Flat config does not read.gitignore, so runningeslintaftertest:coveragein a generated application linted the istanbul report and failed with three warnings about unusedeslint-disabledirectives. Nothing to do after upgrading.#110
213bd8bThanks @l7aromeo! - When Discord refuses a privileged intent at login, the bot now says which privileged intents it requests (GuildMembers,GuildPresences,MessageContent) and where to enable them — Developer Portal → your application → Bot → Privileged Gateway Intents — and that a verified bot in 100 or more servers needs Discord's approval for them. Before, it printed only "Used disallowed intents" and a stack trace. Intents Discord refuses as invalid are explained too. The stack trace moves to debug level; the exit code and the errorapp.start()rejects with are unchanged.#59
d4612b3Thanks @l7aromeo! - Generated controllers, context menu builders and services pass the application's lint as written.meocord goutput carried a blank line at the start of each controller class, a split context menu builder chain and a semicolon in the service, which failed prettier whenevereslint --fixhad not already rewritten the file. Files you generated earlier are unaffected;eslint --fixcorrects them.#58
88cb6f0Thanks @l7aromeo! - Fix handlers of a controller that extends another controller being added to the parent class too. A subclass's@Command,@MessageHandler,@ReactionHandlerand@Autocompletehandlers were written into the base class's metadata, so the base controller listed, and could be routed to, handlers it does not have. Each class now keeps its own copy, including the handlers it inherits.Fix the guard list stored under
MetadataKey.Guardswhen@UseGuardis used on both a class and its methods. The class-level list replaced the method's own guards; it now holds every guard that runs, class-level guards first, in the order they run. Which guards run is unchanged.#109
2d4738aThanks @l7aromeo! - A handler may take fewer parameters than dispatch passes, as any TypeScript callback can.@Command,@Autocomplete,@MessageHandlerand@ReactionHandleron a method with no parameters, such asasync refresh() {}, failed to compile with TS1241 ("Unable to resolve signature of method decorator"), and@Validateor@UsePipeon a handler that ignores its input was refused as a mismatch. Both now compile. A parameter of the wrong type is still refused.- Breaking
#65
53eac95Thanks @l7aromeo! - A class-level@UseGuardnow also guards the handlers a controller inherits. On a controller that extends another, the subclass's guards were applied only to the handlers it declared itself, so inherited commands, components, message and reaction handlers ran without them. They now run the subclass's guards first, then the base class's, then the method's, whether dispatched, called directly or run withTestingModule.invoke.This changes which guards run for inherited handlers. If your bot relied on an inherited handler skipping the subclass's guards, see Class guards now cover inherited handlers.
#64
a42ca18Thanks @l7aromeo! - FixMeoCordFactory.create()for a controller or service that injects a dependency with@inject(Token)on a parameter typed as an interface. The factory followed only the parameter's type, which for an interface isObject, so it boundObjectinstead of the token and resolving the class failed with "missing metadata on type Object". It now binds the@injecttoken, and skips built-in constructors such asObject.#62
04c7333Thanks @l7aromeo! -process.envvalues loaded bymeocord.config.tsare set before the application's modules run.main.tsimportsAppbefore anything else, so an option such as@MeoCord({ activities: [{ name: process.env.STATUS! }] })read the environment before the config'sdotenvimport had loaded.env, and gotundefined. The build now loadsdist/meocord.config.mjsahead ofmain.ts, for every way of starting the bundle. Rebuild to pick it up; no code changes. The README shows how to choose a.envfile per environment.#69
baa94edThanks @l7aromeo! -createMockInteraction(ModalSubmitInteraction)now runs the realisFromMessage(), so it returnstrueonly when the mock has amessage, as a real modal does. It returnedundefined.#113
9b54a71Thanks @l7aromeo! - MeoCord's repository is nowmeocord/meocordon GitHub. The package's repository, homepage and issue links point there, and so does the README thatmeocord createwrites for a new application. Links to the oldl7aromeo/meocordaddress redirect, so nothing needs changing in your code or bookmarks.#61
1f09800Thanks @l7aromeo! - TheRateLimitGuardthat 4.0 copied into generated applications never limited anything: a new guard instance is created for every call, so the counts it kept on the instance started empty each time. New applications no longer get it; they limit their sample commands with@Cooldown.Upgrading
meocorddoes not change the copy in your application. If your app still hassrc/guards/rate-limit.guard.ts, move itsrateLimitsmap out of the class to module level, so every instance shares it, or replace the guard with@Cooldown({ uses, seconds }).The README now explains that a guard instance is created for every call, and shows how to pass options to a guard with
@UseGuard({ provide, params }).#84
ed07729Thanks @l7aromeo! - New applications frommeocord createstart with the 4.1 patterns. Existing applications are not changed: upgradingmeocordnever touches your code.- The sample controllers answer through
respond(interaction). The button, select menu and modal samples use@Defer, and the button sample's second handler is guarded by anOwnerGuardthat denies withGuardDeniedError. - The slash, button, modal and context menu samples limit how often they run with
@Cooldown({ uses: 5, seconds: 60 }), and theRateLimitGuardand its spec are gone. src/presenters/app.presenter.tsstyles the loading and error views, registered with@MeoCord({ presenter }).- Each sample's spec runs its handler with
invokeand checks whatrespond()sent.
- The sample controllers answer through
- Breaking
#88
53517ccThanks @l7aromeo! -SetMetadatarefuses the keys MeoCord stores its own metadata under, such as'guards'and'commandType', and throws when the decorator is created, naming the key. A value under'guards'replaced the guard list dispatch runs, so a handler decorated with@SetMetadata('guards', …)above its@UseGuardran with none of its guards. Choose another key, or declare the decorator withcreateMetadata, whose key is unique; seeSetMetadatarefuses MeoCord's own keys.A guard class listed in
@MeoCord({ services })is warned about at startup: one shared instance takes every call's{ provide, params }. #107
aca4436Thanks @l7aromeo! -@UseGuard,@UseInterceptor,@UseFilter,@UsePipe,@Validate's pipes and@MeoCord({ guards, interceptors, filters })all take the same entries: a class, or{ provide: Class, params? }.paramsis now optional for guards, interceptors and filters too, as it already was for pipes, so{ provide: ChannelGuard }works as the class alone; before, a guard or interceptor given that way failed inside the container on its first call.Anything else, such as
null, aprovidethat is not a class, orparamsthat are not an object, is refused when the decorator applies, with an error naming the decorator and the class or handler, instead of failing when a call first reaches it.#65
1c4ed6fThanks @l7aromeo! - Fix dependency injection for a controller, service or guard that extends another decorated class. The subclass was treated as already set up because its base class was, so its own constructor's dependencies were never injected: a subclass with its own constructor failed to resolve, and one without a constructor was built with no arguments. A subclass now gets its own constructor's dependencies, or its base class's when it declares no constructor, and inherits its base class's injected properties. No change is needed in your code.#92
a531fcbThanks @l7aromeo! - Fix building an application whosetsconfig.jsonusesextends,files, orcompilerOptions.typeRoots. MeoCord builds from a copy oftsconfig.jsonin a temporary directory, and a relativeextendsorfilesentry in that copy pointed at files that are not there; a package inextends, such as@tsconfig/node22/tsconfig.json, could not be found from there either. The copy now carries absolute paths, with a package resolved from the project'snode_modules.typeRootsis read fromcompilerOptions, where TypeScript declares it, andincludeandexcludeare resolved even withoutcompilerOptions.#92
16d9a59Thanks @l7aromeo! -meocord buildno longer rewrites yourtsconfig.json. When the file had comments or trailing commas, which TypeScript allows, the build "repaired" it and wrote the result back, deleting your comments, and the repair broke on a//inside a string such as"$schema": "https://json.schemastore.org/tsconfig", failing the build. MeoCord now reads comments and trailing commas as TypeScript does, leaves strings alone, and only ever writes its own temporary copy. Atsconfig.jsonit cannot parse fails the build with the file and the fix named.#74
5f32513Thanks @l7aromeo! - Fix builds that run at the same time, such as CI jobs sharing a runner, failing with a JSON parse error inmodified-tsconfig.json. Every build wrote its copy of the tsconfig to one fixed file in the system temp directory, so two builds could read each other's half-written file. Each build now writes to a directory of its own, removed when the build exits.