Skip to content
GitHub

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 5b49d3e Thanks @l7aromeo! - Choose where commands are registered, and register without starting the bot.

    • commands in meocord.config.ts: guilds registers to guilds instead of globally, developmentGuild sends everything to one test guild under start --dev, register: false leaves registration to meocord register, and clearOther removes commands left in a scope no longer used, which are otherwise reported as a warning; a development run sending to developmentGuild only 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-register sends it anyway.
    • Commands are read from the controllers' prototypes, so registering constructs no controller.

    With no commands setting, an existing bot still registers every command globally at each start. One thing does change: a builder whose toJSON() 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 developmentGuild wired to DEV_GUILD_ID in .env.

  • #76 6bfe0e3 Thanks @l7aromeo! - Limit how often a handler runs with @Cooldown.

    • @Cooldown({ seconds, uses, per, bypass }) on an interaction or message handler, or a controller, allows uses calls within seconds (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. bypass exempts 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 @Once handler.
    • It is counted after guards, validation and pipes, so a denied call or bad input spends nothing. A blocked call throws CooldownError (from meocord/common), which the built-in fallback answers only to the caller with cooldownMessage(): "Slow down: try again in 12s."
    • Calls are counted in memory by default. @MeoCord({ cooldownStore }) takes a class extending CooldownStore, 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(...).cooldowns lists a handler's cooldowns.
  • #81 017ec6c Thanks @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() without components puts 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 returns false leaves nothing behind, and one that throws GuardDeniedError is answered privately.

    Options: ephemeral, disable ('all', 'clicked' or 'none'), mode: 'auto' to acknowledge only when the handler has not answered after after milliseconds (1500 by default, and never later than 2.5 seconds after the interaction was created), and suppressNotifications. @Defer on a message, reaction, event or autocomplete handler throws.

  • #69 f00309f Thanks @l7aromeo! - Add exception filters, which decide what happens when a handler, its interceptors or its guards throw.

    • @Catch(ErrorType, ...) marks a class implementing ExceptionFilter: catch(error, context) receives the error and the call's ExecutionContext. 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 @Catch matches. { provide, params } passes options, read with context.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. Returning false still 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, and MeoCordTestingModule.create({ app }) includes global filters. invoke resolves with error set 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> (alias f) writes a filter, the error it handles, and a spec.
  • #60 f3ea0df Thanks @l7aromeo! - Add typed handler metadata and ExecutionContext, so guards can read what they guard.

    • createMetadata<T>(description) in meocord/common makes a typed decorator for a controller or a handler, with a unique key.
    • ExecutionContext in meocord/common describes the handler call being run. A guard receives it by constructor injection and reads metadata with context.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 } through getParams(). SetMetadata keys are read with context.get('roles').
    • createExecutionContext(Controller, 'method', { args, params }) in meocord/testing builds that context for a guard's unit test.
    • meocord generate guard now generates a guard that reads a createMetadata decorator through ExecutionContext, with a spec using createExecutionContext.

    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 and MeoCordTestingModule refuse to start with a clear error rather than handing one call's context to another.

  • #110 5d45fc4 Thanks @l7aromeo! - isExplainedError(error) in meocord/common tells whether MeoCord has already logged what went wrong with an error app.start() rejects with, and what to do about it, such as a privileged intent Discord refused. New apps' main.ts uses 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 1eb5f7f Thanks @l7aromeo! - Add gateway event handlers and handler discovery.

    • @On(event) and @Once(event) from meocord/decorator handle any discord.js client event on a controller or service, with the handler's parameters typed from ClientEvents. 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 that clientOptions lacks, for @MessageHandler and @ReactionHandler too.
    • HandlerRegistry from meocord/core lists 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 /help command or generated docs.
    • TestingModule.emit(event, ...args) sends an event to a testing module's handlers through the same pipeline, and MeoCordTestingModule binds HandlerRegistry.
    • A guard's canActivate now also receives an event handler's arguments, so its first parameter accepts any value, and @UseGuard no 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 without types that will also run on events. With no stage that applies to a call, no execution context is built.
  • #65 939e206 Thanks @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 inject ExecutionContext receive it. A controller method called directly still runs only its own guards.

    For tests, MeoCordTestingModule.create({ app: App, ... }) reads the application's global guards, so module.invoke runs them first, and inspectHandler(Controller, 'method', { app: App }) lists them first. Controllers and providers are still listed as before.

  • #78 99a8bd4 Thanks @l7aromeo! - Add respond(interaction), one place to answer an interaction, and presenters to style MeoCord's answers.

    • respond(interaction) in meocord/common returns the interaction's response state, typed as the ResponseState interface: acknowledge(), send(), edit(), followUp(), delete(), modal() and error(). 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 at attachment://.
    • 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) in meocord/common reports 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 a ResponsePresenter that styles the error and loading views. Without one, errors look as before, and the loading view is "⏳ Working on it…" in the new Theme.primaryColor.
    • Interceptors and filters reach the state as context.response.
    • Testing: getResponse(interaction) reports what respond() sent; createDiscordError(code) builds the error discord.js throws; mock messages carry real, empty flags, components, embeds and attachments; mock showModal() and a modal submission's deferUpdate() answer the interaction as the real ones do.
  • #67 6c47f09 Thanks @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 implementing InterceptorInterface: intercept(context, next) receives the call's ExecutionContext and continues with next.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 with context.getParams().
    • One instance serves every call. An interceptor that injects ExecutionContext is 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, and MeoCordTestingModule.create({ app }) includes global interceptors. invoke resolves { ran: false } when an interceptor skips the handler.
    • meocord generate interceptor <name> (alias i) writes an interceptor and its spec.
  • #64 37170f3 Thanks @l7aromeo! - Add lifecycle hooks. A controller or service that implements OnReady from meocord/interface has onReady(client, { primary }) called once the bot is ready; one that implements OnShutdown has onShutdown() 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. onReady hooks run one at a time in dependency order, each class after the classes it injects, and never wait for command registration; onShutdown hooks run in reverse order. A hook that throws is logged and the next one still runs. A signal that arrives while onReady hooks are running starts no further onReady and shuts down only the classes whose onReady finished, and those without one. Shutdown waits for the onShutdown hooks up to the new shutdownTimeout option in meocord.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 42b62a1 Thanks @l7aromeo! - Translate commands and replies from typed catalogs.

    • createTranslator({ default, locales }) in meocord/common builds a translator from one catalog per discord.js Locale. Keys, {name} params and plural forms ({ one, other, … }, chosen through Intl.PluralRules) are type-checked against the default catalog, which defineCatalog(...) or as const keeps literal; other locales may leave messages out, and fall back to a locale of the same language, then the default.
    • t.default(key) and t.localizations(key) fill command builders; t.for(interaction), t.for(interaction, { public: true }), t.forGuild(guild) and t.locale(locale) translate replies.
    • @MeoCord({ i18n: t }) injects it as Translator.
    • expectCompleteCatalog in meocord/testing reports 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 b21b0ed Thanks @l7aromeo! - createMockInteraction accepts authorizingIntegrationOwners as the plain map Discord sends — { [ApplicationIntegrationType.UserInstall]: userId } — and builds the AuthorizingIntegrationOwners object discord.js would, so testing a user-installed command no longer needs as never.

  • #75 b11d77b Thanks @l7aromeo! - Add optionalExternals to meocord.config.ts, for packages a dependency tries to load and runs without, such as supports-color, which debug probes for inside a try and which axios brings in. With bundleDependencies on, such a package made every build warn, and listing it in externals made 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 in optionalExternals stays a require where the dependency calls it, so a missing package is caught by the dependency, and it is copied into dist/node_modules when it is installed. The build warns when a name is also in externals. discord.js's optional accelerators, zlib-sync, bufferutil and utf-8-validate, are handled the same way, as before.

  • #72 d9ad59b Thanks @l7aromeo! - Add sharding, configured by sharding in meocord.config.ts.

    • sharding: { shards: 'auto' } (or a number) runs every shard in one process, in one client, with nothing else changing. Unset, clientOptions.shards works 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 its onShutdown hooks before killing any left after shutdownTimeout plus five seconds. Under meocord start --dev every shard runs in one process unless sharding.development is true.
    • ShardContext from meocord/core gives the shards of the current process and calls a service method in every shard with call(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's primary is true only in the process running shard 0.
    • MeoCordFactory.create() now returns the new MeoCordApplication type, with the same start() and registerCommands() as before.
    • The generated meocord.config.ts shows the sharding option, commented out.
  • #63 9e27912 Thanks @l7aromeo! - Add TestingModule.invoke and inspectHandler to meocord/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, so overrideGuard stubs apply and guards that inject ExecutionContext receive it. It resolves to { ran }, false when 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 as ExecutionContext does, without building a module.

    Calling a controller method directly in a test still runs its guards, as before.

  • #70 ade2be6 Thanks @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 a ValidationError (from meocord/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 marked Piped<T> (from meocord/interface). Mark a class @Pipe() and implement PipeInterface.
    • meocord generate pipe <name> (alias pi) 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.invoke builds the params from the interaction when a test passes none, and createModalFields gives 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 5df8fa3 Thanks @l7aromeo! - A class-level @UseGuard now also guards the controller's @Autocomplete handlers, including inherited ones, as it does commands, components, message and reaction handlers. Global guards from @MeoCord({ guards }) run there too. A guard sees an AutocompleteInteraction and ExecutionContext.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 cc2fb29 Thanks @l7aromeo! - The CLI stops sooner and says what to do when something is wrong.

    • build, start and register check meocord.config.ts first. One that fails to load stops them, naming the file and line, instead of building on. Options of the wrong type, such as sharding.mode: 'bogus' or optionalExternals: '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 --prod without --build checks 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.mjs for every later command to trip over.
    • meocord generate refuses names that leave its folder (.., a leading /, a drive letter), which could write outside src/ or the project, and asks to be run from a project's root. On Windows, \ separates folders in a name.
    • meocord create refuses a name with no letters or digits, which was reported as Directory "" already exists.
    • A missing token is reported with where it comes from: discordToken in meocord.config.ts, which a new app reads from DISCORD_TOKEN in .env.
    • start --dev --build builds once, as the watcher does, instead of twice.
  • #95 86301f7 Thanks @l7aromeo! - meocord start --dev restarts 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 through npm run from a script bun runs is launched on node again, rather than handed to npm.

  • #105 8dc19f4 Thanks @l7aromeo! - meocord --help and each command's help no longer print "No available choices." for arguments that have no choices, and meocord create --help describes its <app-name> argument instead of saying "No description provided".

  • #103 35d2276 Thanks @l7aromeo! - meocord start and meocord register pass 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 18db29e Thanks @l7aromeo! - The warning about two component customId patterns that can match the same id, such as a/{x}/c and a/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 f00309f Thanks @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 c2c09d7 Thanks @l7aromeo! - Update @rsbuild/core to 2.2.9, and generate new applications with prettier 3.9.9.

  • #59 7cfea8d Thanks @l7aromeo! - meocord/eslint ignores coverage/. Flat config does not read .gitignore, so running eslint after test:coverage in a generated application linted the istanbul report and failed with three warnings about unused eslint-disable directives. Nothing to do after upgrading.

  • #110 213bd8b Thanks @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 error app.start() rejects with are unchanged.

  • #59 d4612b3 Thanks @l7aromeo! - Generated controllers, context menu builders and services pass the application's lint as written. meocord g output 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 whenever eslint --fix had not already rewritten the file. Files you generated earlier are unaffected; eslint --fix corrects them.

  • #58 88cb6f0 Thanks @l7aromeo! - Fix handlers of a controller that extends another controller being added to the parent class too. A subclass's @Command, @MessageHandler, @ReactionHandler and @Autocomplete handlers 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.Guards when @UseGuard is 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 2d4738a Thanks @l7aromeo! - A handler may take fewer parameters than dispatch passes, as any TypeScript callback can. @Command, @Autocomplete, @MessageHandler and @ReactionHandler on a method with no parameters, such as async refresh() {}, failed to compile with TS1241 ("Unable to resolve signature of method decorator"), and @Validate or @UsePipe on 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 53eac95 Thanks @l7aromeo! - A class-level @UseGuard now 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 with TestingModule.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 a42ca18 Thanks @l7aromeo! - Fix MeoCordFactory.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 is Object, so it bound Object instead of the token and resolving the class failed with "missing metadata on type Object". It now binds the @inject token, and skips built-in constructors such as Object.

  • #62 04c7333 Thanks @l7aromeo! - process.env values loaded by meocord.config.ts are set before the application's modules run. main.ts imports App before anything else, so an option such as @MeoCord({ activities: [{ name: process.env.STATUS! }] }) read the environment before the config's dotenv import had loaded .env, and got undefined. The build now loads dist/meocord.config.mjs ahead of main.ts, for every way of starting the bundle. Rebuild to pick it up; no code changes. The README shows how to choose a .env file per environment.

  • #69 baa94ed Thanks @l7aromeo! - createMockInteraction(ModalSubmitInteraction) now runs the real isFromMessage(), so it returns true only when the mock has a message, as a real modal does. It returned undefined.

  • #113 9b54a71 Thanks @l7aromeo! - MeoCord's repository is now meocord/meocord on GitHub. The package's repository, homepage and issue links point there, and so does the README that meocord create writes for a new application. Links to the old l7aromeo/meocord address redirect, so nothing needs changing in your code or bookmarks.

  • #61 1f09800 Thanks @l7aromeo! - The RateLimitGuard that 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 meocord does not change the copy in your application. If your app still has src/guards/rate-limit.guard.ts, move its rateLimits map 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 ed07729 Thanks @l7aromeo! - New applications from meocord create start with the 4.1 patterns. Existing applications are not changed: upgrading meocord never 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 an OwnerGuard that denies with GuardDeniedError.
    • The slash, button, modal and context menu samples limit how often they run with @Cooldown({ uses: 5, seconds: 60 }), and the RateLimitGuard and its spec are gone.
    • src/presenters/app.presenter.ts styles the loading and error views, registered with @MeoCord({ presenter }).
    • Each sample's spec runs its handler with invoke and checks what respond() sent.
  • Breaking

    #88 53517cc Thanks @l7aromeo! - SetMetadata refuses 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 @UseGuard ran with none of its guards. Choose another key, or declare the decorator with createMetadata, whose key is unique; see SetMetadata refuses 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 aca4436 Thanks @l7aromeo! - @UseGuard, @UseInterceptor, @UseFilter, @UsePipe, @Validate's pipes and @MeoCord({ guards, interceptors, filters }) all take the same entries: a class, or { provide: Class, params? }. params is 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, a provide that is not a class, or params that 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 1c4ed6f Thanks @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 a531fcb Thanks @l7aromeo! - Fix building an application whose tsconfig.json uses extends, files, or compilerOptions.typeRoots. MeoCord builds from a copy of tsconfig.json in a temporary directory, and a relative extends or files entry in that copy pointed at files that are not there; a package in extends, 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's node_modules. typeRoots is read from compilerOptions, where TypeScript declares it, and include and exclude are resolved even without compilerOptions.

  • #92 16d9a59 Thanks @l7aromeo! - meocord build no longer rewrites your tsconfig.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. A tsconfig.json it cannot parse fails the build with the file and the fix named.

  • #74 5f32513 Thanks @l7aromeo! - Fix builds that run at the same time, such as CI jobs sharing a runner, failing with a JSON parse error in modified-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.