meocord 4.1.0-beta.9 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +823 -0
- package/README.md +3 -3
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/dist/cjs/_shared/{theme-resolvers-DLgZL-uE.cjs → controller.decorator-BywwkU9O.cjs} +2596 -1299
- package/dist/cjs/_shared/providers-BMMLp2SN.cjs +4935 -0
- package/dist/cjs/_shared/{sharded-cooldown-store-ComEj8So.cjs → sharded-cooldown-store-B65iaiAy.cjs} +69 -21
- package/dist/cjs/_shared/startup-roots-lFc69AxC.cjs +1634 -0
- package/dist/cjs/_shared/translate-error-BnM8FbSH.cjs +151 -0
- package/dist/cjs/common/index.cjs +135 -279
- package/dist/cjs/core/index.cjs +649 -250
- package/dist/cjs/decorator/index.cjs +216 -182
- package/dist/cjs/enum/index.cjs +9 -5
- package/dist/cjs/testing/index.cjs +1570 -392
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/meocord.config.ts.template +9 -1
- package/dist/esm/bin/app-template/package.json.template +8 -6
- package/dist/esm/bin/app-template/pnpm-workspace.yaml.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +7 -7
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.spec.ts.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template +16 -9
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template +1 -1
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.spec.ts.template +4 -2
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.spec.ts.template +26 -9
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template +3 -3
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template +7 -3
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +1 -1
- package/dist/esm/bin/app-template/src/types/theme.d.ts.template +3 -12
- package/dist/esm/bin/app-template/tsconfig.eslint.json.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +3 -4
- package/dist/esm/bin/builder-template/builder/context-menu.builder.template +3 -2
- package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +3 -2
- package/dist/esm/bin/builder-template/builder/slash.builder.template +3 -2
- package/dist/esm/bin/builder-template/controller/autocomplete.controller.spec.template +18 -0
- package/dist/esm/bin/builder-template/controller/button.controller.spec.template +24 -0
- package/dist/esm/bin/builder-template/controller/channel-select-menu.controller.spec.template +22 -0
- package/dist/esm/bin/builder-template/controller/context-menu.controller.spec.template +17 -0
- package/dist/esm/bin/builder-template/controller/mentionable-select-menu.controller.spec.template +24 -0
- package/dist/esm/bin/builder-template/controller/message.controller.spec.template +14 -0
- package/dist/esm/bin/builder-template/controller/modal-submit.controller.spec.template +17 -0
- package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.spec.template +17 -0
- package/dist/esm/bin/builder-template/controller/reaction.controller.spec.template +29 -0
- package/dist/esm/bin/builder-template/controller/reaction.controller.template +3 -3
- package/dist/esm/bin/builder-template/controller/role-select-menu.controller.spec.template +19 -0
- package/dist/esm/bin/builder-template/controller/select-menu.controller.spec.template +18 -0
- package/dist/esm/bin/builder-template/controller/slash.controller.spec.template +20 -0
- package/dist/esm/bin/builder-template/controller/user-select-menu.controller.spec.template +19 -0
- package/dist/esm/bin/builder-template/filter.template +1 -1
- package/dist/esm/bin/builder-template/guard.template +2 -1
- package/dist/esm/bin/builder-template/interceptor.template +2 -1
- package/dist/esm/bin/generator.js +30 -16
- package/dist/esm/bin/helper/app-generator.helper.js +68 -15
- package/dist/esm/bin/helper/controller-generator.helper.js +31 -16
- package/dist/esm/bin/helper/filter-generator.helper.js +19 -8
- package/dist/esm/bin/helper/guard-generator.helper.js +19 -8
- package/dist/esm/bin/helper/initial-commit.helper.js +76 -0
- package/dist/esm/bin/helper/interceptor-generator.helper.js +19 -8
- package/dist/esm/bin/helper/observer-generator.helper.js +19 -8
- package/dist/esm/bin/helper/pipe-generator.helper.js +19 -8
- package/dist/esm/bin/helper/service-generator.helper.js +19 -8
- package/dist/esm/bin/meocord.js +204 -122
- package/dist/esm/build/load-config.pre-entry.js +18 -22
- package/dist/esm/build/native-addons.js +78 -56
- package/dist/esm/build/rsbuild-config.js +34 -16
- package/dist/esm/build/runnable-bundle.js +3 -9
- package/dist/esm/common/cooldown-store.js +82 -24
- package/dist/esm/common/decorator.js +38 -19
- package/dist/esm/common/deprecation.js +23 -0
- package/dist/esm/common/errors.js +50 -35
- package/dist/esm/common/execution-context.js +8 -3
- package/dist/esm/common/log-level.js +1 -1
- package/dist/esm/common/logger.js +102 -39
- package/dist/esm/common/meocord-messages.js +4 -5
- package/dist/esm/common/meocord-text.js +6 -6
- package/dist/esm/common/metadata.js +1 -0
- package/dist/esm/common/redis-cooldown-store.js +78 -34
- package/dist/esm/common/response/call-order.js +10 -0
- package/dist/esm/common/response/components.js +5 -4
- package/dist/esm/common/response/presenter.js +109 -4
- package/dist/esm/common/response/response-state.js +384 -90
- package/dist/esm/common/response/send-failure.js +11 -4
- package/dist/esm/common/route.js +4 -2
- package/dist/esm/common/sharded-cooldown-store.js +61 -18
- package/dist/esm/common/theme.js +32 -16
- package/dist/esm/common/token.js +2 -2
- package/dist/esm/common/translate-error.js +4 -4
- package/dist/esm/common/translator.js +55 -11
- package/dist/esm/common/user-outcome.js +4 -5
- package/dist/esm/core/command-conflicts.js +247 -15
- package/dist/esm/core/command-localizations.js +31 -18
- package/dist/esm/core/command-registration.js +25 -15
- package/dist/esm/core/command-routes.js +28 -0
- package/dist/esm/core/component-routes.js +17 -2
- package/dist/esm/core/cooldown-runner.js +171 -57
- package/dist/esm/core/defer.js +3 -3
- package/dist/esm/core/dispatcher.js +57 -63
- package/dist/esm/core/dm-reactions.js +55 -0
- package/dist/esm/core/event-requirements.js +21 -9
- package/dist/esm/core/failed-login.js +15 -0
- package/dist/esm/core/fallback.js +184 -80
- package/dist/esm/core/filter-runner.js +12 -12
- package/dist/esm/core/guard-runner.js +42 -26
- package/dist/esm/core/handler-input.js +19 -16
- package/dist/esm/core/handler-pipeline.js +90 -29
- package/dist/esm/core/handler-registry.js +32 -18
- package/dist/esm/core/input-runner.js +11 -8
- package/dist/esm/core/interceptor-runner.js +144 -13
- package/dist/esm/core/lifecycle-hooks.js +76 -10
- package/dist/esm/core/lifecycle-order.js +7 -3
- package/dist/esm/core/meocord-classes.js +48 -0
- package/dist/esm/core/meocord-factory.js +138 -73
- package/dist/esm/core/meocord.app.js +183 -98
- package/dist/esm/core/message-entities.js +5 -5
- package/dist/esm/core/message-help.js +10 -6
- package/dist/esm/core/message-params.js +31 -29
- package/dist/esm/core/message-routes.js +84 -38
- package/dist/esm/core/message-words.js +4 -4
- package/dist/esm/core/observer-runner.js +4 -5
- package/dist/esm/core/providers.js +93 -18
- package/dist/esm/core/scalar-types.js +13 -7
- package/dist/esm/core/shard-context.js +59 -18
- package/dist/esm/core/shard-exit.js +50 -0
- package/dist/esm/core/shard-manager.js +78 -30
- package/dist/esm/core/shard-messages.js +2 -1
- package/dist/esm/core/stage-scope.js +9 -7
- package/dist/esm/core/startup-checked.js +29 -0
- package/dist/esm/core/startup-roots.js +82 -0
- package/dist/esm/core/theme-defaults.js +3 -3
- package/dist/esm/core/theme-resolvers.js +45 -16
- package/dist/esm/core/theme-runtime.js +14 -16
- package/dist/esm/core/theme-scope.js +8 -6
- package/dist/esm/core/theme-validation.js +30 -9
- package/dist/esm/decorator/app.decorator.js +17 -69
- package/dist/esm/decorator/command-builder.decorator.js +7 -6
- package/dist/esm/decorator/controller-class.decorator.js +10 -6
- package/dist/esm/decorator/controller.decorator.js +147 -61
- package/dist/esm/decorator/cooldown.decorator.js +12 -7
- package/dist/esm/decorator/defer.decorator.js +9 -6
- package/dist/esm/decorator/event.decorator.js +6 -4
- package/dist/esm/decorator/filter.decorator.js +26 -9
- package/dist/esm/decorator/guard.decorator.js +35 -28
- package/dist/esm/decorator/inject.decorator.js +6 -1
- package/dist/esm/decorator/interceptor.decorator.js +9 -6
- package/dist/esm/decorator/observer.decorator.js +6 -4
- package/dist/esm/decorator/service.decorator.js +7 -4
- package/dist/esm/decorator/theme.decorator.js +3 -2
- package/dist/esm/decorator/validation.decorator.js +14 -9
- package/dist/esm/enum/controller.enum.js +1 -1
- package/dist/esm/enum/metadata-key.enum.js +8 -4
- package/dist/esm/package.json.js +7 -3
- package/dist/esm/testing/catalog.js +3 -3
- package/dist/esm/testing/cooldown-store-suite.js +85 -17
- package/dist/esm/testing/discord-defaults.js +55 -5
- package/dist/esm/testing/discord-shape.js +3 -3
- package/dist/esm/testing/index.js +1 -1
- package/dist/esm/testing/inspect-handler.js +1 -1
- package/dist/esm/testing/meocord-testing-module.js +189 -104
- package/dist/esm/testing/mock-fn.js +2 -6
- package/dist/esm/testing/mock-interaction.js +1128 -176
- package/dist/esm/testing/modal-fields.js +31 -13
- package/dist/esm/testing/response.js +25 -15
- package/dist/esm/testing/routing.js +6 -3
- package/dist/esm/testing/snowflake.js +8 -0
- package/dist/esm/util/bundle-entry.util.js +11 -6
- package/dist/esm/util/common.util.js +34 -33
- package/dist/esm/util/dev-runner.util.js +36 -27
- package/dist/esm/util/generator-cli.util.js +66 -25
- package/dist/esm/util/inherited-env.util.js +81 -0
- package/dist/esm/util/interaction.util.js +27 -19
- package/dist/esm/util/meocord-cli.util.js +2 -3
- package/dist/esm/util/meocord-config-loader.util.js +66 -6
- package/dist/esm/util/meocord-config-validation.util.js +7 -2
- package/dist/esm/util/meocord-source-config.util.js +1 -8
- package/dist/esm/util/metadata-keys.js +43 -0
- package/dist/esm/util/parent-send.util.js +27 -0
- package/dist/esm/util/platform.util.js +9 -2
- package/dist/esm/util/refusal.util.js +28 -8
- package/dist/esm/util/registration-mode.util.js +1 -1
- package/dist/esm/util/shard-process.util.js +6 -0
- package/dist/esm/util/sharding-mode.util.js +5 -5
- package/dist/esm/util/shutdown-timeout.util.js +22 -1
- package/dist/esm/util/timer-limit.util.js +15 -0
- package/dist/esm/util/tsconfig.util.js +78 -16
- package/dist/esm/util/user-text.util.js +35 -0
- package/dist/esm/util/value.util.js +12 -0
- package/dist/types/common/index.d.cts +107 -42
- package/dist/types/common/index.d.ts +107 -42
- package/dist/types/{controller.enum-yd-fOr9m.d.cts → controller.enum-DTDn69N9.d.cts} +1 -1
- package/dist/types/{controller.enum-yd-fOr9m.d.ts → controller.enum-DTDn69N9.d.ts} +1 -1
- package/dist/types/core/index.d.cts +96 -26
- package/dist/types/core/index.d.ts +96 -26
- package/dist/types/decorator/index.d.cts +330 -185
- package/dist/types/decorator/index.d.ts +330 -185
- package/dist/types/enum/index.d.cts +9 -5
- package/dist/types/enum/index.d.ts +9 -5
- package/dist/types/{index-darLQYaE.d.cts → index-D7n0FBMm.d.ts} +639 -89
- package/dist/types/{index-CI7kQPXk.d.ts → index-DEP0NYrU.d.cts} +639 -89
- package/dist/types/interface/index.d.cts +2 -2
- package/dist/types/interface/index.d.ts +2 -2
- package/dist/types/{route-DIqsQIDN.d.cts → route-C-aMI7Sl.d.cts} +2 -1
- package/dist/types/{route-DIqsQIDN.d.ts → route-C-aMI7Sl.d.ts} +2 -1
- package/dist/types/testing/index.d.cts +310 -81
- package/dist/types/testing/index.d.ts +310 -81
- package/dist/types/{theme-resolvers-kvBzQHrv.d.cts → theme-resolvers-COHdX5g6.d.cts} +1 -1
- package/dist/types/{theme-resolvers-zrU0w_Bc.d.ts → theme-resolvers-DedxyscT.d.ts} +1 -1
- package/dist/types/{translator-dZuNvbo2.d.cts → translator-B4mc1lzJ.d.cts} +91 -35
- package/dist/types/{translator-Caddk39-.d.ts → translator-J6tIUfdp.d.ts} +91 -35
- package/meocord.eslint.cjs +5 -2
- package/meocord.eslint.d.cts +2 -2
- package/meocord.eslint.d.ts +2 -2
- package/meocord.eslint.mjs +5 -2
- package/package.json +20 -19
- package/dist/cjs/_shared/providers-CyQkBU8g.cjs +0 -2196
- package/dist/cjs/_shared/shard-context-BWzzLmaJ.cjs +0 -3243
- package/dist/cjs/_shared/translate-error-yLJa8q4Q.cjs +0 -316
- package/dist/esm/bin/builder-template/controller/controller.spec.template +0 -17
- package/dist/types/cooldown-store-BwDedkQk.d.cts +0 -154
- package/dist/types/cooldown-store-BwDedkQk.d.ts +0 -154
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,828 @@
|
|
|
1
1
|
# meocord
|
|
2
2
|
|
|
3
|
+
## 4.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#424](https://github.com/meocord/meocord/pull/424) [`43b9454`](https://github.com/meocord/meocord/commit/43b945447262405f61cced6660cd1d2be881d4b8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A mock guild's `members.fetch({ user: ids })` now resolves to a collection of each of those members, found in the cache or made the way `members.fetch(id)` makes one. It resolved to an empty collection, so in a test a message command naming two or more members by ID that the guild hasn't cached refused them all as not members of the server, while one such member resolved. Mentioned members were not affected: they come from the message's mentions.
|
|
8
|
+
|
|
9
|
+
- [#422](https://github.com/meocord/meocord/pull/422) [`22daff2`](https://github.com/meocord/meocord/commit/22daff26ac4a694a8d70eace38a4c038d3cef0ef) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `createMockClient()` is typed as a logged-in client, `DeepMocked<Client<true>>`, as the mock already behaves. Pass it straight to `createMockInteraction`, `createMockChannel`, `createMockUser` or `module.init({ ready: { client } })`: each of these failed to compile without a cast, since an interaction's client is `Client<true>`. `client.user` is no longer nullable, so `client.user!.id` can be `client.user.id`. Remove any `as unknown as Client<true>` cast on the mock.
|
|
10
|
+
|
|
11
|
+
- [#430](https://github.com/meocord/meocord/pull/430) [`1d0c0cf`](https://github.com/meocord/meocord/commit/1d0c0cf7d9755e5d24a0f218809a2f95fe403ccc) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Mocks from `meocord/testing` now compute what discord.js computes from them, where they returned `undefined` or a placeholder object:
|
|
12
|
+
|
|
13
|
+
- **Lookups:** a manager's `resolve()` and `resolveId()` read its cache, so `guild.members.resolve(id)` finds a member given to `createMockGuild`. `guild.members.me` is the bot's member, the cached one or one with @everyone. A guild's roles and channels, and the guilds of a client's messages and interactions, belong where discord.js puts them, and everything in a guild shares its client.
|
|
14
|
+
- **Ranking and permissions:** `role.comparePositionTo()`, `channel.permissionsFor()`, `member.permissionsIn()` and an interaction's `appPermissions` compute from the roles, the channel's `permissionOverwrites` and the bot's member. Being real, they keep working after `resetAllMocks()`.
|
|
15
|
+
- **Messages:** `message.member` reads the guild's member cache, so a test that removes the author's member there gets `null`, as discord.js gives for an author it hasn't cached. A default message now caches its author and the author's member, as Discord's message event does. `message.mentions.has(user)` answers from what the content mentions.
|
|
16
|
+
- **Defaults:** a role is not hoisted, managed or mentionable, with no icon, tags or colours. A member's `joinedTimestamp` is when the mock was made, so `partial` is `false`. A message has no `reference`, `poll` or stickers. `avatarURL()` and `iconURL()` are `null` without an avatar or icon, `displayAvatarURL()` is Discord's default avatar, and a mock client `isReady()`.
|
|
17
|
+
- **User context menus:** `targetUser` is the user with `targetId`, the client's cached one or one made, and `targetMember` that user's member in a server. A `targetId` given later picks its user, and both stay assignable.
|
|
18
|
+
|
|
19
|
+
- [#431](https://github.com/meocord/meocord/pull/431) [`b24a0bc`](https://github.com/meocord/meocord/commit/b24a0bca78b72c602718a5a8f496251484f37bfe) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A mock still reads some values discord.js computes as a truthy placeholder: a message's `editable`, `deletable`, `pinnable`, `crosspostable`, `bulkDeletable`, `hasThread` and `partial`, a member's `manageable`, `kickable`, `bannable` and `moderatable`, a role's `editable`, a channel's `viewable`, `manageable` and `deletable` and its thread and voice counterparts, and `partial` on users, channels and reactions. A test that reads one now gets a warning, once per run, that 5.0 computes it as discord.js does, and how to set it on the mock, such as `message.editable = false`, to test either way. A value the test sets is read without a warning.
|
|
20
|
+
|
|
21
|
+
`message.thread` is the thread the message's channel caches under the message's id, as discord.js reads it. Without one it is still a placeholder thread, with a warning that 5.0 gives `null` there.
|
|
22
|
+
|
|
23
|
+
- [#429](https://github.com/meocord/meocord/pull/429) [`961455d`](https://github.com/meocord/meocord/commit/961455d86534be61c2b22739240fa48d53564dd7) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Mocks from `meocord/testing` now pass to one another, and to the testing module, without a cast:
|
|
24
|
+
|
|
25
|
+
- **A mock reached through a property that may be `null`** keeps its mock API, such as `message.member?.fetch.mockResolvedValue(member)` or `message.guild?.members.fetch`.
|
|
26
|
+
- **An overloaded method's mock** takes what any of its overloads takes or resolves to, such as `guild.members.fetch.mockResolvedValue(member)` and `channel.messages.fetch.mockResolvedValue(message)`, where only the list form compiled before.
|
|
27
|
+
- **`createMockInteraction` given `guild: null`, `member: null` or a DM channel** builds an interaction outside a server, typed so its `guild` and `member` may be `null`.
|
|
28
|
+
- **`createMockMessage`** is typed as discord.js emits a message, so `module.emit('messageCreate', createMockMessage())` compiles.
|
|
29
|
+
- **`createMockChannel(ThreadChannel)`** is typed as a public or private thread, as discord.js types every thread, so it fits an interaction's `channel`, a guild's channels and the `threadCreate` event.
|
|
30
|
+
- **`getResponse(interaction).calls[n].payload`** is typed by the call's `method`: after `call.method === 'reply'`, it is the reply's options. `ResponseCall` itself is unchanged, so code that builds or extends one compiles as before.
|
|
31
|
+
|
|
32
|
+
## 4.1.0
|
|
33
|
+
|
|
34
|
+
### Highlights
|
|
35
|
+
|
|
36
|
+
- **Adds to 4.0.** Upgrading from 4.0, below, lists what may change for a 4.0 bot ([docs](https://meocord.dev/docs/4.1/whats-new)).
|
|
37
|
+
- **One way to answer.** `respond()`, `@Defer()` and presenters, in a theme's colours and emojis ([docs](https://meocord.dev/docs/4.1/responses)).
|
|
38
|
+
- **A pipeline around every handler.** Global guards, interceptors, filters, pipes, `@Cooldown` and observers, in one order ([docs](https://meocord.dev/docs/4.1/how-a-call-runs)).
|
|
39
|
+
- **Message command patterns.** Typed params, flags, aliases, usage replies and a built-in `!help` ([docs](https://meocord.dev/docs/4.1/message-commands)).
|
|
40
|
+
- **Sharding, providers and lifecycle.** Sharding in one process or a process per shard, with `ShardContext.call`, async providers, `onReady`/`onShutdown` and `app.stop()` ([docs](https://meocord.dev/docs/4.1/sharding)).
|
|
41
|
+
- **Localisation and testing.** Typed catalogs, and `MeoCordTestingModule` with Discord-shaped mocks ([docs](https://meocord.dev/docs/4.1/testing)).
|
|
42
|
+
|
|
43
|
+
### Upgrading from 4.0
|
|
44
|
+
|
|
45
|
+
- **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](https://meocord.dev/docs/4.1/migrating#upgrading-from-40-to-41)).
|
|
46
|
+
- **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](https://meocord.dev/docs/4.1/migrating#class-guards-now-cover-inherited-handlers)).
|
|
47
|
+
- **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](https://meocord.dev/docs/4.1/migrating#a-base-controllers-class-stages-cover-its-subclasses)).
|
|
48
|
+
- **`applyDecorators(A, B)` applies in stack order.** Guards run in the order listed; reverse them to keep 4.0's order ([guide](https://meocord.dev/docs/4.1/migrating#applydecorators-applies-its-decorators-in-the-order-they-stack)).
|
|
49
|
+
- **Message keywords ignore case, and one handler runs.** They match word by word; set `caseSensitive: true` to match case. Two handlers on one keyword stop the bot, and an app `prefix` applies to keywords; use `{ prefix: false }` for bare messages ([guide](https://meocord.dev/docs/4.1/migrating#message-keywords-match-in-any-case-and-only-one-runs)).
|
|
50
|
+
- **Bot reactions are ignored**, the bot's own included. Add `{ bots: true }` to a `@ReactionHandler` that needs them, and recheck counts that subtracted the bot's own reactions ([guide](https://meocord.dev/docs/4.1/migrating#reactions-from-bots-reach-no-handler)).
|
|
51
|
+
- **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](https://meocord.dev/docs/4.1/migrating#two-handlers-of-one-command-stop-the-bot)).
|
|
52
|
+
- **A re-declared inherited route takes the subclass's options.** Its own builder or `@MessageHandler`/`@ReactionHandler` options apply; to keep the base's, don't re-declare it ([guide](https://meocord.dev/docs/4.1/migrating#a-re-declared-handler-that-keeps-its-inherited-route-logs-a-warning)).
|
|
53
|
+
- **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](https://meocord.dev/docs/4.1/migrating#errors-after-a-reply-or-deferral-are-answered)).
|
|
54
|
+
- **`Theme` follows the app's theme, with new defaults,** as do MeoCord's own answers: `Theme.errorColor` is `#E3606D`, not `#DC3545`, and the success, info and warning colours change too. Set `@MeoCord({ theme: { colors } })` to keep 4.0's ([guide](https://meocord.dev/docs/4.1/migrating#theme-is-deprecated-and-its-colours-changed)).
|
|
55
|
+
- **MeoCord's metadata keys start with `meocord:`.** `SetMetadata` refuses only those and the DI keys (inversify's injectable flag, `design:paramtypes`); read MeoCord's metadata with `inspectHandler` ([guide](https://meocord.dev/docs/4.1/migrating#setmetadata-refuses-meocords-own-keys)).
|
|
56
|
+
- **Activities rotate in order, only when set.** Without `activities`, MeoCord leaves the presence alone; with them, it starts at ready.
|
|
57
|
+
- **Reactions use the cached message.** `reaction.message` is fetched only when held by id alone; call `reaction.message.fetch()` for fresh data.
|
|
58
|
+
- **customId params arrive decoded.** `%2F` and `%25` reach the handler as `/` and `%`; drop your own decoding.
|
|
59
|
+
- **Logs and exit codes.** `[DEBUG]` prints only in development unless `logLevel` or `MEOCORD_LOG_LEVEL=debug` asks; `info()`/`verbose()` print `[INFO]`/`[VERBOSE]`, objects as `console.log` does, and a signal after a failed login exits 1. Skip it in a 4.0 `main.ts` catch with `if (!isExplainedError(error))` from `meocord/common`, or a refused token or intent logs twice ([docs](https://meocord.dev/docs/4.1/configuration#logging)).
|
|
60
|
+
|
|
61
|
+
### Upgrading builds and the CLI
|
|
62
|
+
|
|
63
|
+
- **A mistyped config option stops `build`, `start` and `register`,** listing every problem; unknown options only warn ([guide](https://meocord.dev/docs/4.1/migrating#a-config-option-of-the-wrong-type-stops-the-cli)).
|
|
64
|
+
- **Config and assets load beside `dist/main.js`.** `.env` still loads from the working directory, so set it there when starting elsewhere. Rebuild ([guide](https://meocord.dev/docs/4.1/migrating#smaller-changes)).
|
|
65
|
+
- **`NODE_ENV` follows the mode.** `build --prod` compiles `meocord.config.ts` as production, and `start --dev` runs the bot as development whatever the shell sets. On Bun, start production with `NODE_ENV=production`, or `.env.development` loads first, with a warning ([guide](https://meocord.dev/docs/4.1/migrating#smaller-changes)).
|
|
66
|
+
- **No `eval` devtool.** Development builds use `cheap-module-source-map`, and `eval-*` becomes its non-eval form with a warning. Set `sourceMappedStacks: false` for trackers that apply uploaded source maps ([docs](https://meocord.dev/docs/4.1/configuration#stack-traces)).
|
|
67
|
+
- **`meocord/eslint` flags unawaited promises.** `await`, `return` or `void` them, or turn the rule off ([guide](https://meocord.dev/docs/4.1/migrating#smaller-changes)).
|
|
68
|
+
- **The generated rate-limit guard never limited.** Move its map to module level, or use `@Cooldown` ([guide](https://meocord.dev/docs/4.1/migrating#the-generated-rate-limit-guard-limits)).
|
|
69
|
+
- **What a 4.0 app can copy.** New apps read every `.env` file and type asset imports; add the `swc` `include` for coverage of untested files. An app created for pnpm adds `reflect-metadata`, `@types/node` and a `pnpm-workspace.yaml`, and one installed with npm 11.16+ adds `allowScripts` to `package.json` ([guide](https://meocord.dev/docs/4.1/migrating#installing-with-pnpm-or-npm-1116-and-later)).
|
|
70
|
+
|
|
71
|
+
### Upgrading your tests
|
|
72
|
+
|
|
73
|
+
- **Mocks have distinct snowflake ids.** Pass `{ user: first.user }` to share a user. A mock without `guildId` is a DM, a given channel sets `channelId`, `guildId` and `guild`, and a member given to `createChatInputOptions` fills a `User` param with the `User`, where 4.0 passed the `GuildMember` ([guide](https://meocord.dev/docs/4.1/migrating#smaller-changes)).
|
|
74
|
+
- **Handler arguments are type-checked.** Select menu choices and plain-string customId params that never matched now fail to compile ([guide](https://meocord.dev/docs/4.1/migrating#smaller-changes)).
|
|
75
|
+
- **Changed messages.** Load-time refusals start with `Class.method:`, and a `@Command` given the wrong interaction names the handler, not "Invalid interaction type passed to @Command". A modal mock's `isFromMessage()` needs a `message`, and autocomplete `respond()` rejects over 25 choices.
|
|
76
|
+
- **Option mocks resolve as Discord's.** `getMember()` returns the member, `null` in a DM. A getter of another type, `getInteger()` on a fraction (use `getNumber()`), `getChannel()` on a channel type the option doesn't allow, or `getSubcommand()` with no subcommand throws discord.js's error, where 4.0's mock returned `null` (`getSubcommand(false)` reads none as `null`); only a user or member read as a role, or the reverse, stays `null`.
|
|
77
|
+
- **Load `.env` in tests yourself,** in the vitest setup or with `dotenv`, if a suite relied on an earlier build for it.
|
|
78
|
+
|
|
79
|
+
### Responses and presenters
|
|
80
|
+
|
|
81
|
+
- **`respond(interaction)` answers any interaction**: acknowledge, send, edit, follow up or report an error as its state allows, in user-installed apps too ([docs](https://meocord.dev/docs/4.1/responses)).
|
|
82
|
+
- **`@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](https://meocord.dev/docs/4.1/defer)).
|
|
83
|
+
- **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](https://meocord.dev/docs/4.1/presenters)).
|
|
84
|
+
- **Unanswered handlers are named** in a development warning, which `@MeoCord({ warnUnanswered })` toggles.
|
|
85
|
+
|
|
86
|
+
### Handlers and the call pipeline
|
|
87
|
+
|
|
88
|
+
- **One fixed pipeline** runs every handler: guards, then interceptors around validation, pipes, cooldowns and the handler, inside exception filters ([docs](https://meocord.dev/docs/4.1/how-a-call-runs)).
|
|
89
|
+
- **Guards** can be global, in `@MeoCord({ guards })`; they read typed facts through `ExecutionContext` and `createMetadata`, and throw `GuardDeniedError` to say why ([docs](https://meocord.dev/docs/4.1/guards)).
|
|
90
|
+
- **Interceptors** wrap a handler, for timing, logging, caching or mapping errors ([docs](https://meocord.dev/docs/4.1/interceptors)).
|
|
91
|
+
- **Exception filters and `UserError`** decide what the user sees when a call throws; a `UserError` answers privately, or in a reply that doesn't ping ([docs](https://meocord.dev/docs/4.1/exception-filters)).
|
|
92
|
+
- **`@Validate` and pipes** check input with any Standard Schema library and reshape it ([docs](https://meocord.dev/docs/4.1/validation)).
|
|
93
|
+
- **Typed options**: each `{ provide, params }` is checked against the params its stage declares, and `MeoCordOptions` names what `@MeoCord` takes.
|
|
94
|
+
- **Observers** hear each call start and settle, with its outcome and duration ([docs](https://meocord.dev/docs/4.1/observers)).
|
|
95
|
+
- **`@On` and `@Once`** handle any discord.js event, on a controller or a service, through the same pipeline ([docs](https://meocord.dev/docs/4.1/gateway-events)).
|
|
96
|
+
- **`HandlerRegistry`** lists every handler with its metadata ([docs](https://meocord.dev/docs/4.1/handler-discovery)).
|
|
97
|
+
- **Startup checks** name dead handlers, shadowing handlers or builders, and missing intents or partials before login; a refused app gets one line naming `Class.method` and exits 1.
|
|
98
|
+
|
|
99
|
+
### Theming
|
|
100
|
+
|
|
101
|
+
- **Themes** name colours, emojis and button styles by role, set with `@MeoCord({ theme })`, `@UseTheme` or per server and user with `themeFor` and `ThemeCache`; `useTheme()` reads them, and apps add roles ([docs](https://meocord.dev/docs/4.1/theming)).
|
|
102
|
+
- **`ThemeResolver` classes** look themes up with the app's services: `themeFor` takes one, resolved from the container, cached as the functions are ([docs](https://meocord.dev/docs/4.1/theming#per-server-and-per-user)).
|
|
103
|
+
- **`respond()` fills in colour**: an answer without one takes `primary`, error views take `warning` or `danger`, and `{ fill: false }` sends as written.
|
|
104
|
+
|
|
105
|
+
### Cooldowns
|
|
106
|
+
|
|
107
|
+
- **`@Cooldown`** limits a handler per user, channel, server, everyone or a `by` value; cooldowns stack, count only calls that pass guards and validation, and show the wait as a Discord timestamp ([docs](https://meocord.dev/docs/4.1/cooldowns)).
|
|
108
|
+
- **Cooldown stores**: memory by default, `ShardedCooldownStore` across shards, `RedisCooldownStore` on Redis or Valkey, or your own, checked with `testCooldownStore`; `cooldownStoreFailure` decides calls while one is down ([docs](https://meocord.dev/docs/4.1/recipes/cooldown-stores)).
|
|
109
|
+
|
|
110
|
+
### Message commands
|
|
111
|
+
|
|
112
|
+
- **Patterns**: `@MessageHandler('roll {sides:int} {note...?}')` matches after a prefix or mention, with typed params such as `int`, `member` or the app's own, flags and lists, checked at compile time ([docs](https://meocord.dev/docs/4.1/message-params)).
|
|
113
|
+
- **Starts**: `messages: { prefix, mention, caseSensitive }` sets how commands start; `mention: 'only'` needs no MessageContent intent.
|
|
114
|
+
- **Answers**: a misfit gets its usage; `aliases`, `description` and `scope` describe a command, `help` adds `!help`, and `dmOnError` and `dmOnCooldown` DM the author ([docs](https://meocord.dev/docs/4.1/message-commands)).
|
|
115
|
+
- **Fetched after guards**: named members, users and channels are fetched only once guards pass, uncached members together; a role is read from the cache.
|
|
116
|
+
|
|
117
|
+
### Components and routing
|
|
118
|
+
|
|
119
|
+
- **Typed customId params** such as `{count:int}` arrive as values, and `route()` builds matching ids, checked at compile time ([docs](https://meocord.dev/docs/4.1/components)).
|
|
120
|
+
- **Select choices and modal fields arrive in params**, with resolved users, members, roles and channels, and file uploads as `Attachment`s.
|
|
121
|
+
- **Context menu handlers are typed** from the builder's `setType()` ([docs](https://meocord.dev/docs/4.1/context-menus)).
|
|
122
|
+
- **Reactions match a custom emoji by id** as well as by name ([docs](https://meocord.dev/docs/4.1/reactions)).
|
|
123
|
+
|
|
124
|
+
### Sharding
|
|
125
|
+
|
|
126
|
+
- **`sharding`** runs shards in one process or, with `mode: 'process'`, one each; the manager registers once, restarts exited shards, stops when all would fail, and shuts shards down through their hooks ([docs](https://meocord.dev/docs/4.1/sharding)).
|
|
127
|
+
- **`ShardContext.call`** runs a service method in every shard, each result typed as the JSON it arrives as, in tests too.
|
|
128
|
+
|
|
129
|
+
### Providers and lifecycle
|
|
130
|
+
|
|
131
|
+
- **Providers** supply values, classes and async factories under any token, `createToken` included, injected with `@Inject`; `factoryProvider` types a factory ([docs](https://meocord.dev/docs/4.1/services)).
|
|
132
|
+
- **`OnReady` and `OnShutdown`** run in dependency order on controllers, services, providers and the cooldown store, within `shutdownTimeout` ([docs](https://meocord.dev/docs/4.1/lifecycle-hooks)).
|
|
133
|
+
- **`app.stop()`** shuts the bot down from code, as a signal does.
|
|
134
|
+
|
|
135
|
+
### Testing
|
|
136
|
+
|
|
137
|
+
- **`invoke` and `dispatch`** run a handler, or route an interaction, message or reaction, through the pipeline; `getResponse` shows what was sent ([docs](https://meocord.dev/docs/4.1/invoke-and-dispatch)).
|
|
138
|
+
- **`MeoCordTestingModule.fromApp(App)`** wires the whole app as the bot does, with `override*()`; `init({ ready: true })`, `close()` and `emit()` run hooks and events ([docs](https://meocord.dev/docs/4.1/testing)).
|
|
139
|
+
- **Mocks behave like discord.js**, with members, roles, permissions, channels and locales; `createMockMember`, `createMockMessage` and the rest take properties, a message its `author`, and `createMockInteraction` Discord's `authorizingIntegrationOwners` map ([docs](https://meocord.dev/docs/4.1/mocks)).
|
|
140
|
+
- **Checks**: `inspectHandler`, `resolveRoute`, `expectCompleteCatalog`, `testCooldownStore` and `resetAllMocks()`.
|
|
141
|
+
|
|
142
|
+
### Localisation
|
|
143
|
+
|
|
144
|
+
- **`createTranslator`** checks 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](https://meocord.dev/docs/4.1/localisation)).
|
|
145
|
+
- **MeoCord's own texts** translate through a `meocord` group in the app's catalog.
|
|
146
|
+
|
|
147
|
+
### The CLI and builds
|
|
148
|
+
|
|
149
|
+
- **Command registration** goes global, per server or to a dev server, at startup or with `meocord register` over REST; unchanged dev commands skip resending unless `--force-register` ([docs](https://meocord.dev/docs/4.1/slash-commands#registering-commands)).
|
|
150
|
+
- **`meocord create`** commits the lockfile, adds `npm start`, writes samples whose specs test what they answer, and makes its first commit by running git directly, so MeoCord no longer installs `simple-git`.
|
|
151
|
+
- **`meocord generate`** adds observers, filters, interceptors and pipes, a customId per component, and specs that test the answer.
|
|
152
|
+
- **`meocord start --dev`** restarts on source, config, `tsconfig.json` and dev `.env` changes, one bot at a time, keeping the last good build while code doesn't compile ([docs](https://meocord.dev/docs/4.1/cli)).
|
|
153
|
+
- **`.env` files**: a new app reads `.env.<mode>.local`, `.env.local`, `.env.<mode>` and `.env` on every runtime.
|
|
154
|
+
- **Self-contained builds** pack native addons from any package manager and run on Bun too; `optionalExternals` covers optional packages ([docs](https://meocord.dev/docs/4.1/self-contained-builds)).
|
|
155
|
+
- **Source-mapped stacks and `logLevel`**: traces name your source lines in production too, and `logLevel` or `MEOCORD_LOG_LEVEL` sets what prints ([docs](https://meocord.dev/docs/4.1/configuration)).
|
|
156
|
+
- **`dist/cli.json`** describes every CLI command and option as data.
|
|
157
|
+
|
|
158
|
+
### Deprecations
|
|
159
|
+
|
|
160
|
+
- **Names removed in 5.0.** Each still works and its JSDoc names the replacement. A new app's ESLint config sets `@typescript-eslint/no-deprecated` to warn outside specs, which finds them; add it to a 4.0 app's config.
|
|
161
|
+
- `Theme`: use `useTheme().colors` and `@MeoCord({ theme })`; reading or setting a colour warns once ([guide](https://meocord.dev/docs/4.1/migrating#theme-is-deprecated-and-its-colours-changed)).
|
|
162
|
+
- `SetMetadata` and string metadata keys: use `createMetadata` and `ExecutionContext.get(decorator)`; each warns once ([guide](https://meocord.dev/docs/4.1/migrating#setmetadata-and-string-metadata-keys-are-deprecated)).
|
|
163
|
+
- `ReactionHandlerOptions`: renamed `ReactionEvent` ([guide](https://meocord.dev/docs/4.1/migrating#reactionhandleroptions-is-now-reactionevent)).
|
|
164
|
+
- `MetadataKey`, `CommandMetadata`, `AutocompleteMetadata`: internal; drop the import ([guide](https://meocord.dev/docs/4.1/migrating#metadatakey-commandmetadata-and-autocompletemetadata-are-deprecated)).
|
|
165
|
+
- **`@Autocomplete<void>` loses its type parameter in 5.0.** Lint misses it: search for `@Autocomplete<` and write `@Autocomplete(…)`.
|
|
166
|
+
- **Retrying `start()` after a failed login warns,** logging in again with its handlers, and rejects in 5.0. Make a new app with `MeoCordFactory.create` per attempt; retrying after a provider failure stays supported ([guide](https://meocord.dev/docs/4.1/migrating#retrying-start-after-a-failed-login-is-deprecated)).
|
|
167
|
+
- **`@Controller`, `@Service`, `@Guard`, `@CommandBuilder` or `@MeoCord` on a method** applies nothing, as in 4.0, and warns; 5.0 refuses it. Move it to the class ([guide](https://meocord.dev/docs/4.1/migrating#a-class-decorator-on-a-method-logs-a-warning)).
|
|
168
|
+
- **`@MessageHandler('')` warns,** and 5.0 refuses it. Write `@MessageHandler()` ([guide](https://meocord.dev/docs/4.1/migrating#messagehandler-logs-a-warning)).
|
|
169
|
+
- **Handlers that never run warn at startup,** and 5.0 refuses to start:
|
|
170
|
+
- a command or autocomplete handler Discord never sends, such as an unregistered subcommand path, a renamed builder or an option without autocomplete ([guide](https://meocord.dev/docs/4.1/migrating#a-command-handler-discord-never-sends-logs-a-warning));
|
|
171
|
+
- a second `@Autocomplete` for one option ([guide](https://meocord.dev/docs/4.1/migrating#a-second-autocomplete-for-one-option-logs-a-warning));
|
|
172
|
+
- a command, message, reaction or autocomplete handler on a class that isn't a controller ([guide](https://meocord.dev/docs/4.1/migrating#a-handler-on-a-class-that-isnt-a-controller-logs-a-warning)).
|
|
173
|
+
- **Changes in 5.0, warned now:**
|
|
174
|
+
- a re-declared handler on another route still answers its inherited one; in 5.0 its own routes replace it ([guide](https://meocord.dev/docs/4.1/migrating#a-re-declared-handler-that-keeps-its-inherited-route-logs-a-warning));
|
|
175
|
+
- between equally specific overlapping component patterns, the first listed runs; in 5.0, the more spelled-out one does ([guide](https://meocord.dev/docs/4.1/migrating#overlapping-component-patterns-meocord-5-prefers-the-one-that-spells-out-more)).
|
|
176
|
+
|
|
177
|
+
### Fixes to 4.0 behaviour
|
|
178
|
+
|
|
179
|
+
- **Running the bot**
|
|
180
|
+
- Retrying `app.start()` after a failed login no longer attaches every handler twice.
|
|
181
|
+
- `node dist/main.js` has the config's `.env` values before the app's modules run. Rebuild.
|
|
182
|
+
- A refused privileged intent, or a refused or empty token, is explained in one line with where to fix it.
|
|
183
|
+
- A builder that can't be built is named with its command in the registration error.
|
|
184
|
+
- 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.
|
|
185
|
+
- One SIGINT and SIGTERM listener per process: many apps no longer trigger `MaxListenersExceededWarning`.
|
|
186
|
+
- A signal during login stops the bot at once, with "Bot has shut down".
|
|
187
|
+
- A missing or broken built config is reported once, with the file, reason and fix.
|
|
188
|
+
- Reactions in uncached DMs reach their handlers again on discord.js 14.26.2 and later.
|
|
189
|
+
- **Routing and dispatch**
|
|
190
|
+
- A click a discord.js collector answers no longer gets "Command not found!".
|
|
191
|
+
- A user and a message context menu of one name each reach their own handler.
|
|
192
|
+
- Overlapping component patterns are warned about at startup, not the first click.
|
|
193
|
+
- A subclass controller no longer adds its handlers to its base class.
|
|
194
|
+
- **Dependency injection**
|
|
195
|
+
- A subclass with its own constructor gets its own dependencies.
|
|
196
|
+
- `@inject(Token)` on an interface-typed parameter works, not "missing metadata on type Object".
|
|
197
|
+
- An injection cycle is refused naming it, not "Circular dependency found: (No dependency trace)".
|
|
198
|
+
- A constructor parameter with no runtime type is refused naming the class and parameter, not inversify's `emitDecoratorMetadata` error.
|
|
199
|
+
- 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.
|
|
200
|
+
- A guard in `services` or `providers`, or injected into a service, reads each call's own `params`; one with a setter param or a sealed instance shares them, with a warning.
|
|
201
|
+
- 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.
|
|
202
|
+
- **Handler types**
|
|
203
|
+
- A handler may return a value or take fewer parameters, not "Unable to resolve signature of method decorator".
|
|
204
|
+
- `applyDecorators` passes on what a wrapping decorator returns.
|
|
205
|
+
- **Builds**
|
|
206
|
+
- Production builds keep class names, where a clash renamed `Shop` to `shop_controller_Shop`.
|
|
207
|
+
- Imported files land in `dist/assets` under their own names; a WebAssembly module takes a content hash.
|
|
208
|
+
- On Windows, `new URL('./file', import.meta.url)` gives a `file:` URL, not a `c:` path `fileURLToPath` refused with `ERR_INVALID_URL_SCHEME`. Rebuild.
|
|
209
|
+
- A `tsconfig.json` with `extends`, `files`, `typeRoots`, comments or `baseUrl` paths builds as TypeScript reads it, is never rewritten, and concurrent builds no longer clash.
|
|
210
|
+
- 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.
|
|
211
|
+
- **The CLI**
|
|
212
|
+
- `meocord start` forwards SIGINT and SIGTERM, so Docker, pm2 and systemd stop the bot cleanly.
|
|
213
|
+
- `start --dev` runs 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 an `rsbuild` hook throws.
|
|
214
|
+
- `start --dev --build` builds once, and `start --dev` exits 1 when watching can't start.
|
|
215
|
+
- `create` keeps 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, not `Directory "" already exists`.
|
|
216
|
+
- A created app passes `lint` and `test` on pnpm and installs on pnpm 11+ and npm 11.16+ without warnings; `create` warns on Node.js 22.0 to 22.12.
|
|
217
|
+
- `generate` works on Windows, writes lint-clean files formatted in one ESLint run, and refuses a name outside `src/`, with `\` separating folders on Windows.
|
|
218
|
+
- `build` and `start --prod` don't clear the screen, `start --dev` keeps scrollback, and no command writes screen-clearing codes into piped output.
|
|
219
|
+
- `--help` no longer prints "No available choices.".
|
|
220
|
+
- The CLI runs at the filesystem root, not "Cannot locate the "MeoCord" package directory".
|
|
221
|
+
- `require('meocord/package.json')` resolves, and `meocord/eslint` ignores `coverage/`.
|
|
222
|
+
- **Logging**
|
|
223
|
+
- `Logger` prints any value, a `Symbol` included, and colours a line only when its own stream is a terminal.
|
|
224
|
+
- Log lines escape what a user sent, and shorten long message text, with its length.
|
|
225
|
+
- A project that loads meocord with `require()`, as CommonJS code or Jest in CommonJS mode does, logs again: every `Logger` method threw `chalk.bold is not a function`. Built bots were not affected.
|
|
226
|
+
- The CLI and tests no longer read the app's name or `.env` from a stale `dist`.
|
|
227
|
+
- **Testing**
|
|
228
|
+
- Mocks have an `'en-US'` locale, `createdTimestamp` and `createdAt`, a working `inGuild()` and resolving promise methods.
|
|
229
|
+
- A mock interaction without a `client` gets one from `createMockClient`, and `getAttachment()` returns the `Attachment` given, or `null`.
|
|
230
|
+
- `createMockChannel` takes `ThreadChannel`, stubs `threads.create` on text, announcement, forum and media channels, and gives a subclass its base's managers.
|
|
231
|
+
|
|
232
|
+
### Security
|
|
233
|
+
|
|
234
|
+
- **`Logger` no longer writes the bot token to logs** ([GHSA-62w2-fp4p-4jq8](https://github.com/meocord/meocord/security/advisories/GHSA-62w2-fp4p-4jq8)). In 4.0.0, a discord.js object a bot logged through `Logger`, 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: `Logger` prints objects as `console.log` does 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.
|
|
235
|
+
|
|
236
|
+
## 4.1.0-beta.12
|
|
237
|
+
|
|
238
|
+
### Minor Changes
|
|
239
|
+
|
|
240
|
+
- [#411](https://github.com/meocord/meocord/pull/411) [`abaf630`](https://github.com/meocord/meocord/commit/abaf630dbb833940238590d71e7efc33809a3fa7) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `@MeoCord({ themeFor })` takes a class implementing the new `ThemeResolver` interface, so a theme can be looked up with the app's services, such as a user's saved choice in a database a provider connects. Its `guild()` and `user()` methods are optional and typed as the functions are. The class is resolved from the app's container like the cooldown store: it isn't listed in `providers`, its constructor injects the app's services and providers, and it runs `OnReady` and `OnShutdown` in dependency order. Its results are cached, timed out and logged as the functions' are, and `ThemeCache` clears them. In tests, `fromApp` binds it, `overrideProvider` replaces it or what it injects, and `overrideThemeFor` takes a class too. See [Theming](https://meocord.dev/docs/4.1/theming).
|
|
241
|
+
|
|
242
|
+
### Patch Changes
|
|
243
|
+
|
|
244
|
+
- [#412](https://github.com/meocord/meocord/pull/412) [`c934277`](https://github.com/meocord/meocord/commit/c9342771cf87c894e2f0452e1450b1b6d28a74b5) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A class whose constructor injects but which has no decorator is refused as the app is created, naming the class and the decorator to add, rather than with inversify's `Found unexpected missing metadata on type …` error, at startup or, for a guard, at its first call. The advice is the class's own: `@Controller()` for a listed controller; `@Guard()`, `@Interceptor()`, `@Catch()` or `@Pipe()` for a guard, interceptor, filter or pipe, global or on a handler; `@Service()`, or a provider in `@MeoCord({ providers })` for a class from a package, for any other. Injecting one of MeoCord's classes an app makes itself, such as `Logger` or an error, is refused saying how to make it instead. A subclass whose constructor takes its decorated base's types, a parameter with a default value and a rest parameter are created as before.
|
|
245
|
+
|
|
246
|
+
## 4.1.0-beta.11
|
|
247
|
+
|
|
248
|
+
### Minor Changes
|
|
249
|
+
|
|
250
|
+
- [#401](https://github.com/meocord/meocord/pull/401) [`ae9d548`](https://github.com/meocord/meocord/commit/ae9d548775b552e1af20b12199b56dd87c67c90f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - MeoCord asks for `discord.js` 14.27.0 or later and `dotenv` 18.0.5 or later as peers. A bot on an older `discord.js` 14 or `dotenv` 18 upgrades them with MeoCord: `npm install discord.js@^14.27.0 dotenv@^18.0.5`, or the same with your package manager.
|
|
251
|
+
|
|
252
|
+
MeoCord's own dependencies move to their latest stable releases, and `meocord create` makes the new app's first commit by running `git` directly, so installing MeoCord no longer pulls in `simple-git`. A new app starts on the latest stable releases of its tools, with TypeScript held at 6.0.3.
|
|
253
|
+
|
|
254
|
+
### Patch Changes
|
|
255
|
+
|
|
256
|
+
- [#403](https://github.com/meocord/meocord/pull/403) [`ec97a2f`](https://github.com/meocord/meocord/commit/ec97a2f4a9172c9ed6b9ddb99f7378353f1a0e7b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A self-contained build that copies one package into `dist/node_modules` says "holds 1 package", not "1 packages".
|
|
257
|
+
|
|
258
|
+
- [#407](https://github.com/meocord/meocord/pull/407) [`4d91448`](https://github.com/meocord/meocord/commit/4d91448a936da9c11feb5aa2bca3ddb013b95275) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `require('meocord/common')`, and every other entry point a CommonJS project loads with `require()`, logs again: `Logger`'s `log()`, `info()`, `verbose()` and `debug()` threw `TypeError` there, as the CommonJS build read `chalk`, a package that ships only as an ES module, without its default export. A bot built with `meocord build` was not affected, since its bundle loads the ES module build.
|
|
259
|
+
|
|
260
|
+
- [#410](https://github.com/meocord/meocord/pull/410) [`ca34a30`](https://github.com/meocord/meocord/commit/ca34a30335d72d0b6fef87628fb667adf8bddd2c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A decorator applied where it can't work is now named where it applies, directly or inside an `applyDecorators` composite:
|
|
261
|
+
|
|
262
|
+
- **A decorator that goes only on a method, on a class,** stops the bot as the class loads, with one line naming the class: `Shop: @Defer goes on a method, not on a class.` This covers `@Command`, `@Autocomplete`, `@MessageHandler`, `@ReactionHandler`, `@On`, `@Once`, `@Validate`, `@UsePipe` and `@Defer`, and `@Inject`, which goes on a constructor parameter or a property. `@Defer`, `@Validate`, `@UsePipe`, `@On` and `@Once` did nothing there, and the handler decorators failed with a `TypeError`.
|
|
263
|
+
- **`@Interceptor`, `@Catch`, `@Pipe` and `@Observer` on a method** stop the bot the same way: `Shop.buy: @Interceptor goes on a class, not on a method.`
|
|
264
|
+
- **`@Controller`, `@Service`, `@Guard`, `@CommandBuilder` and `@MeoCord` on a method** still apply nothing, as in 4.0, and now log a warning once: `@Guard on the method Shop.buy is deprecated; in the next major version (5.0) it is refused. Use @Guard on a class instead.` Move the decorator to the class.
|
|
265
|
+
|
|
266
|
+
Decorators that go on a class or a method, such as `@UseGuard`, `@Cooldown`, `@UseTheme` and those `createMetadata` makes, apply as before. See [Custom decorators](https://meocord.dev/docs/4.1/custom-decorators).
|
|
267
|
+
|
|
268
|
+
- [#409](https://github.com/meocord/meocord/pull/409) [`ca62ec5`](https://github.com/meocord/meocord/commit/ca62ec523545205fedadd4516ed96ae52655e1e7) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `createChatInputOptions` reads options as discord.js does, and throws discord.js's own errors: each a `DiscordjsTypeError` with discord.js's code and message.
|
|
269
|
+
|
|
270
|
+
- A getter of another type throws discord.js's type error, such as `Option "x" is of type: 4; expected 3.`, with or without `required`: `getString()` on a number, `getInteger()` on a fraction, `getChannel()` on a user. A user or member read as a role, or a role read as a user or member, is `null`, or that error when `required`, as the option may be a mentionable one.
|
|
271
|
+
- A missing required option throws `Required option "x" not found.`, `getSubcommand()` throws `No subcommand specified for interaction.` unless given `false`, as in discord.js, `getSubcommandGroup(true)` and `getFocused()` throw discord.js's messages, and `getChannel()` checks the channel types it's given.
|
|
272
|
+
- `get()` returns the option as discord.js does, and `getMessage()` reads no option of a slash command.
|
|
273
|
+
- Options assigned to a mock interaction after it is created, as with `interaction.options = createChatInputOptions({ target })`, belong to it, so `getMember()` is the server's cached member, or `null` in a DM.
|
|
274
|
+
|
|
275
|
+
- [#406](https://github.com/meocord/meocord/pull/406) [`6958c64`](https://github.com/meocord/meocord/commit/6958c6422454bf6b11fe1ebdfc3a9ab20a873f43) Thanks [@l7aromeo](https://github.com/l7aromeo)! - MeoCord keeps its own metadata under keys beginning `meocord:`, so `SetMetadata` takes any key a 4.0 bot used, such as `'guards'` or `'commandType'`, and stores it as 4.0 did. It refuses only a key beginning `meocord:` and the two keys dependency injection reads, `design:paramtypes` and inversify's injectable flag. A 4.1 beta refused `'guards'`, `'commandType'` and `'inversify:container'`, so a 4.0 bot that set one of them failed to load.
|
|
276
|
+
|
|
277
|
+
Code that read MeoCord's guard list or a builder's command type under `'guards'` or `'commandType'` no longer finds them there. To read the guards a handler runs, use `inspectHandler` from `meocord/testing`.
|
|
278
|
+
|
|
279
|
+
Every key MeoCord keeps its metadata under is now such a string, so a test or a tool that loads both meocord's CommonJS and ES module entries sees a controller one of them decorated from the other, where it found none.
|
|
280
|
+
|
|
281
|
+
- [#402](https://github.com/meocord/meocord/pull/402) [`2be278a`](https://github.com/meocord/meocord/commit/2be278a504158721fc6ab2f6bb6d2e6773e94ce4) Thanks [@l7aromeo](https://github.com/l7aromeo)! - - **A bot that imports two or more WebAssembly modules builds again.** Each imported module is written to `dist/assets` under a content hash, as `<hash>.module.wasm`. A wasm file read through `new URL('./file.wasm', import.meta.url)` keeps its own name.
|
|
282
|
+
- **`new URL('./file', import.meta.url)` for a bundled file gives a `file:` URL on Windows.** It was built on the bundle's directory as a disk path, which a URL reads as scheme `c:`, so `fileURLToPath` threw `ERR_INVALID_URL_SCHEME`. The URL is now relative to the bundle on every platform.
|
|
283
|
+
|
|
284
|
+
## 4.1.0-beta.10
|
|
285
|
+
|
|
286
|
+
### Minor Changes
|
|
287
|
+
|
|
288
|
+
- [#295](https://github.com/meocord/meocord/pull/295) [`a304494`](https://github.com/meocord/meocord/commit/a304494919b8a4cda5d1cf6609caf534ee8d96cc) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Stop a bot from code with `app.stop()`. It runs the `onShutdown` hooks under your `shutdownTimeout` and closes the client, so an owner-only shutdown command, a graceful restart or an integration test needs no signal. A bot in one process keeps its process running. With process sharding, the manager's `stop()` asks every shard to shut down and waits for it, and keeps the manager running. Called in a shard, `stop()` asks the manager to stop every shard, so it stops the bot whichever process calls it; that shard's process ends with the others. A stop while the bot logs in ends that login, so its `start()` rejects. Calls after the first wait for it, and a stopped app does not start again: use `MeoCordFactory.create` to make a new one. A client that fails to close, or a shard the manager has to kill, sets `process.exitCode` to 1, unless another code is set, so the process exits as a signal's shutdown would. A signal, or a shard manager's request, that comes while `stop()` runs waits for its hooks to finish rather than exiting at once.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const app = MeoCordFactory.create(App)
|
|
292
|
+
await app.start()
|
|
293
|
+
// later
|
|
294
|
+
await app.stop()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
A SIGINT or SIGTERM after a failed login now exits with the code the failed login set, 1, where 4.0 exited 0, so a process supervisor no longer reads a bot that never came online as a clean stop. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#smaller-changes).
|
|
298
|
+
|
|
299
|
+
- [#310](https://github.com/meocord/meocord/pull/310) [`79015cf`](https://github.com/meocord/meocord/commit/79015cfff42a11e817ab18bb0e66edc5153bab49) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `applyDecorators(A, B)` now applies its decorators as `@A @B` does, stacked in the order written: `B` first, then `A`. It applied them the other way round, so guards listed in it ran in the reverse of the order written, and moving stacked decorators into `applyDecorators` changed what ran first. A method or class a decorator returns in place of the one it was given, as a wrapping decorator does, now reaches the next decorator and TypeScript; it was dropped.
|
|
300
|
+
|
|
301
|
+
`applyDecorators(UseGuard(A), UseGuard(B))` now runs `A` before `B`. To keep 4.0's order, write `applyDecorators(UseGuard(B), UseGuard(A))`. The same holds for interceptors, pipes and filters composed this way. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#applydecorators-applies-its-decorators-in-the-order-they-stack).
|
|
302
|
+
|
|
303
|
+
- [#326](https://github.com/meocord/meocord/pull/326) [`a3c2a2e`](https://github.com/meocord/meocord/commit/a3c2a2e5f6e116d80ce3112b63df99687dc46c63) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A catalog message shows a brace as text when it is written twice, and a translator says when a key has no message.
|
|
304
|
+
|
|
305
|
+
- `{{` and `}}` are one brace each, when the code compiles and when translating, so `'Buttons use ticket/{{id}}'` shows `ticket/{id}` and takes no `id` param. A 4.1 beta catalog that writes `{{name}}` for a param inside braces now shows `{name}` as written; write `{{{name}}}` instead.
|
|
306
|
+
- `localizations(key)` takes only a message without `{params}`, as Discord shows a name or description as written. Another key fails to compile, or, in a catalog the compiler can't read, such as a JSON file, throws as the app loads. A translation with a `{param}` is left out, so Discord shows the default for that locale, and `expectCompleteCatalog` reports it.
|
|
307
|
+
- In development, a key with no message, or one naming a group of messages, logs a warning once, since the key is shown in its place.
|
|
308
|
+
|
|
309
|
+
See [Localisation](https://meocord.dev/docs/4.1/localisation).
|
|
310
|
+
|
|
311
|
+
- [#399](https://github.com/meocord/meocord/pull/399) [`24f33e2`](https://github.com/meocord/meocord/commit/24f33e2bc0652cee90ba5cf436d7cc4808fbbd22) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Fixes in `meocord/common`:
|
|
312
|
+
|
|
313
|
+
- **A send Discord could not read is logged as an error.** When a reply, an edit, a direct message or an acknowledgement fails, MeoCord still logs a refusal for something Discord reports at debug level, such as a missing permission, an interaction already answered or a message already gone. A body Discord could not read is logged as an error with its cause: an invalid form body (`50035`), invalid JSON (`50109`) or an empty message (`50006`). Only the code that built the request can fix those, and they were logged at debug, where they went unseen.
|
|
314
|
+
- **`RedisCooldownStore` on Redis Cluster counts a call against all of a handler's keys or none.** When a handler's cooldowns sit in different slots, each key is counted by a script of its own. If a later key's script fails, the uses already counted are now given back before the failure is reported, so the call does not leave earlier keys counted for a call that was refused.
|
|
315
|
+
- **`RedisCooldownStore`'s error for a reply it cannot read** describes the reply it expects, including the wait's end on a refusal.
|
|
316
|
+
- **`meocord/common` exports `ResponseLockOptions`**, the options `respond(interaction).lock()` takes, so a helper that passes them on can type them.
|
|
317
|
+
- **`MemoryCooldownStore` keeps its key count and its sweep to itself.** It dropped expired keys once a minute through a public `sweep()`, beside a `size` getter, both of them for MeoCord's tests. Neither is part of the store's API, and code that called them uses the store as any other `CooldownStore`.
|
|
318
|
+
- **A catalog's `meocord` group with a text where MeoCord has a group** is refused with one message naming it, such as "meocord.usage is a group, not a text", where it named every property of a string.
|
|
319
|
+
|
|
320
|
+
- [#349](https://github.com/meocord/meocord/pull/349) [`380a2a8`](https://github.com/meocord/meocord/commit/380a2a8d275972f0b8a9aafd9ba2268e08f5e4e7) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A call refused because the cooldown store answered too late no longer costs its caller a use. Under `cooldownStoreFailure: 'deny'`, the default, a store slower than `cooldownStoreTimeoutMs` refused the call with "try again shortly", yet its late answer still recorded it, so the retry was told to wait the whole window although the handler never ran: a `/daily` was lost. Now `@Cooldown` gives that use back.
|
|
321
|
+
|
|
322
|
+
`CooldownStore.consumeMany` may return a verdict with `release()`, which undoes the call it recorded. `MemoryCooldownStore`, `RedisCooldownStore` and `ShardedCooldownStore` give it, and `@Cooldown` calls it for any call it has already refused when the late answer arrives. A store of your own can add it to the verdict its `consumeMany` returns; one without it keeps such a call counted, as before, and `testCooldownStore` checks either. Under `'allow'`, the call ran uncounted, so the late count is its own and stays.
|
|
323
|
+
|
|
324
|
+
`RedisCooldownStore` now stores each call under its nonce alone, which `release` removes; calls recorded before the upgrade leave their windows as usual. On Redis Cluster, where a handler's keys sit in different slots without `hashTag: 'handler'`, each key is counted by a script of its own: a call counted that way is given back on every key, and a cooldown that refuses it gives back the keys counted before it, so the call counts against all of them or none, as on a single node.
|
|
325
|
+
|
|
326
|
+
- [#372](https://github.com/meocord/meocord/pull/372) [`efe4518`](https://github.com/meocord/meocord/commit/efe451814b4ebc1fc66357210124abb334ba9b86) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A cooldown store's refusal can say when the next call is allowed: `CooldownVerdict.retryTimestamp`, a Unix timestamp in milliseconds on the store's own clock. Every refusal in one wait gives the same one. `MemoryCooldownStore`, `RedisCooldownStore` and `ShardedCooldownStore` give it, and `messages.dmOnCooldown` tells one wait from the next by it. A store of your own can add it to its refusals; without it, waits are told apart by `retryAfterMs` and the bot's clock, as before. `testCooldownStore` checks a store that gives it.
|
|
327
|
+
|
|
328
|
+
- [#352](https://github.com/meocord/meocord/pull/352) [`5667989`](https://github.com/meocord/meocord/commit/56679894422c4b962d3a2688b857852b88155208) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A cooldown's wait is shown as a Discord timestamp, so a daily cooldown no longer says "try again in 1440m". The built-in answer to a blocked call, in replies, DMs and the presenter, is the new `meocord.cooldown.until` text, "Slow down: try again {when}.", where `{when}` is `<t:…:R>`: Discord words it in the reader's language and counts it down. The time is rounded up to the second, so it never reads as now while the call is still refused.
|
|
329
|
+
|
|
330
|
+
For a bot upgrading from an earlier 4.1 beta: `meocord.cooldown.seconds`, `meocord.cooldown.minutes` and `meocord.cooldown.wholeMinutes` are gone. Translate `meocord.cooldown.until` instead, keeping `{when}`.
|
|
331
|
+
|
|
332
|
+
`CooldownError` gains `retryAt`, the `Date` the next call is allowed, and `limit`, the `uses` and `windowMs` of the cooldown that blocked the call, so a filter or presenter needs no arithmetic: `time(error.retryAt, 'R')` from discord.js gives the same timestamp. Its `message`, which reaches logs and tests, stays plain text, now in the two biggest units that fit, and `cooldownMessage()` gives the same. For a wait of an hour or more that changes what it says: "Slow down: try again in 1439m." now reads "Slow down: try again in 23h 59m.", so a test that matches it changes too.
|
|
333
|
+
|
|
334
|
+
- [#376](https://github.com/meocord/meocord/pull/376) [`5a58dc1`](https://github.com/meocord/meocord/commit/5a58dc14ad346a844baa0f220f8c37784e61634b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `@MeoCord`'s options and `@Validate`'s pipes have names you can import:
|
|
335
|
+
|
|
336
|
+
- **`MeoCordOptions`**, from `meocord/decorator`, is what `@MeoCord` takes, each option documented where your editor shows it as you write the object. Name a base two app classes share with it; its guard, interceptor and filter lists are still checked against the classes they hold.
|
|
337
|
+
- **`ValidatePipes<S>`**, from `meocord/interface`, is the pipes `@Validate` takes for schema `S`, so a decorator of your own that wraps `@Validate` checks the pipes it passes on against the schema, as `@Validate` does.
|
|
338
|
+
|
|
339
|
+
A misused decorator's message names it more exactly: `@Command` called with the wrong interaction names the builder it was declared with, as `@Command('stats', StatsBuilder)`, and puts the right article before the class, `an AutocompleteInteraction`; a stage entry that is a string is quoted, `"Allow" is not a class`, and one that is a list, as `guards: [[StaffGuard]]` writes, is named as one, `an array is not a class`, rather than `{ provide } does not name a class`.
|
|
340
|
+
|
|
341
|
+
- [#296](https://github.com/meocord/meocord/pull/296) [`90d859c`](https://github.com/meocord/meocord/commit/90d859c4ef0733793ce061c68447f1b3fb84cbab) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `ReactionEvent` names the second argument a `@ReactionHandler` method receives, `{ user, action }`. `ReactionHandlerOptions` stays as a deprecated alias of it: every other `…Options` type is something you pass in, to a decorator, a function or a constructor.
|
|
342
|
+
|
|
343
|
+
These are deprecated, and removed in the next major version (5.0). Each still works in 4.x, and its JSDoc names what to use instead:
|
|
344
|
+
|
|
345
|
+
- `ReactionHandlerOptions`: use `ReactionEvent`. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#reactionhandleroptions-is-now-reactionevent).
|
|
346
|
+
- `SetMetadata`: use `createMetadata`. It logs a warning once.
|
|
347
|
+
- `ExecutionContext.get(key)` and `getAll(key)` with a string or symbol key, and the same on a `HandlerRegistry` entry: pass a decorator made by `createMetadata`. They log a warning once. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#setmetadata-and-string-metadata-keys-are-deprecated).
|
|
348
|
+
- `respond()`'s `ephemeral` option: use `flags: MessageFlags.Ephemeral`. It logs a warning once.
|
|
349
|
+
- `Theme` and its colours: read `useTheme().colors`, and set the colours in `@MeoCord({ theme })`. Reading a `Theme` colour now logs a warning once too, as assigning one already did. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#theme-is-deprecated-and-its-colours-changed).
|
|
350
|
+
- `MetadataKey`, `CommandMetadata` and `AutocompleteMetadata`: internal names with nothing to use instead. In the next major version (5.0) they are no longer exported. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#metadatakey-commandmetadata-and-autocompletemetadata-are-deprecated).
|
|
351
|
+
|
|
352
|
+
Run ESLint with `@typescript-eslint/no-deprecated`, as a new app's config does, to find every use in your code.
|
|
353
|
+
|
|
354
|
+
- [#367](https://github.com/meocord/meocord/pull/367) [`a497b6e`](https://github.com/meocord/meocord/commit/a497b6e266b199e54b9c0e27646247c1e4fe6043) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord start --dev` fixes:
|
|
355
|
+
|
|
356
|
+
- **A save that doesn't compile leaves the bot running.** A build with errors emitted a bundle that throws them, and the bot restarted onto it. Now a build with errors emits nothing and restarts nothing, and the bot keeps running its last good build until the code compiles again.
|
|
357
|
+
- **Ctrl+C while watch mode restarts the bot** joins that shutdown, so the bot's `onShutdown` hooks finish and the session exits 0. A second Ctrl+C still stops it at once.
|
|
358
|
+
|
|
359
|
+
A new app reads the same .env files on every runtime and however it is started, `meocord start` or `node dist/main.js` under pm2, systemd or Docker: `.env.<mode>.local`, `.env.local` (not under `test`), `.env.<mode>` and `.env`, a more specific file winning and the shell over all of them, as Bun reads them. An app made before this keeps `import 'dotenv/config'`, which reads `.env` alone under node, as 4.0 did. To read them all, replace that import in `meocord.config.ts` with:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { config } from 'dotenv'
|
|
363
|
+
|
|
364
|
+
const mode = process.env.NODE_ENV || 'development'
|
|
365
|
+
config({
|
|
366
|
+
path: [`.env.${mode}.local`, ...(mode === 'test' ? [] : ['.env.local']), `.env.${mode}`, '.env'],
|
|
367
|
+
quiet: true,
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
On Bun, set `NODE_ENV=production` where you start a production bot yourself, as with `bun dist/main.js` under pm2, systemd or Docker. With `NODE_ENV` unset, Bun loads `.env.development` and `.env.development.local` before any code runs, and dotenv keeps what is already set, so their values win over `.env.production`. The bot now warns when that happens, naming the variables that hold a development value: "Bun loaded .env.development because NODE_ENV is unset, and this is a production build, so API_URL has its development value; set NODE_ENV=production, or start with `bun --no-env-file`." `meocord start --prod` sets `NODE_ENV=production` already.
|
|
372
|
+
|
|
373
|
+
`meocord build --prod` compiles `meocord.config.ts` in production mode, as it builds the bot, so `process.env.NODE_ENV` in the config reads `production` in a production build however the bot is started. It read `development`, the mode the config was always compiled in. So a config that branches on `NODE_ENV`, such as to register commands to a development guild, now takes its production branch in a production build: check what that branch does before you deploy, and rebuild to pick this up.
|
|
374
|
+
|
|
375
|
+
- [#301](https://github.com/meocord/meocord/pull/301) [`a2991bf`](https://github.com/meocord/meocord/commit/a2991bf65b27b362d78d478da81ea63f9a07acf1) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/eslint` turns on `@typescript-eslint/no-floating-promises`. A promise nothing awaits, such as `respond(interaction).send()` or a database write left without `await`, rejects outside every handler MeoCord runs, so its error reaches no exception filter and can end the bot. An interceptor's `next.handle()` left that way runs the code after it before the handler starts, so that code never sees the handler's result or error.
|
|
376
|
+
|
|
377
|
+
After upgrading, `bun run lint` may report calls like these in your code. Each one is a promise that runs on its own:
|
|
378
|
+
|
|
379
|
+
- `await` it, or `return` it, where the code after it should wait, as an interceptor's `next.handle()` always should;
|
|
380
|
+
- or write `void` before it where it is meant to run on its own, and handle its failure with `.catch()`.
|
|
381
|
+
|
|
382
|
+
To keep the previous behaviour, set the rule to `'off'` in your `eslint.config.ts`.
|
|
383
|
+
|
|
384
|
+
- [#320](https://github.com/meocord/meocord/pull/320) [`404ba3c`](https://github.com/meocord/meocord/commit/404ba3c9f320855cc01f6c568f6c2a8434ceaa5d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `getResponse(interaction)` reports every answer a mock interaction got, whether the handler made it through `respond()` or with discord.js directly, such as `interaction.reply()` or `interaction.followUp()`. Before, it reported only what `respond()` made, so a handler written with discord.js's own methods showed `sent: true` with no `calls`, and a test had to read the mock's methods instead. Each call appears once, in the order made, with what it sent, without `withResponse`, and the `error` of one Discord refused. A test asserting the old `calls` of a handler that mixes the two now sees the direct calls too.
|
|
385
|
+
|
|
386
|
+
- [#373](https://github.com/meocord/meocord/pull/373) [`e01776a`](https://github.com/meocord/meocord/commit/e01776a3eba3859e4e338c7bc2b587f9bd0e0f85) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/common` exports `LocalizationKey<C>`, the keys `Translator.localizations()` takes: a single string with no `{params}`. The method's parameter used that type without a name you could import, so a helper that forwards a key to `localizations()` could not type it. `StringMessageKey<C>`'s documentation no longer says it is the type command names and descriptions need, and `TranslatorOptions` and `CatalogDefinition` are listed with the other localisation types.
|
|
387
|
+
|
|
388
|
+
- [#312](https://github.com/meocord/meocord/pull/312) [`32ede56`](https://github.com/meocord/meocord/commit/32ede56d5a4af6852ad9da9ac43aa11a9b93636e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing`'s mock channels, and the managers that fetch, answer as discord.js does, and `createMockMessage` takes the `channel` it was sent in.
|
|
389
|
+
|
|
390
|
+
- An interaction's `channel` is a text channel of its server, the one the server caches under `channelId`, or the user's DM channel in a DM. It was a stub, whose `send()` and `isTextBased()` threw. A `channel` given still wins.
|
|
391
|
+
- A message's `channel` is a text channel of its server, cached there, or the author's DM channel for a DM. It was a bare text channel with no managers. Pass `createMockMessage({ channel })` to send it in another.
|
|
392
|
+
- A mock channel's type guards, such as `isTextBased()`, `isDMBased()`, `isThread()` and `isSendable()`, run discord.js's own logic. They returned `undefined`, so `if (!channel.isTextBased()) return` returned early. A voice channel carries its text chat, and a forum its tags, as discord.js tells them apart by.
|
|
393
|
+
- A manager's `fetch(id)` resolves to its cached item with that id, such as a member given to `createMockGuild({ members })`, or to a new one with that id, which it caches. It returned an item with another id. A member fetched from a guild's `members` is in that guild.
|
|
394
|
+
|
|
395
|
+
A test that relied on one of the old answers changes with it; nothing changes in a bot.
|
|
396
|
+
|
|
397
|
+
- [#291](https://github.com/meocord/meocord/pull/291) [`836e026`](https://github.com/meocord/meocord/commit/836e02680218d9374f6aa75feed3cf85d761f522) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing` adds `createMockMember({ user, guild, roles, nickname })`, and its mock users and members answer as discord.js does.
|
|
398
|
+
|
|
399
|
+
- `createMockMember()` makes a member with the roles given. `roles.cache` holds the server's @everyone role, then those roles; `roles.add()`, `remove()` and `set()` change them and resolve to the member; `roles.highest` is the role that ranks highest, by position, then the lower id. `permissions` are its roles' permissions combined, @everyone's included, or every permission for the server's owner. Pass the member to `createMockGuild({ members })`, and an interaction or a message from its user in that server has it as its `member`. See [Mocks](https://meocord.dev/docs/4.1/mocks).
|
|
400
|
+
- Every mock member has a `roles` manager and `permissions`, with only @everyone unless given roles (for an interaction with a `guildId` but no `guild`, an @everyone with that id), instead of stubs that threw on `roles.cache.has()` or `permissions.has()`. A mock role has an id, `position` 1, above @everyone, and no permissions unless given.
|
|
401
|
+
- A mock guild has an @everyone role, `roles.everyone`, in `roles.cache`: the role given to `createMockGuild({ roles })` with the guild's id, or one at position 0 with no permissions. A guild's `roles.cache` therefore holds one more role than the roles given, unless one of them has the guild's id.
|
|
402
|
+
- An interaction in a server has the member's permissions as `memberPermissions`, and `null` in a DM, instead of a stub whose `has()` threw.
|
|
403
|
+
- Every mock user is a person, `bot: false`, however it is made. `createMockInteraction(User)` gave a truthy `bot`, so a message from that user reached no handler.
|
|
404
|
+
- A DM sent to a member goes through its user's `send()`, and a user's through its one DM channel, so `user.send`, `member.send` and `(await user.createDM()).send` each see it. `createDM()` resolves to that channel; it returned `undefined`.
|
|
405
|
+
- In `createChatInputOptions`, a user option's `getMember()` is the user's member in the interaction's server, and `null` in a DM, instead of the user; a member given resolves `getUser()` to its user. A whole number is an Integer option and a fraction a Number one: `getInteger()` returned `1.5` for a fraction, which only a Number option holds, and now returns `null`.
|
|
406
|
+
|
|
407
|
+
A test that relied on one of the old answers changes with it; nothing changes in a bot.
|
|
408
|
+
|
|
409
|
+
- [#324](https://github.com/meocord/meocord/pull/324) [`3b41afd`](https://github.com/meocord/meocord/commit/3b41afda3c9f57ed19f4147334372907b836f4fc) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/interface` names the options of the stage decorators, `GuardOptions`, `InterceptorOptions`, `ObserverOptions` and `ValidateOptions`, so a decorator of your own that wraps one can type what it passes on, instead of `Parameters<typeof Guard>[0]`. It also exports `PrimaryEntryPointCommandData`, the body an entry point command's builder returns, which could otherwise be written only as `CommandBuildResult<CommandType.PRIMARY_ENTRY_POINT>`.
|
|
410
|
+
|
|
411
|
+
- [#354](https://github.com/meocord/meocord/pull/354) [`1eb4fa0`](https://github.com/meocord/meocord/commit/1eb4fa0ca7643e85c3e68c71648b1807869b6d96) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A presenter can draw its views, such as with a canvas library, and attach what it draws:
|
|
412
|
+
|
|
413
|
+
- **`ResponseView.files`** carries files, an `AttachmentBuilder` or `{ name, data }`, which MeoCord sends with the view and shows for you. In an embed, the first image is the embed's image. In a Components V2 container, images go in galleries of up to 10 below the text, and other files as file components. A file the view's own `components` show by `attachment://<name>` is not shown again.
|
|
414
|
+
- **`ResponseView.image` and `thumbnail`** name one of the files, or a URL, to use as the embed's image or thumbnail, or the container's leading image and the text's thumbnail.
|
|
415
|
+
- **`loading()` and `error()` may return a promise.** A slow drawing never misses Discord's three seconds. The loading view is drawn after `@Defer` acknowledges the click. An interaction not yet acknowledged is acknowledged privately before a drawn error view, which then replaces the acknowledgement; should that drawing fail, MeoCord's own view answers instead. Discord refusing the acknowledgement, as for an interaction past its three seconds, is logged once as the refused send, never as the presenter failing, and an interaction Discord no longer knows is not answered again.
|
|
416
|
+
- **A view past Discord's limits is sent without its files**, with a warning: more than 10 attachments on the message, counting those it keeps, or a file over the interaction's attachment size limit, 20 MiB without one. A send Discord refuses as too large, as one with a file whose size could not be checked first, is sent again without its files. The user still gets the answer.
|
|
417
|
+
- **A presenter's optional `messageError()` draws a message command's error replies**: the usage reply, a guard's or validation's reason, a `UserError`'s message, and the DMs `dmOnError` and `dmOnCooldown` send, as embeds with its files. It receives a `MessageResponseContext`, with the `message` in place of an interaction. A presenter without it, such as the one a new app is generated with, leaves those replies plain text, exactly as before. Should `messageError()` throw, reject or return a view an embed cannot hold, the reply or DM is sent as that plain text, and the failure is logged as the call's fault, so a testing module's `dispatch` rejects with it.
|
|
418
|
+
|
|
419
|
+
Edits that add a view's files keep the message's own attachments, and a drawn loading view's files leave when the lock does.
|
|
420
|
+
|
|
421
|
+
- [#377](https://github.com/meocord/meocord/pull/377) [`f543e78`](https://github.com/meocord/meocord/commit/f543e78009df4d18bd3a07a668355b1cb1e8f5c1) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `ShardContext.call` types each result as it arrives: through JSON, as every mode passes it. `meocord/core` exports `Jsonified<T>`, the type a value has after that trip: a `Date` is a `string`, a `Map` or a `Set` is `{}`, a class with `toJSON()` is what it returns, and a function or `undefined` property is left out. A method returning `Promise<Date>` now gives `ShardCallResult<string>[]`, where it said `Date` and gave a string. Code that read the value as the method's own type changes to the JSON form.
|
|
422
|
+
|
|
423
|
+
The arguments take the same trip, so `call()` refuses a method whose params JSON would change, such as one taking a `Date`: the compile error says the argument arrives as JSON and names the type to declare (`string` for a `Date`). An `undefined` argument now arrives as `undefined`, where it arrived as `null`. `ShardContext` is new in 4.1, so only beta users see these changes.
|
|
424
|
+
|
|
425
|
+
- [#350](https://github.com/meocord/meocord/pull/350) [`1f39632`](https://github.com/meocord/meocord/commit/1f39632c0aed5fcfcae9c753da417954699cbb49) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `MeoCordTestingModule.create()` and `fromApp()` take `shutdownTimeout`, which mirrors the option of that name in `meocord.config.ts`: from 0 to 2147478647 ms, refused otherwise, with the message `meocord.config.ts` gets for it. `close()` runs the bot's own shutdown sequence, so before the cooldown store shuts down, the calls `invoke`, `dispatch` and `emit` have under way finish. It waits up to `shutdownTimeout`, 10 seconds unless set, for the calls, the store's operations and the `onShutdown` hooks. Then it stops waiting and logs that it did, as the bot does, and still rejects with any hook that failed before then. A test whose fake store never answers, or whose `onShutdown` never settles, sets it short:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
const module = MeoCordTestingModule.create({
|
|
429
|
+
app: App,
|
|
430
|
+
providers: [{ provide: CooldownStore, useValue: silent }],
|
|
431
|
+
shutdownTimeout: 50,
|
|
432
|
+
}).compile()
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
- [#325](https://github.com/meocord/meocord/pull/325) [`c5ca2b9`](https://github.com/meocord/meocord/commit/c5ca2b9bd79ff2734e7d5bd3548e1fac5a9cb35f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `createTranslator` and `defineCatalog` read as `createTranslator(options: TranslatorOptions<Locales, Default>)` and `defineCatalog(catalog: CatalogDefinition<T>)` in your editor and on the API pages, instead of spelling out the compiler checks they make. `TranslatorOptions` and `CatalogDefinition` are exported from `meocord/common`, each documented with what it refuses. The checks are unchanged. Nothing to change in your code.
|
|
436
|
+
|
|
437
|
+
### Patch Changes
|
|
438
|
+
|
|
439
|
+
- [#297](https://github.com/meocord/meocord/pull/297) [`94b573c`](https://github.com/meocord/meocord/commit/94b573c0e90130de8943c88e151f826ddc061280) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A bot without `activities` keeps the presence it sets. 4.0 rotated through `activities` every 10 seconds whether or not any were set, and with none it cleared the bot's activity each time, so a status set in `onReady`, in `clientOptions.presence` or by a command disappeared 10 seconds after ready, and an empty presence update was sent every 10 seconds. Now MeoCord touches the presence only when `activities` lists some, and shows one as soon as the bot is ready rather than 10 seconds later. With `activities` set, they cycle in order, as documented: the first once the bot is ready, then the next every 10 seconds, starting again after the last. 4.0 picked one at random each time. A timer that set the status again to work around it can go. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#smaller-changes).
|
|
440
|
+
|
|
441
|
+
- [#329](https://github.com/meocord/meocord/pull/329) [`94c8b2c`](https://github.com/meocord/meocord/commit/94c8b2c7ab12ef6fec9480da00b5d9b084d754a2) Thanks [@l7aromeo](https://github.com/l7aromeo)! - An asset import resolves beside the bundle, wherever `dist` is run from. `import logo from './logo.png'` gave the absolute path of the folder the build ran in. So a `dist` built in CI, on a laptop and then copied to a server, or in an image stage with another `WORKDIR`, read its assets from a path that wasn't there, and failed with ENOENT at the first attachment. The path is now set when the bot starts, from the bundle's own location. It's still an absolute path on disk, now the right one. Rebuild to pick this up.
|
|
442
|
+
|
|
443
|
+
- [#314](https://github.com/meocord/meocord/pull/314) [`4c0a815`](https://github.com/meocord/meocord/commit/4c0a81534204c5e52e6894392f08793b658b79a0) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `bundleDependencies` packs every package a bot needs into `dist/node_modules`, each with the dependency versions it was installed with, from a pnpm project as from npm, yarn and bun.
|
|
444
|
+
|
|
445
|
+
- A package listed in `externals` or `optionalExternals` was packed without the packages it depends on, in a pnpm project. pnpm keeps those beside the package in its store, not in your project's `node_modules`, so they were left out, and the bot failed at startup with "Cannot find module". They're packed now.
|
|
446
|
+
- A native package that ships its binary as a per-platform package, as napi-rs packages do, is recognised as native in a pnpm project and packed with that binary, whether you list it in `externals` or the bot imports it. Before, a listed one was packed without its binary, and an imported one stopped the build, asking for it in `externals`.
|
|
447
|
+
- Each packed package loads the version of each dependency it was installed with. When packages need different versions of one package, the first stays at the top of `dist/node_modules`, and each other version is nested where Node, resolving from the package that needs it, finds it first. Before, every package got the first.
|
|
448
|
+
- A package npm nested under another, because another version of it holds the top of `node_modules`, is packed with the packages it needs from the top. Before, they were left out, and the bot failed at startup with "Cannot find module".
|
|
449
|
+
|
|
450
|
+
Rebuild to pick these up.
|
|
451
|
+
|
|
452
|
+
- [#316](https://github.com/meocord/meocord/pull/316) [`d2284d9`](https://github.com/meocord/meocord/commit/d2284d95fc1927ad3ab1237e6c9987d12e94b36f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `respond()` could see a locked message's content as changed when it wasn't. It compares the message with what MeoCord last wrote by sorting keys with the runtime's collation, which ranks two different keys equal when they differ only in Unicode normalization, such as `café` written with `é` and with `e` and a combining accent. Their order then followed the input, so equal content could compare unequal. Keys are now sorted in code-unit order, which needs no locale. This also no longer loads the runtime's ICU data on the first lock or restore. Nothing to change in your code.
|
|
453
|
+
|
|
454
|
+
- [#395](https://github.com/meocord/meocord/pull/395) [`d5d07cb`](https://github.com/meocord/meocord/commit/d5d07cb96dac7757503a3b1aa68f6886c9db80c5) Thanks [@l7aromeo](https://github.com/l7aromeo)! - CLI and build fixes:
|
|
455
|
+
|
|
456
|
+
- **Every imported file lands in `dist/assets` under its own name.** A pdf, txt, webmanifest or wasm import was written to `dist/static/assets` with a content hash, unlike images, fonts and media. Its import gives that path as before, so nothing in your code changes; rebuild to pick it up. Two imported files of one name in different folders stop the build with Rspack's conflict error, naming the file.
|
|
457
|
+
- **`meocord start --dev` runs the bot on the development .env files whatever `NODE_ENV` your shell holds**, as its development build's config reads them: it watches them, and starts the bot with `NODE_ENV=development`. With `NODE_ENV=production` in the shell, it watched the production files, and a bot on Bun read the production values.
|
|
458
|
+
- **A production bot on Bun warns about another mode's .env values for any `NODE_ENV` but `production`.** Bun reads the development files for every `NODE_ENV` except `production` and `test`, so a bot started with `NODE_ENV=staging` ran on development values without the warning an unset `NODE_ENV` gets.
|
|
459
|
+
- **`meocord create` warns on Node 22.0 to 22.12**, below the `>=22.13` the package requires. It compared the major version alone.
|
|
460
|
+
- **The CLI finds an installed package in the filesystem root's `node_modules`**, as with a project directly under `/` in a container.
|
|
461
|
+
|
|
462
|
+
- [#319](https://github.com/meocord/meocord/pull/319) [`218b6c5`](https://github.com/meocord/meocord/commit/218b6c5739d8dce0c34f4e013f528ac10ae1958f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The CLI clears the screen only where it helps: as `meocord start --dev` begins, in a terminal. `meocord build` and `meocord start --prod` no longer clear it, so the output of the commands before them, such as a failing test run, stays on screen. No command writes escape codes into piped output any more, such as CI logs, `docker logs`, pm2 or systemd. When `start --dev` clears, it keeps your scrollback.
|
|
463
|
+
|
|
464
|
+
A build's output no longer lists the temporary folder the config is compiled in. The line after it still says where the config went.
|
|
465
|
+
|
|
466
|
+
- [#391](https://github.com/meocord/meocord/pull/391) [`f8dcc1a`](https://github.com/meocord/meocord/commit/f8dcc1ab3211f4379c1c1bdd20f6bb48d5f01b00) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Corrections to comments that ship with the package and in a new app; no behaviour changes.
|
|
467
|
+
|
|
468
|
+
- The `meocord/eslint` example ignores generated code, in place of `coverage`, which the config already ignores.
|
|
469
|
+
- In a new app, the comments in `vitest.config.ts` give the reasons that apply to the versions it installs, `src/types/theme.d.ts` explains the theme augmentation in three lines, and the sample message controller's TODO reads correctly. The sample slash, modal and guard spec titles say only what their tests check.
|
|
470
|
+
- `meocord generate`: the guard template lists `'autocomplete'` among the handler kinds it can limit itself to, the interceptor template says interceptors skip autocomplete, and the filter template says `response` is undefined for autocomplete too.
|
|
471
|
+
|
|
472
|
+
- [#364](https://github.com/meocord/meocord/pull/364) [`23514e7`](https://github.com/meocord/meocord/commit/23514e74966fd470d9749d804f8a4d6c997e1072) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The CLI reads the config the bot it runs reads. `meocord start --dev` uses the `shutdownTimeout` and `sourceMappedStacks` of the config it last compiled, so an edit to `meocord.config.ts` applies from the next restart. Before, it kept the ones from whatever `dist` held when the session began, and waited a shorter `shutdownTimeout` than the bot's own, killing it before its `onShutdown` hooks finished. The token check follows the same rule. `start` and `register` check the source config when they build first. Otherwise they check the compiled config the bundle runs, so `meocord register` without `--build` now stops at a compiled config that fails to load, as `start --prod` does.
|
|
473
|
+
|
|
474
|
+
- [#394](https://github.com/meocord/meocord/pull/394) [`bfb3e07`](https://github.com/meocord/meocord/commit/bfb3e071747456a56acf040044716ad3869c2e37) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The editor documentation of `meocord/common` and `meocord/interface` matches what the code does:
|
|
475
|
+
|
|
476
|
+
- **`respond()`'s state.** `message` is the last reply, update or edit, never a follow-up. `lock()` with `disable: 'none'` leaves the message alone. `error()` puts a locked message back as it was before it follows up.
|
|
477
|
+
- **Errors.** `GuardDeniedError`, `ValidationError`, `CooldownError` and `CooldownStoreError` say how a message command is answered. A message command gets a reply in the channel for a denial or invalid input. For a cooldown or a store outage it gets nothing, or a direct message under `messages.dmOnCooldown` or `messages.dmOnError`. `CooldownStoreError` also says that the recovery is logged once the store has answered for 30 seconds without failing. The constructors and helpers have their `@param` and `@returns`.
|
|
478
|
+
- **`ExecutionContext`.** `getController()` is the class the handler runs on, the subclass for an inherited one. `getParams()` covers pipes too.
|
|
479
|
+
- **Cooldown stores.** `MemoryCooldownStore`, `RedisCooldownStore.using` and the Redis constructor document all their parameters, `hashTag` included. On Redis Cluster, a refused call counts against no key unless giving a use back fails.
|
|
480
|
+
- **`route().build()`** also throws a `TypeError` for a value that is not of its param's type.
|
|
481
|
+
- **Localisation.**
|
|
482
|
+
- `createTranslator` documents `options` and what it returns.
|
|
483
|
+
- `defineCatalog` says which catalogs need it.
|
|
484
|
+
- `LocaleCatalog` says where a locale's `{params}` are checked.
|
|
485
|
+
- A plural needs its `other` form to be read as one.
|
|
486
|
+
- **`createToken`** names what its type checks: `TestingModule.get` and a provider's `useValue` or `useFactory`.
|
|
487
|
+
- **`Logger`.** A string prints in its tag's colour. Each method documents `args`.
|
|
488
|
+
- **App options.**
|
|
489
|
+
- `caseSensitive` covers choice words and flag names.
|
|
490
|
+
- `deleteUsageRepliesAfter` covers a guard's or validation's reason.
|
|
491
|
+
- `replyEmoji` notes that help begins with the info emoji.
|
|
492
|
+
- `help` and `MessageHelp` say that `!help` leaves out hidden and guarded commands.
|
|
493
|
+
- `scope: 'dm'` with a server-only param is refused.
|
|
494
|
+
- **`OnShutdown`** runs on `app.stop()` too.
|
|
495
|
+
- **Examples.** The examples for a param type's and a theme's `declare module` import what they use.
|
|
496
|
+
|
|
497
|
+
- [#302](https://github.com/meocord/meocord/pull/302) [`e5724e8`](https://github.com/meocord/meocord/commit/e5724e84f6a60135c7ca56cf84cdea64b75e3a0a) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A component handler's params are checked against what a call gets, when the code compiles:
|
|
498
|
+
|
|
499
|
+
- **A select menu's choices** have their real types: `values: string[]`, `users: User[]`, `members`, `roles` and `channels` as discord.js resolves them. A declaration no choice can have, such as `values: number` or `users: string`, fails to compile, where it used to compile and then fail on the first selection. A narrower type that a choice can hold, such as `members: GuildMember[]` or `readonly Role[]`, still compiles.
|
|
500
|
+
- **A plain-string customId pattern's keys** are checked as a `route()`'s are. With `@Command('stats/{id}', CommandType.BUTTON)`, a handler declaring `{ uid }` fails to compile, naming `uid`, since that param was always `undefined`. Fix the name to one the pattern captures.
|
|
501
|
+
- **A key a pipe produces** is declared `Piped<T>`, as with `@Validate`, and the check leaves it to the pipe. That covers a choice `@UsePipe` turns into something else, a typed customId param such as `{id:int}` piped into an object, and a `@MessageHandler` pattern's param piped the same way, which no declaration could satisfy before. The check reads only the handler's own type, so it cannot see the pipe. A 4.1 beta handler that writes `@UsePipe('values', ToQuantities)` with `{ values: number[] }` now fails to compile, with a message ending "a key a pipe produces is marked Piped<T>"; write `{ values: Piped<number[]> }` instead.
|
|
502
|
+
|
|
503
|
+
- [#317](https://github.com/meocord/meocord/pull/317) [`eb6698e`](https://github.com/meocord/meocord/commit/eb6698e81a35887b8e662f78fd276e940dfbde7d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A built bot finds its config beside its bundle, wherever it's started from. Before, it looked in `dist` under the working directory, so a bot started from elsewhere failed with "MeoCord config not found … Run `meocord build`", even with a fresh build. That covered pm2 without `cwd`, a systemd unit without `WorkingDirectory`, and `cd dist && node main.js`.
|
|
504
|
+
|
|
505
|
+
When the config really is missing, the message names the file it looked for and the working directory. When the file is there but fails to load, the message says so, gives the reason and says what to do: install the package it names when one is missing, naming the installed package that imports it if the config does not import it itself, or fix `meocord.config.ts`, then run `meocord build`.
|
|
506
|
+
|
|
507
|
+
`.env` is still read from the working directory, through `dotenv` in your `meocord.config.ts`. If you start the bot from elsewhere, set its environment there or point `dotenv` at the file.
|
|
508
|
+
|
|
509
|
+
The check that refuses a build made for another platform also reads its record beside the bundle. It now applies when a process manager's wrapper starts the bot.
|
|
510
|
+
|
|
511
|
+
A development build (`meocord build --dev`) does the same, also when a process manager such as pm2 starts it through its own wrapper: its config, its asset imports and the script its shards start from are all found beside its bundle.
|
|
512
|
+
|
|
513
|
+
Rebuild to pick these up.
|
|
514
|
+
|
|
515
|
+
- [#363](https://github.com/meocord/meocord/pull/363) [`b38234c`](https://github.com/meocord/meocord/commit/b38234c71ab07e39834161fbc95f6fe5a281c116) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A compiled config that fails to load is reported once, with its reason and what to do: "MeoCord config at … failed to load: <reason>. Fix meocord.config.ts, then run `meocord build`.", or, when the config needs a package that isn't installed, one that says to install it. A built bot printed a separate "[MeoCord] Failed to load …" line before that one, and `meocord start --prod` printed it a third time. `meocord start --prod` without `--build` now stops with that message, rather than check `meocord.config.ts` in its place, which could stop on a missing token without naming the broken config the bot would run.
|
|
516
|
+
|
|
517
|
+
- [#313](https://github.com/meocord/meocord/pull/313) [`0060a3c`](https://github.com/meocord/meocord/commit/0060a3ce0a1481df831673a76221c74b4ded02c8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Three mistakes with `@Command` and `@MessageHandler` are now named where they happen:
|
|
518
|
+
|
|
519
|
+
- **A command builder whose constructor throws**, such as one reading a translator or the environment in a field, is refused as the class loads, like a builder whose `build()` throws: `Stats.stats: StatsBuilder could not be made for "stats": missing translator.` It used to surface as the bare error at import, naming neither the builder nor the command.
|
|
520
|
+
- **A `@Command` handler called with another kind of interaction**, as a direct call in a test can be, throws `Cards.card: @Command('card/{id}', CommandType.BUTTON) takes a ButtonInteraction, not a ChatInputCommandInteraction.`, or `…; it was given undefined.` for something that is no interaction at all, instead of `Invalid interaction type passed to @Command for method: card`.
|
|
521
|
+
- **`@MessageHandler('')`** still runs for every message, as `@MessageHandler()` does, and now logs a warning naming the handler: `@MessageHandler('') on Chat.every is deprecated; in the next major version (5.0) it is refused. Use @MessageHandler() instead.` See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#messagehandler-logs-a-warning).
|
|
522
|
+
|
|
523
|
+
A builder error that ends in a full stop no longer gets a second one in its refusal.
|
|
524
|
+
|
|
525
|
+
- [#344](https://github.com/meocord/meocord/pull/344) [`7b68f81`](https://github.com/meocord/meocord/commit/7b68f81271790b5da208e7baeb502caf9e5608e8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `@Cooldown`'s `seconds` is now counted in whole milliseconds, and a window no store can count is refused where the decorator applies.
|
|
526
|
+
|
|
527
|
+
- A `seconds` value whose milliseconds weren't a whole number, such as `16.1`, made `RedisCooldownStore` fail every call to that handler with "ERR value is not an integer or out of range", while the memory store used in development and tests counted it fine. `seconds` is now rounded to the millisecond, once, so every store gets a whole `windowMs` of at least 1.
|
|
528
|
+
- `seconds` must be from `0.001` to `4320000000000`. `Infinity` and other values beyond that were accepted, and each store did something different with them: the memory store refused with "try again in Infinitym NaNs", Redis failed every call, and process sharding never limited anything.
|
|
529
|
+
- `@MeoCord({ cooldownStoreTimeoutMs })` must be at most `2147483647`, the longest delay a timer keeps. A longer one fired at once, so every call with a cooldown timed out.
|
|
530
|
+
- `testCooldownStore` checks a store that keeps the default `consumeMany` or `peekMany` against what those defaults do, where its batch and peek cases passed without checking anything. A store whose `consume` lets two concurrent calls take the last use now fails the concurrent batch case as well.
|
|
531
|
+
- The `cooldownStoreTimeoutMs` refusal shows `Infinity` and `NaN` as they are, rather than as `null`.
|
|
532
|
+
|
|
533
|
+
- [#348](https://github.com/meocord/meocord/pull/348) [`63f9514`](https://github.com/meocord/meocord/commit/63f9514fe2d5351cf0acb74879a30ded6db55f81) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A cooldown's count is kept under a key named for its window, not its position among the handler's cooldowns. A release that added, removed or reordered a `@Cooldown` moved the counts a persistent store such as Redis kept to other cooldowns: adding a short cooldown above a daily one reset the daily for everyone, and a short one could inherit a long one's history. Now a deploy that adds, removes or reorders cooldowns leaves the others' counts where they are. Changing a cooldown's `uses` keeps the calls counted so far, held to the new number; changing its `seconds` starts its count again. A handler's cooldowns with the same `seconds`, `per`, `by` and `bypass` count the same calls, so they share one count, held to the smallest `uses`. The exception is two cooldowns over the same `seconds` and `per`, both with `by` or both without, whose `by` or `bypass` functions differ (two inline functions differ even when written alike): `uses` tells them apart, so for those, changing `uses`, or adding or removing another such cooldown, starts their counts again, and reordering two with the same `uses` swaps their counts. A call that `by` returns `undefined` for is counted apart from a cooldown with no `by` over the same window. For a bot upgrading from an earlier 4.1 beta, counts kept under the old keys are not carried over, so every cooldown's count starts afresh once.
|
|
534
|
+
|
|
535
|
+
- [#345](https://github.com/meocord/meocord/pull/345) [`a3358e4`](https://github.com/meocord/meocord/commit/a3358e4ebce33ba67e2c0b021bffa2f32e193700) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `messages.dmOnCooldown` now DMs an author once per wait, as documented, however often they retry within it. The notice was counted over the wait left at each refusal, which shrinks with every retry, so it expired halfway through: an author retrying every second during a 60-second wait got six DMs (at 1, 31, 46, 53, 57 and 59 seconds), and about twelve in an hour-long one. Each wait now has a notice of its own, kept for the cooldown's full window. A wait is told apart by when it ends on the store's own clock, so a retry whose answer comes back late, or from another shard sharing the store, finds the notice already taken.
|
|
536
|
+
|
|
537
|
+
- [#346](https://github.com/meocord/meocord/pull/346) [`3f9d6f7`](https://github.com/meocord/meocord/commit/3f9d6f769046e7da6d7b15a647b010aa88337f90) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A cooldown store that fails some calls and answers others, as a Redis Cluster with one node down does, is now logged as one outage. Any answer ended the outage and the next failure began a new one, so each failing call logged an error with its stack and then a recovery line. An outage now ends when the store answers 30 seconds or more after its last failure, and the recovery line counts every call that failed in it.
|
|
538
|
+
|
|
539
|
+
- [#350](https://github.com/meocord/meocord/pull/350) [`b36cba3`](https://github.com/meocord/meocord/commit/b36cba326bd13686dee18d60c715d71bcc382df9) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A class you bind with `@MeoCord({ cooldownStore })` now gets its `onReady` and `onShutdown` hooks, as a service does. Before, neither ran, so a store that opens a connection in `onReady` and closes it in `onShutdown` never connected and leaked its connection on every shutdown.
|
|
540
|
+
|
|
541
|
+
The order suits a store that connects:
|
|
542
|
+
|
|
543
|
+
- Its `onReady` runs before the services'. A call that comes while it runs waits for it, within `cooldownStoreTimeoutMs`. One that would wait longer meets your `cooldownStoreFailure` policy, as a store that doesn't answer does.
|
|
544
|
+
- Its `onShutdown` runs after the services'. The bot first stops taking new calls and lets the ones under way finish, along with every store operation they started, even an answer that came after its call stopped waiting. So the store closes after the last write it is asked for.
|
|
545
|
+
- What the store injects, such as the queries it runs, is ready before it and shuts down after it, so it is there for the store's last write.
|
|
546
|
+
- A store with no `onShutdown`, and nothing it injects with one, doesn't hold shutdown up: the bot waits for the calls under way only when a hook needs the store.
|
|
547
|
+
|
|
548
|
+
`MeoCordTestingModule` runs the store's hooks in the same order, in `init({ ready: true })` and `close()`, for the app's store or the `CooldownStore` a test provides in its place.
|
|
549
|
+
|
|
550
|
+
- [#393](https://github.com/meocord/meocord/pull/393) [`1403136`](https://github.com/meocord/meocord/commit/140313695131f7893a3f5d55b3a393a8eda1c5c3) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Corrected the editor documentation of several public APIs to match what they do:
|
|
551
|
+
|
|
552
|
+
- `HandlerRegistry`: `list()` gives the handlers in the order the app makes its classes, not the order they were bound. `messageHelp()` lists by where the message was sent, not by its author, and an entry's `hidden` leaves a command out of help's lists while `!help <command>` still shows it.
|
|
553
|
+
- `useTheme()`: outside a call it reads the app's theme from when its start begins until it has shut down. The chain of `@UseTheme` stops where `inheritStages: false` does, and only the theme's plain objects and arrays are frozen.
|
|
554
|
+
- `MeoCordFactory.create`: with process sharding, the manager logs an error a shard throws.
|
|
555
|
+
- `ShardContext`: `runHere` takes a service's class in a bot of one process, and its name with process sharding.
|
|
556
|
+
|
|
557
|
+
- [#311](https://github.com/meocord/meocord/pull/311) [`bcda23d`](https://github.com/meocord/meocord/commit/bcda23d5174209539128d400c42b4001a9cdbf12) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord create` writes any app name into `meocord.config.ts` as a valid string. A name with an apostrophe, such as `"Bob's Bot"`, or one ending in a backslash made a config that failed to parse, so the new app never built. A backslash elsewhere changed the name silently: `Back\slash` was read as `Backslash`. The name is now quoted and escaped the way the app's own Prettier config writes it, so the file also passes the app's lint unchanged. An app created with such a name before this needs its `appName` fixed by hand.
|
|
558
|
+
|
|
559
|
+
- [#306](https://github.com/meocord/meocord/pull/306) [`d04a276`](https://github.com/meocord/meocord/commit/d04a27626b6c13bd8a2a8a89ce365e79ab3f7e66) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord create` keeps the app when git can't make its first commit. Before, a git failure deleted the app it had just written and exited with an error. That happened on a machine where git has no `user.email` (many fresh Linux machines, containers and CI runners) and on one without git. Now `create` finishes, says what happened, and tells you how to finish the commit. Inside an existing Git repository it makes no new one and leaves the files to that repository.
|
|
560
|
+
|
|
561
|
+
The first commit now comes after the install, so your lockfile (`bun.lock`, `package-lock.json`, `yarn.lock` or `pnpm-lock.yaml`) is part of it, rather than showing as an untracked file in a new app's first `git status`.
|
|
562
|
+
|
|
563
|
+
- [#389](https://github.com/meocord/meocord/pull/389) [`6ac2e26`](https://github.com/meocord/meocord/commit/6ac2e26c38f08da270c91aa57b99d9862d9debdb) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord create --use-npm` makes an app that npm 11.16 and later install without the `allow-scripts` warning. Those versions list every dependency install script your `package.json` neither allows nor denies, and a new app listed `@swc/core`, `unrs-resolver` and, on macOS, `fsevents`. An app created for npm now denies all three under `allowScripts`:
|
|
564
|
+
|
|
565
|
+
- `@swc/core` and `unrs-resolver` load the native binding npm installs for your platform. Their scripts check that binding and, only where it fails to load, fetch a fallback: `@swc/core`'s installs `@swc/wasm`, which `@swc/core` itself does not load, and `unrs-resolver`'s downloads the binding npm installs anyway.
|
|
566
|
+
- `fsevents` ships its binary prebuilt. Its script rebuilds it from source, which fails because the package has no build files, so npm left the optional `fsevents` out. Denied, it is installed with its prebuilt binary.
|
|
567
|
+
|
|
568
|
+
npm before 11.16 ignores the field. An app created with pnpm, yarn or bun gets no `allowScripts`.
|
|
569
|
+
|
|
570
|
+
An app created for npm before this gets the same by adding to its `package.json`:
|
|
571
|
+
|
|
572
|
+
```json
|
|
573
|
+
"allowScripts": {
|
|
574
|
+
"@swc/core": false,
|
|
575
|
+
"fsevents": false,
|
|
576
|
+
"unrs-resolver": false
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- [#375](https://github.com/meocord/meocord/pull/375) [`8c212ae`](https://github.com/meocord/meocord/commit/8c212ae5de1d9b0646f1e80479ec110cc2b7c9a8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord create --use-pnpm` makes an app that installs and passes its own checks on pnpm, 10 and later.
|
|
581
|
+
|
|
582
|
+
- pnpm 11 and later refuse to install while a dependency's build script is neither allowed nor denied, so `create` stopped at "Failed to install dependencies" with `ERR_PNPM_IGNORED_BUILDS` for `@swc/core` and `unrs-resolver`. An app created for pnpm now has a `pnpm-workspace.yaml` that leaves both scripts off under `allowBuilds`: each only checks the native binding pnpm installs for your platform, and fetches a fallback without it. The same file lets pnpm install the `meocord` that created the app, which pnpm 11 and later would otherwise hold back for a day after its release (`minimumReleaseAgeExclude`). An app created with npm, yarn or bun gets no such file.
|
|
583
|
+
- The app declares `reflect-metadata` and `@types/node`, which its test setup imports and its tsconfig names. npm and bun hoist them from other packages, but pnpm links only what `package.json` declares, so on pnpm 10 the app's `lint` and `test` failed.
|
|
584
|
+
|
|
585
|
+
An app created for pnpm before this gets the same fix by adding both packages to its `devDependencies`, and on pnpm 11 or later this `pnpm-workspace.yaml`:
|
|
586
|
+
|
|
587
|
+
```yaml
|
|
588
|
+
allowBuilds:
|
|
589
|
+
'@swc/core': false
|
|
590
|
+
unrs-resolver: false
|
|
591
|
+
minimumReleaseAgeExclude:
|
|
592
|
+
- meocord
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
- [#397](https://github.com/meocord/meocord/pull/397) [`e1168b9`](https://github.com/meocord/meocord/commit/e1168b987609f173d25389652e47176c4207437a) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `@Defer()` written below `@MessageHandler`, `@ReactionHandler`, `@On` or `@Autocomplete` is refused in one line at `create()`, as it is when written above one, rather than printed with a stack trace.
|
|
596
|
+
|
|
597
|
+
- [#343](https://github.com/meocord/meocord/pull/343) [`87dbef1`](https://github.com/meocord/meocord/commit/87dbef18f0ca4d13bdda9d9b68389d21c1ce6df8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord start --dev` restarts the bot through the bot's own stop, the one SIGINT and SIGTERM run, on every platform. On Windows, a restart used to end the bot outright, so its `onShutdown` hooks never ran on a save, and whatever they release or flush was left as it was. The dev runner now asks the bot to stop over the channel it already gives it, and the bot shuts down as it does on Ctrl+C, before the new build starts. A bot that doesn't exit within its `shutdownTimeout` and a short grace period is still killed, as before. Rebuild to pick this up.
|
|
598
|
+
|
|
599
|
+
When watch mode can't start, it stops a bot it had already started the same way, and exits once that bot has. Before, a bot that held on to its stop signal could outlive the CLI.
|
|
600
|
+
|
|
601
|
+
One save no longer restarts the bot twice. An editor's save can produce two builds of the same output, and a bot already running the latest output is now left alone. A change to `meocord.config.ts`, `tsconfig.json` or `.env` still always restarts it.
|
|
602
|
+
|
|
603
|
+
- [#337](https://github.com/meocord/meocord/pull/337) [`8504f45`](https://github.com/meocord/meocord/commit/8504f45d18ac85a52f82044b307e68e5893fa926) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord start --dev` keeps watching when the bot cannot log in, such as with a wrong token or an intent Discord refuses. It says so, and starts the bot again on the next change, whether to your code or to `.env`.
|
|
604
|
+
|
|
605
|
+
`start --dev` also watches `.env`, `.env.local`, `.env.development` and `.env.development.local`: saving one restarts the bot with the values the files now hold, without a rebuild. The bot reads the files itself as it starts, and inherits only what your shell set.
|
|
606
|
+
|
|
607
|
+
When watch mode cannot start, for example because an `rsbuild` hook in `meocord.config.ts` throws, `start --dev` exits with code 1, as `meocord build` does, so a script or process manager around it sees the failure.
|
|
608
|
+
|
|
609
|
+
A rebuild that can't start partway through a session, such as when you save a `meocord.config.ts` whose `rsbuild` hook or plugin throws, no longer ends `start --dev` with the bot still running in the background. It says the rebuild failed and why, keeps the bot and its last build running, and tries again when you save the file again.
|
|
610
|
+
|
|
611
|
+
- [#350](https://github.com/meocord/meocord/pull/350) [`b36cba3`](https://github.com/meocord/meocord/commit/b36cba326bd13686dee18d60c715d71bcc382df9) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The startup check now names a `@MessageHandler` with `scope: 'dm'` that can never receive a DM. A DM reaches the bot only with the `DirectMessages` intent, and only with discord.js's `Partials.Channel`, since no DM channel is cached after the bot starts. The generated app's client options have neither, so a DM-only command added to it never ran, and nothing said why. The warning names the handler and what is missing. A command for servers, or for both, gets no new warning.
|
|
612
|
+
|
|
613
|
+
- [#372](https://github.com/meocord/meocord/pull/372) [`270d315`](https://github.com/meocord/meocord/commit/270d315b41e3f59bb6591aa7f4c98f0a6df6e8b9) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The direct message `messages.dmOnError` sends for a command in a server now says what went wrong. `meocord.dm.error` reads `{command} in {channel} on {server}: {reason}`, where `{reason}` is what the fallback answers the error with: "An error occurred while executing the command." for a fault, and "Cooldowns can't be checked right now: try again shortly." when the cooldown store is down. It said "Something went wrong running {command} in {channel} on {server}. Try again later." for both, so a store outage read as a broken command. The text has the same shape as `meocord.dm.cooldown` beside it. A catalog that translates `meocord.dm.error` adds `{reason}` to it.
|
|
614
|
+
|
|
615
|
+
- [#355](https://github.com/meocord/meocord/pull/355) [`d642b0c`](https://github.com/meocord/meocord/commit/d642b0c17a924f65e9b25e00c38eb29807f7b2e8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A reaction in a DM the bot has not cached since it started reaches its handlers again. From discord.js 14.26.2, discord.js makes a channel it has not cached from a gateway event only when the event says the channel is a DM, and a reaction's event names its channel by id alone, so such reactions were dropped before any listener saw them, even with `Partials.Channel`. With the `DirectMessageReactions` intent, MeoCord now fetches that DM channel once, on its first reaction, and hands the reaction back to discord.js, which delivers it to `@ReactionHandler` and to your own `messageReactionAdd` and `messageReactionRemove` listeners alike. Later reactions in that DM need no channel fetch, though the first reaction on a message the bot doesn't hold still fetches that message once, and a reaction discord.js delivers itself is left alone. One window remains: a DM reaction that arrives before the gateway is ready, in the seconds while discord.js waits for the bot's servers, is replayed by discord.js later without a raw event, so it is still dropped.
|
|
616
|
+
|
|
617
|
+
- [#332](https://github.com/meocord/meocord/pull/332) [`bb22d53`](https://github.com/meocord/meocord/commit/bb22d5307225ed7a804474cee0a8cfe57515dfd8) Thanks [@l7aromeo](https://github.com/l7aromeo)! - On Windows, `meocord generate` in a project installed with npm, yarn or pnpm reported "Failed to create" and exited 1 for every file, though the file was written. Its formatting step spawned `node_modules/.bin/eslint.cmd`, which Node refuses to start without a shell. The project's ESLint now runs through the same runtime as the CLI. A formatting failure never marks a written file as failed.
|
|
618
|
+
|
|
619
|
+
- [#357](https://github.com/meocord/meocord/pull/357) [`cfb94c8`](https://github.com/meocord/meocord/commit/cfb94c8eb25c1869a3cb06ef9c57fa67695bb5bf) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord generate` formats the files it writes with your project's ESLint in one run, and says so. Before, it started a separate ESLint for each file, all at once. Each built your project's type information, and the command sat silent until the slowest finished: about 2.3 times the CPU for a controller with its spec and builder, enough to stall `generate` on a busy machine. Now it prints "Formatting with your project's ESLint..." after the files are created and waits for that one run. If ESLint can't run, or reports problems it can't fix, `generate` says so, and the files stay as written either way.
|
|
620
|
+
|
|
621
|
+
- [#332](https://github.com/meocord/meocord/pull/332) [`c95beab`](https://github.com/meocord/meocord/commit/c95beabd3c5bff005d7283618eac95fe766fc4b2) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord generate controller` writes a spec that tests the handler. It invokes the handler with a mock of the interaction, message or reaction it handles, and checks what it answers: how it answers, such as a reply or an update of the message, and the text, such as "Hello from /ping!" or, for a mentionable select menu given a user and a role, "Selected 1 user(s) and 1 role(s)." Before, every spec only checked that the controller existed, and passed whatever the handler did. A generated slash, context menu or entry point builder now takes the command's name from `@Command` (`build(commandName)`), so the two can't drift apart. Files you've already generated are unchanged.
|
|
622
|
+
|
|
623
|
+
The sample specs `meocord create` writes check the same way. The slash, context menu, button and modal samples check the text they answer. The message and reaction samples send a message or a reaction through `module.dispatch()`, as the bot routes it, and check the reply, where before they only checked that the controller existed.
|
|
624
|
+
|
|
625
|
+
- [#400](https://github.com/meocord/meocord/pull/400) [`dcc5087`](https://github.com/meocord/meocord/commit/dcc50871ced6e08b02ebbe41e769edc69cca9bcb) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `MeoCordFactory.create()` and the testing module's `compile()` now warn about each `@MessageHandler`, `@ReactionHandler`, `@Command` or `@Autocomplete` on a class that is not one of the app's `@MeoCord({ controllers })`, such as a service or a class one injects. MeoCord dispatches only to controllers, so these handlers never run, and nothing said so. Move them to a controller: the next major version (5.0) refuses to start with them, as the [upgrade guide](https://meocord.dev/docs/4.1/migrating#a-handler-on-a-class-that-isnt-a-controller-logs-a-warning) describes. A sharded bot warns once, from its manager. The warnings about missing intents and partials no longer name these handlers, since no intent would make them run. `@On` and `@Once` handlers run on any bound class, as before.
|
|
626
|
+
|
|
627
|
+
- [#300](https://github.com/meocord/meocord/pull/300) [`a4afcf9`](https://github.com/meocord/meocord/commit/a4afcf99a46b22095613f0d1906690d73e12420f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A handler may return a value. `@Command`, `@MessageHandler`, `@ReactionHandler` and `@Autocomplete` accepted only a method returning nothing, so `return interaction.reply(…)` or `return message.reply(…)`, as discord.js code often ends a handler, failed to compile with "Unable to resolve signature of method decorator". As with `@On`, any return type compiles: MeoCord answers nothing with it, and an interceptor receives it from `next.handle()`. A parameter of the wrong type is still refused.
|
|
628
|
+
|
|
629
|
+
- [#397](https://github.com/meocord/meocord/pull/397) [`622010c`](https://github.com/meocord/meocord/commit/622010c64cabe82aaa97f232732070b1e8590f4f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The built-in help, and the list of subcommands a parent command with no handler of its own answers, now run the app's `@MeoCord({ guards })` first, as a command does. A guard that returns `false` leaves the message unanswered, and one that throws `GuardDeniedError` gets its reason as the reply, as a denied command does. They answered whatever the app's guards decide, so a bot its guards close in a channel, or to a user, still answered help there.
|
|
630
|
+
|
|
631
|
+
- [#334](https://github.com/meocord/meocord/pull/334) [`4de3194`](https://github.com/meocord/meocord/commit/4de319435fb7420968b181e29ad534cb6618318f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A handler that a subclass re-declares follows one rule for `@Command`, `@MessageHandler`, `@ReactionHandler` and `@Autocomplete`.
|
|
632
|
+
|
|
633
|
+
- **On the route it inherits**, the subclass's declaration takes that route's place, so the subclass's options apply: a re-declared `@Command('ping', LoudPingBuilder)`, `@MessageHandler('roll', { description })` or `@ReactionHandler('👍', { bots: true })` uses its own builder, description or settings. In 4.0 the base's applied, since the base's declaration came first. To keep the base's builder or options, don't re-declare that route on the subclass, or declare it with the base's builder. The routes the subclass answers are unchanged.
|
|
634
|
+
- **On another route**, the subclass still answers the route it inherits too, as in 4.0. For example, a subclass overrides `page()`, which its base declares as `@Command('page/{n}', …)`, with `@Command('shop/page/{n}', …)`, and answers both. The bot now names each such handler in one warning as it starts, with the routes it inherits and its own. In the next major version (5.0), a handler's own routes replace the ones it inherits. To keep an inherited route, declare it on the subclass's method as well.
|
|
635
|
+
|
|
636
|
+
A class between them that declares nothing changes neither. A subclass that declares every route itself, or none, gets no warning. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#a-re-declared-handler-that-keeps-its-inherited-route-logs-a-warning).
|
|
637
|
+
|
|
638
|
+
- [#341](https://github.com/meocord/meocord/pull/341) [`e09926b`](https://github.com/meocord/meocord/commit/e09926b1d0814410ab68c0de0c39eb3d885c3335) Thanks [@l7aromeo](https://github.com/l7aromeo)! - An interceptor that calls `next.handle()` without returning or awaiting it no longer crashes the bot when the handler throws. The handler's error was an unhandled rejection, which ends the process, and the filters, the fallback and the user never saw it, while observers reported the call as `'ran'` and `@Defer()` released the message before the handler finished. Now, when an interceptor returns and leaves what `next.handle()` returns, or a `then` or `finally` chain from it, without a rejection handler, as `next.handle().then(log)` does, the call ends when the handler does and fails with what it throws: its filters and the fallback answer it as they would any handler error. An interceptor that awaits, returns or catches the promise, or races it against a timeout, behaves as before. A promise handed to something else, such as `Promise.all`, is still that one's to handle.
|
|
639
|
+
|
|
640
|
+
A handler that an interceptor took on, such as by racing it against a timeout, and that throws after the interceptor has returned is now logged as a warning naming the interceptor, the handler and the error, unless a handler of the interceptor's own, in a chain from `next.handle()`, disposes of it; one that rethrows it, or wraps it in an error of the app's, still leaves it to the warning. The call has ended by then, so nothing else reports it.
|
|
641
|
+
|
|
642
|
+
- [#328](https://github.com/meocord/meocord/pull/328) [`dffb24d`](https://github.com/meocord/meocord/commit/dffb24d02fa70750895708e99cadbdb9f018df0a) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The editor documentation of three APIs says more about what they do:
|
|
643
|
+
|
|
644
|
+
- `TestingModule.invoke` says that a guard's `GuardDeniedError`, a `UserError` and a `CooldownError` reject it, where `dispatch` resolves `{ ran, error }`, and its example shows both.
|
|
645
|
+
- `useTheme` names `themeFor`'s layers: the server's theme, then the user's, over the handler's `@UseTheme`.
|
|
646
|
+
- `createMock` says that a property its type declares as data is a mock function, so truthy, and shows passing the values the code reads.
|
|
647
|
+
|
|
648
|
+
- [#282](https://github.com/meocord/meocord/pull/282) [`1f8fbf4`](https://github.com/meocord/meocord/commit/1f8fbf44df456135a46036d5680423e36a573c0a) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `Logger` prints as `console.log` does. It redacts the bot's credentials from everything it prints, as 4.0.1 does.
|
|
649
|
+
|
|
650
|
+
- **Colour follows each line's stream.** Warnings and errors go to stderr and the other levels to stdout, but colour followed stdout alone, so `node dist/main.js 2>>errors.log` from a terminal wrote colour codes into the file. Each line now takes colour only where its own stream is a terminal, or `FORCE_COLOR` asks for it.
|
|
651
|
+
- **An object is coloured only where the rest of its line is.** An object or other non-string argument was always printed with colour codes, even into a file or a log collector, where they appear as raw escape sequences. It now prints in colour on a terminal, and plain where the output is not one. Set `FORCE_COLOR=1` to keep colour where your log viewer shows it.
|
|
652
|
+
- **Objects print as `console.log` prints them.** A small object such as `{ id, name }` takes one line, where 4.0 printed one property per line. Objects print four levels deep, and without their non-enumerable properties, as 4.0.1 does, where 4.0.0 and the earlier 4.1 betas printed every level and those properties: nested data a bot logs, such as a payload or its settings, still shows in full, and a discord.js structure prints a few hundred lines instead of everything it reaches. An error prints its stack, its own properties such as `code`, its `cause`, and an `AggregateError`'s errors, with its message and stack printed once, as in 4.0.1.
|
|
653
|
+
- **A value of any type prints, and none makes `Logger` throw.** A `Symbol` threw `Cannot convert a Symbol value to a string`, so a handler or an observer that threw a `Symbol` made MeoCord's own log of it throw too, and from an observer that became an unhandled rejection. Anything other than a string now prints as `console.log` prints it: `Symbol(boom)`, `10n`, `[Function: handler]`. A number or a boolean takes `console.log`'s colour rather than the level's.
|
|
654
|
+
- **`logger.info()` and `logger.verbose()` lines are tagged `[INFO]` and `[VERBOSE]`.** 4.0 tagged them `[LOG]`. They still print at the `log` level, so a filter or parser that matches `[LOG]` needs to match `[INFO]` and `[VERBOSE]` too to keep catching them. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#smaller-changes).
|
|
655
|
+
|
|
656
|
+
- [#322](https://github.com/meocord/meocord/pull/322) [`1ceb803`](https://github.com/meocord/meocord/commit/1ceb803e4f108254c1bec561ecb7d22651c4f091) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A `@MessageHandler(pattern)` handler whose params don't fit its pattern is now explained in three lines rather than twenty-four. The decorator had one form for each way a handler can be declared, so TypeScript explained the mismatch against every form, and the reason, such as a param the pattern lacks, came last. It now has one form, and the second line names the params that don't fit and what the pattern gives each: `"The handler's params do not fit the pattern": { side: { readonly 'not a param of the pattern': "side" } }`. What compiles is unchanged.
|
|
657
|
+
|
|
658
|
+
- [#304](https://github.com/meocord/meocord/pull/304) [`31f0f02`](https://github.com/meocord/meocord/commit/31f0f02cf68e4a67f4da33487bb06b5328701e0d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Two message handlers that can take the same message stop the bot at startup, wherever their starts are known as it starts. One with its own `prefix: '!'` and one using the app's `'!'`, own prefixes that share one (`'!'` and `['!', '?']`), `prefix: false` beside an app with no prefix, or two a mention starts in a server were all taken as different starts, so the order of your `controllers` decided which ran. The refusal names both handlers and their patterns; give one another prefix or pattern. An app whose prefix is a function gives its prefixes only as each message arrives, so a handler using it is refused only beside another that uses it too. Beside one with its own prefix or `prefix: false`, which the function can also give, the handler with its own start runs, whatever the order of your `controllers`.
|
|
659
|
+
|
|
660
|
+
A prefix function that finds no prefix for a message, returning an empty list, `undefined` or `null`, now lets no prefix start a command for it; a mention still does when `mention` is on. It took the message as it is, so in a server with no prefix of its own, plain chat starting with a command's word ran that command. Return `''` to take a message as it is. The function's type takes `undefined` and `null` too, so a lookup such as `return prefixes.get(id)` needs no cast.
|
|
661
|
+
|
|
662
|
+
- [#347](https://github.com/meocord/meocord/pull/347) [`928aa57`](https://github.com/meocord/meocord/commit/928aa578da374301a5388a40b770db2152422712) Thanks [@l7aromeo](https://github.com/l7aromeo)! - With `messages.dmOnError` on, a message command refused because the cooldown store is down now DMs its author, once per outage, the `meocord.dm.error` message, or `meocord.cooldown.storeDown` for a command sent in a DM. Such a command got no answer at all, so while the store was down every `!command` with a cooldown looked like a dead bot. Without `dmOnError` it is still skipped silently, as before.
|
|
663
|
+
|
|
664
|
+
- [#370](https://github.com/meocord/meocord/pull/370) [`0ff8eba`](https://github.com/meocord/meocord/commit/0ff8ebaba05a1453b90c15f5ac587cf2f8f29034) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing`'s mocks take where they were made, and what a user option carries, as discord.js reads them:
|
|
665
|
+
|
|
666
|
+
- **A channel given to `createMockInteraction` or `createMockMessage`** sets the `channelId`, `guildId` and `guild` the test leaves out. A DM channel is no server, so `inGuild()` is `false`, and a server's channel puts the mock in its server. An interaction given a channel kept a `channelId` of its own, so a per-channel cooldown counted each one apart, and a message given a DM channel said it was in a server. A server's channel that names no server is put in the mock's, and a channel in another server than the one the test gives, or a DM's where it gives a server, is refused with both named. The channel is cached on the mock's client, as the gateway caches it.
|
|
667
|
+
- **A mock channel's managers** have it as their `channel`, and a thread's `members` as their `thread`, as discord.js's do.
|
|
668
|
+
- **A user option from `createChatInputOptions`** carries its `user`, and in a server its `member`, as the gateway sends them. A member given carried only `member`, as in 4.0, so a handler's param typed `User` got the `GuildMember`.
|
|
669
|
+
- **A select menu given its `users`, `members`, `roles` or `channels`** has their ids as `values`, as Discord sends them. They stayed empty unless given as well.
|
|
670
|
+
|
|
671
|
+
- [#333](https://github.com/meocord/meocord/pull/333) [`60e0290`](https://github.com/meocord/meocord/commit/60e0290f69504f8547a00fea9dba46f8090603dd) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A mock select menu from `createMockInteraction` has picked nothing unless the test gives its choices, as discord.js builds one: `values` is an empty array, and `users` and `members`, `roles` or `channels` are empty `Collection`s, the ones its kind picks. They were stubs, so a handler's `interaction.users.map(...)` or `interaction.values.length` threw or read a function on a default mock. Values and collections a test gives are kept.
|
|
672
|
+
|
|
673
|
+
- [#374](https://github.com/meocord/meocord/pull/374) [`78d444e`](https://github.com/meocord/meocord/commit/78d444e178890d6e5c5327d259b97b311409352b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A modal Discord refuses as already acknowledged (40060) now leaves the interaction answered, as a refused reply or update does, so the next `respond().send()` edits the answer that was made elsewhere instead of failing the same way. `modal()` still rejects with the refusal.
|
|
674
|
+
|
|
675
|
+
- [#397](https://github.com/meocord/meocord/pull/397) [`13d8521`](https://github.com/meocord/meocord/commit/13d852149541a2dc9f9bb6aef84e85f44cb39115) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A modal's file upload field reaches the handler's params as an array of the uploaded `Attachment`s, the ones `interaction.fields.getUploadedFiles()` gives. It gave the attachments' ids. For a bot upgrading from an earlier 4.1 beta, a handler that read ids from the field takes them from the attachments: `files.map(file => file.id)`.
|
|
676
|
+
|
|
677
|
+
`createModalFields` takes an array of `Attachment`s for a file upload field, so a test can submit one: `createModalFields({ screenshot: [attachment] })`.
|
|
678
|
+
|
|
679
|
+
- [#358](https://github.com/meocord/meocord/pull/358) [`8773cf8`](https://github.com/meocord/meocord/commit/8773cf887717cb407c70dd31f0d59c2317004aaa) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A presenter that fails to draw a view no longer leaves the user without an answer:
|
|
680
|
+
|
|
681
|
+
- **An error view** whose `error()` throws or rejects, or returns a view MeoCord cannot render, such as a colour that is no colour or an empty text, is answered with MeoCord's own error view. The failure is still logged as the call's fault, and a testing module's `dispatch` still rejects with it. An asynchronous `error()` was the only one answered this way before.
|
|
682
|
+
- **A loading view** whose `loading()` fails the same way, or takes longer than a second to draw, is replaced by MeoCord's own loading view, with a warning naming the presenter, so the click is still locked and the handler still runs. A drawing that comes later is left unused.
|
|
683
|
+
|
|
684
|
+
- [#342](https://github.com/meocord/meocord/pull/342) [`d9a03d0`](https://github.com/meocord/meocord/commit/d9a03d072c550f408bf77f6dbb58a1fc99ce2240) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Three fixes to how `@MeoCord({ providers })` and a testing module wire classes and providers:
|
|
685
|
+
|
|
686
|
+
- A provider that would receive `ExecutionContext` is refused as the app is created: a factory provider whose `inject` lists it, or a `useClass` provider whose class injects it. The message names where it is declared and its token, such as `App: @MeoCord({ providers }): the provider for 'audit' uses Audit, which injects ExecutionContext, …`. Such a provider is made once, so the context it received was an empty one, bound for the whole app, and every later call shared it.
|
|
687
|
+
- A class whose own source contains the text `[native code]`, such as one that inspects functions, is injected like any other class of the app. It was taken for a built-in constructor and never bound, so the app stopped at startup with `No bindings found for service`.
|
|
688
|
+
- Providers or classes that inject each other in a cycle are refused as the app is created, naming the cycle, such as `'a' → 'b' → 'a'`. The app failed when the first of them was made, with `Circular dependency found: (No dependency trace)`.
|
|
689
|
+
|
|
690
|
+
- [#353](https://github.com/meocord/meocord/pull/353) [`bbe1ef2`](https://github.com/meocord/meocord/commit/bbe1ef286981e687385c0d25df60b793c9cc2b1e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A reaction no longer re-fetches its message from Discord's API when the bot already holds it whole. Every reaction add and remove cost one request, queued behind Discord's rate limits, so a reaction-role or poll message delayed every handler under a burst of reactions. Now `reaction.message` is the copy the gateway keeps current, fetched first only when the bot holds the message by its id alone, and a reaction that arrives without its count (with `Partials.Reaction`) is fetched once, so `reaction.count` is no longer `null`.
|
|
691
|
+
|
|
692
|
+
This changes 4.0's behaviour, which fetched the message for every reaction. The cached copy differs from a fresh fetch only rarely: after a reconnect that could not resume and so missed an edit, or for a poll's counts when the bot lacks the `GuildMessagePolls` intent. A handler that needs the message straight from Discord calls `await reaction.message.fetch()` itself.
|
|
693
|
+
|
|
694
|
+
- [#397](https://github.com/meocord/meocord/pull/397) [`fe021d7`](https://github.com/meocord/meocord/commit/fe021d7a2230d292ce01fb953b6e177e14193907) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `HandlerRegistry.messageHelp()` reads the message commands from the table dispatch routes with, so a help command of the app's own lists exactly what the built-in help lists. It also read `@MessageHandler`s on services, which no message reaches, and listed or described them as commands.
|
|
695
|
+
|
|
696
|
+
- [#288](https://github.com/meocord/meocord/pull/288) [`6e62061`](https://github.com/meocord/meocord/commit/6e62061bb663ac4d802dcca3d2d7e03bc6e5434d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `respond()` makes the answers of one interaction one after another, and keeps its state right when Discord refuses one.
|
|
697
|
+
|
|
698
|
+
- Answers asked for together, such as two `send()` calls at once or the fallback's error while a `send()` is in flight, run in the order they were made: the first replies and the next edits or follows up, instead of both replying.
|
|
699
|
+
- An acknowledgement that fails leaves the interaction unanswered, so the next `send()` replies instead of throwing that failure again.
|
|
700
|
+
- A reply or an update Discord refuses as already acknowledged still throws, and the next `send()` edits. The error answer MeoCord follows up with after such a refusal now reaches Discord.
|
|
701
|
+
- `send()`, `edit()` and `delete()` after `modal()` throw an error saying to answer the modal's submit, since a modal has no message. `modal()` also throws while another answer is in flight.
|
|
702
|
+
- In `meocord/testing`, a mock command that showed a modal rejects `editReply()`, `fetchReply()` and `deleteReply()` with `Unknown Message` (10008).
|
|
703
|
+
|
|
704
|
+
- [#335](https://github.com/meocord/meocord/pull/335) [`e524937`](https://github.com/meocord/meocord/commit/e5249372ca2170003a05d96924bf4ab2fb42add3) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Routing fixes for handlers that compete for the same interaction:
|
|
705
|
+
|
|
706
|
+
- **Two `@Autocomplete` handlers for one option are named at startup.** Two handlers that complete the same option of a command, or every option of one path, used to start silently, and the first controller listed always won. The warning about command handlers that never run now names the one that never does, and the one that runs instead. Handlers of an option Discord never asks to complete are named for that alone. The bot still starts. In the next major version (5.0), it refuses to start. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#a-second-autocomplete-for-one-option-logs-a-warning).
|
|
707
|
+
- **The warning about overlapping component patterns names the handler that runs.** For each pair of patterns that can match the same customId, such as `a/{x}/c` and `a/b/{y}`, it now says which handler runs for the ids both match, and why: the more specific pattern, or between equally specific ones, the one whose controller is listed (or handler declared) first, as in 4.0. Where the next major version (5.0) runs the other one instead, preferring the pattern that spells out the first segment where the two differ, the warning says so, and what to do so the bot does the same before and after: list that handler's controller (or declare that handler) first, or make the patterns distinct. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#overlapping-component-patterns-meocord-5-prefers-the-one-that-spells-out-more).
|
|
708
|
+
- **The overlap warning is given once, as the bot starts.** It is a startup check like the others: `meocord start`, `meocord register` and the shard manager give it once, where each process-sharded shard gave it again, and `MeoCordTestingModule.compile()` gives it without anything being dispatched. Two handlers whose patterns match exactly the same customIds are refused there too: by the shard manager before it spawns a shard, and by `MeoCordTestingModule.compile()`. A test that expected `invoke()` to reject such a module now sees `compile()` throw the same error.
|
|
709
|
+
- `@Command`'s documentation now says that only patterns matching exactly the same ids stop the bot, that overlapping ones are warned about, and how the next major version (5.0) breaks a tie. `@Autocomplete`'s names the two startup warnings about its handlers.
|
|
710
|
+
|
|
711
|
+
- [#396](https://github.com/meocord/meocord/pull/396) [`5ae9cc1`](https://github.com/meocord/meocord/commit/5ae9cc1b3e3d6ce4adb2a0d411976117b5c62c5f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Fixes at the edges of shutdown, sharding, themes and the handler registry:
|
|
712
|
+
|
|
713
|
+
- With `sharding.development`, a Ctrl+C while `meocord start --dev` restarts the bot joins that stop, as it does in one process; it no longer kills every shard and exits 1.
|
|
714
|
+
- The shard manager's `stop()` sets `process.exitCode` to 1 when it has to kill a shard, unless another code is set, as a bot in one process does when its client fails to close.
|
|
715
|
+
- A shard's `stop()`, and its report of a failed start, wait at most a second for a manager that is gone, where Bun would otherwise wait for ever.
|
|
716
|
+
- An `onShutdown` hook does not run for a class whose `onReady` was still running when shutdown began, even when it finishes while the calls under way are waited for, as the `OnShutdown` docs say.
|
|
717
|
+
- `useTheme()` outside a call reads the app's theme until the app has shut down, so `onShutdown` hooks and the calls shutdown waits for read it too.
|
|
718
|
+
- A `themeFor` lookup that `ThemeCache` forgot while it was in flight logs nothing when it fails.
|
|
719
|
+
- `HandlerRegistry` gives an entry point command, whose builder returns a REST body, its `command` and `description`. A handler without a builder of its own gets the JSON of its own kind of command, by type and name as Discord tells commands apart, and a context menu's by its whole name. A message command's `scope` is narrowed to `'guild'` by a `member`, `role` or `channel` param or flag, as help shows it.
|
|
720
|
+
- A context menu whose name has a space and whose builder cannot be serialised is no longer reported as one no builder registers.
|
|
721
|
+
- A localization under a key that is not a Discord locale is reported once, as an unknown locale, and its value is not checked.
|
|
722
|
+
|
|
723
|
+
- [#284](https://github.com/meocord/meocord/pull/284) [`204f93f`](https://github.com/meocord/meocord/commit/204f93fba64dbf6a93aa577c754f588ea08ab599) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Message command params, flags and customId segments look up param types and booleans in tables with no inherited keys, so a customId param named like an inherited key, such as `{__proto__:int}`, keeps its type. A word such as `constructor` or `toString` is now an invalid `bool` value, answered with the usage like any other wrong word. A `{name:type}` whose type is such a name is refused at startup as naming no type, whether or not the app adds its own `messages.types`. An app type is matched only by a name the app gave it.
|
|
724
|
+
|
|
725
|
+
`route().build()` reads a param's value and type by the param's own name, so a param named `constructor` or `toString` builds from the value given, and asks for one when none is.
|
|
726
|
+
|
|
727
|
+
- [#327](https://github.com/meocord/meocord/pull/327) [`47069bc`](https://github.com/meocord/meocord/commit/47069bc77b309963bf9cf80a8d18a6716f65799d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `ShardContext.call` fixes:
|
|
728
|
+
|
|
729
|
+
- **Identical concurrent calls run once each.** With process sharding, two identical calls made at once, such as two users triggering the same announcement, used to run once in each shard and share one answer, and a later identical call could get an earlier call's answer. Each call now runs in every shard and gets its own answer.
|
|
730
|
+
- **One process and tests now pass values as JSON, as process sharding does.** In one process, which is how `meocord start --dev` and the testing module run, the arguments and the result were passed as live objects, while process sharding sends them as JSON. A `Date` arrived as a `Date` in development and tests, then as a string in production, and a returned `Map` arrived as `{}`. They're now passed through JSON in every mode, so a test sees what production gets.
|
|
731
|
+
- **A class a provider stands in for can be called.** `call(Payments, 'charge')` with `providers: [{ provide: Payments, useClass: StripePayments }]` answered "Payments is not a controller or service of this app." It now runs `StripePayments.charge`. With process sharding, a call from another shard names the class, so the bot refuses to start when a provided class shares a name with a controller, a service or another provided class, as it already did for two controllers or services.
|
|
732
|
+
|
|
733
|
+
- [#380](https://github.com/meocord/meocord/pull/380) [`e0631ab`](https://github.com/meocord/meocord/commit/e0631ab6c7f5c7e54688890ed394ee4727446b6a) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The process-sharding manager checks what it can before it does anything:
|
|
734
|
+
|
|
735
|
+
- **A build for another platform stops before the manager registers commands or spawns a shard.** The check ran only in each shard, so the manager registered the commands and spawned shard 0 before the build was refused.
|
|
736
|
+
- **A missing bundle is found before anything is registered.** A manager started without a bundle to spawn shards from registered the commands and asked Discord for the shard count before saying it could not find one.
|
|
737
|
+
- **`start()` starts the manager once.** A second call, at once or later, registered the commands and spawned every shard again; it now waits for the first.
|
|
738
|
+
|
|
739
|
+
- [#321](https://github.com/meocord/meocord/pull/321) [`22e3c3e`](https://github.com/meocord/meocord/commit/22e3c3e5d5da091c42b8c52268f0bb8e88e6e50e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Process sharding now handles a shard whose start fails:
|
|
740
|
+
|
|
741
|
+
- **A failed start ends the shard, so its manager restarts it.** The failure might be a network error at login, a provider factory that rejects, or Discord being briefly unavailable. Before, the shard set exit code 1 and kept running without logging in, so the manager never restarted it and its servers stayed offline until the whole bot was restarted. The shard now exits 1 once `main.ts` has handled the rejection, and the manager restarts it with the usual backoff.
|
|
742
|
+
- **An app MeoCord refuses stops the bot.** Examples are two services with one name, a provider of the wrong shape, or native addons built for another platform. Every shard would refuse it alike, so the shard tells its manager, which logs "Shard N cannot start; stopping every shard." with the reason, and where it is, on lines of its own, stops every shard and exits 1. Under `meocord start --dev`, the session then reports that the application exited with code 1 and waits for a change, rather than that the bot could not log in. Before, the manager restarted the shard about once a minute, forever, while a process supervisor saw a healthy process.
|
|
743
|
+
- **A shard listens to its manager from the start.** A stop request, or the manager going away, while the shard's providers are still being made now stops the shard before it logs in. Before, the shard went on to log in with no manager, and a restarted manager then ran a second copy of it.
|
|
744
|
+
|
|
745
|
+
Outside process sharding, a failed `start()` still leaves the process to `main.ts`.
|
|
746
|
+
|
|
747
|
+
- [#351](https://github.com/meocord/meocord/pull/351) [`2ce5380`](https://github.com/meocord/meocord/commit/2ce5380e54d95e898cb49923dcdb43ca71cc16c0) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A shard whose manager is gone no longer crashes on a call with a cooldown. When the manager died, each shard began its graceful shutdown, but `ShardedCooldownStore` still sent to the closed IPC channel. On Node, that send raised an unhandled `'error'` event, so the shard exited 1 at once and skipped its shutdown hooks. On Bun, the message was dropped, and the call waited out `cooldownStoreTimeoutMs`. Now the store checks the channel before sending and reports a failed delivery to the call, so the call fails with `CooldownStoreError` at once on both runtimes, and the shutdown carries on. A call sent just before the manager died fails as soon as the channel closes, too, rather than waiting out `cooldownStoreTimeoutMs`.
|
|
748
|
+
|
|
749
|
+
- [#369](https://github.com/meocord/meocord/pull/369) [`951d5ac`](https://github.com/meocord/meocord/commit/951d5ac8eecf60df5acb7f1feaa5e4c5aec12292) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `shutdownTimeout` in `meocord.config.ts` is at most 2147478647 ms. Node fires a timer longer than 2147483647 ms at once, and the shard manager and `meocord start --dev` wait up to 5 seconds past `shutdownTimeout`, so a larger value made the bot or those two give up on shutdown at once. The config now refuses such a value as it loads, as it refuses a negative one. A bot started without the CLI, whose config is not checked, waits the default 10 seconds instead and warns, naming the value it was given.
|
|
750
|
+
|
|
751
|
+
`@MeoCord({ themeForTimeoutMs })` names `Infinity` or `NaN` in its refusal, where it said `null`, and words it as `cooldownStoreTimeoutMs` does.
|
|
752
|
+
|
|
753
|
+
- [#315](https://github.com/meocord/meocord/pull/315) [`f12857d`](https://github.com/meocord/meocord/commit/f12857d509aa72dd564102d64deda90f44153900) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A `@Catch` given something that is not an error class, such as an `undefined` from an import cycle, is named in a warning as the filter loads, and now matches no error: `Broken: @Catch's first entry, undefined, which matches no error, is deprecated; in the next major version (5.0) it is refused. Use an error class, such as @Catch(CooldownError), instead.` It used to throw "Right-hand side of 'instanceof' is not an object" at the first error to reach the filter, which hid the handler's own error and handled the call a second time. The bot still starts; in the next major version (5.0) it is refused. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#a-catch-entry-that-isnt-a-class-logs-a-warning).
|
|
754
|
+
|
|
755
|
+
- [#336](https://github.com/meocord/meocord/pull/336) [`1e442d1`](https://github.com/meocord/meocord/commit/1e442d11a3fd92735975ac1b86bde414288892d6) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A base controller's class stages now wrap the classes that extend it, as global stages wrap controllers. Guards and interceptors run the top base's first, then each subclass's, then the method's. Filters are tried the other way: the method's, then the subclass's, then each base's, then the global ones. In the 4.1 betas the subclass's guards ran before the base's, so a guard a subclass added ran even for callers the base's auth guard would refuse. For a handler the subclass declared itself, the base's filters were also tried first, so a catch-all on the base hid the subclass's own, more specific filter. Class cooldowns keep their order, and `inspectHandler` and the `MetadataKey.Guards` metadata list the stages in the order they now run.
|
|
756
|
+
|
|
757
|
+
- [#287](https://github.com/meocord/meocord/pull/287) [`dea37a4`](https://github.com/meocord/meocord/commit/dea37a41444c0ba00eef95864dace1d2eada49e5) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `start()` sets the bot up once. Calling it again after a failed login used to attach every event handler a second time, so each command, message and reaction ran twice, `onReady` ran twice, and an app with a presenter could no longer answer the built-in `help`. Now a retry logs in again with the handlers it already has. Two calls at once share one start, and a call once the bot is online does nothing.
|
|
758
|
+
|
|
759
|
+
After a retry logs in, the client works as a fresh one would: `isReady()` reports it, shutdown closes the gateway, and configured cache sweepers run again. A failed login makes discord.js destroy its client, so MeoCord undoes that before trying again.
|
|
760
|
+
|
|
761
|
+
A retry that logs in now clears the exit code the failed login set to `0`, which Bun keeps, rather than to `undefined`, which Bun ignores. It also gives code that runs outside a handler, such as a scheduled job calling `useTheme()`, the app's theme again, which the failed login had dropped.
|
|
762
|
+
|
|
763
|
+
Retrying `start()` after a failed login is deprecated; in the next major version (5.0) it rejects. Use `MeoCordFactory.create` to make a new app instead. It logs that warning once. A retry after a provider's factory failed stays supported, with no warning. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#retrying-start-after-a-failed-login-is-deprecated).
|
|
764
|
+
|
|
765
|
+
- [#379](https://github.com/meocord/meocord/pull/379) [`d5a2e03`](https://github.com/meocord/meocord/commit/d5a2e03e3fafd599a4081964b3e3d2b5eeda0e45) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A class that injects `CooldownStore` gets the app's store, and nothing else is made of it.
|
|
766
|
+
|
|
767
|
+
- **The store's hooks run once.** A service that injected `CooldownStore` beside `@MeoCord({ cooldownStore })` made the token count as a class of its own, so the store's `onReady` and `onShutdown` each ran twice, and the first `onShutdown` came before the calls under way had finished. The token now stands for the app's store, so the service depends on the store, and the store's hooks run once, after the last call.
|
|
768
|
+
- **`MeoCordTestingModule` binds the app's store first, as the bot does.** A module made with `fromApp()`, or with `app`, whose app has a `cooldownStore`, failed to compile with "Ambiguous bindings found for service: CooldownStore" when one of its classes injected the token.
|
|
769
|
+
- **A cycle through the token is named.** A store that injects a class which injects `CooldownStore` is refused where it is declared, naming the classes, as any other cycle is, rather than failing with inversify's "Circular dependency" as the bot starts.
|
|
770
|
+
|
|
771
|
+
- [#330](https://github.com/meocord/meocord/pull/330) [`c62618f`](https://github.com/meocord/meocord/commit/c62618fa7fff13dd07b28701020f030d014c2e3c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `MeoCordTestingModule.create({ app })` counts cooldowns in the app's `cooldownStore`, as `fromApp` and the bot do. It counted them in a store of its own in memory, so a test of a cooldown never reached the app's store. A `CooldownStore` in the module's `providers` still takes the app's place, and the app's own store is then never built, so what it injects needs no provider. A store of the app's that injects something the test doesn't list needs it in `providers`, or a `CooldownStore` provider in its place.
|
|
772
|
+
|
|
773
|
+
- [#392](https://github.com/meocord/meocord/pull/392) [`421081b`](https://github.com/meocord/meocord/commit/421081b9bc86caf5dbb7b0539b5ee4407795abd5) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The documentation your editor shows for `meocord/testing` and `meocord/decorator` now matches what the code does. Nothing to change in your code.
|
|
774
|
+
|
|
775
|
+
- **Mocks:** `createMockClient` says only a mock message gets a client of its own; give an interaction its `client` when the code under test reaches it. `createMockGuild` says what a manager's `fetch(id)` makes: a member or channel in the guild, a role with that id, or a ban. `createMockUser` is for a user, and `MockProps` names only a property the mock does not let you assign. `createMock`, `createMockGuild` and `createChatInputOptions` document their parameter.
|
|
776
|
+
- **Testing module:** the `invoke`, `themeCache` and `overrideThemeFor` examples compile, and `expectCompleteCatalog`'s example passes. `observers` and `init()` name `dispatch` beside `invoke` and `emit`. `inspectHandler`'s `app` says the app's filters are tried after the handler's own. `resolveRoute`'s `dm` says when a handler outside its scope is returned. A `testCooldownStore` case title claims only what it checks.
|
|
777
|
+
- **Decorators:** `@Cooldown`, `@Validate` and `cooldownStoreFailure` say how a message command over its limit, with invalid input or with the store down is answered. `@Defer` describes `mode: 'auto'` and when a misplaced `@Defer` is refused. `@Service` says when to list a class in `services`. `@Controller` says which handlers its class stages reach. `@Command` lists everything it refuses as it applies, and `@Autocomplete` says a handler's own guards run. `@Guard`, `@Interceptor`, `@Observer` and `@Validate` document their options.
|
|
778
|
+
|
|
779
|
+
- [#398](https://github.com/meocord/meocord/pull/398) [`14f6d3c`](https://github.com/meocord/meocord/commit/14f6d3ce63a758437b3d7eb57d1d2a5a74ae4958) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing` mocks and `resolveRoute` behave as the bot does in four more places:
|
|
780
|
+
|
|
781
|
+
- **A mock interaction has a client.** `createMockInteraction` gives an interaction made without a `client` one from `createMockClient`, as a mock message has, with the interaction's user in `client.users.cache` and its channel in `client.channels.cache` once read. Code that reaches `interaction.client`, such as `interaction.client.users.cache` or `interaction.client.user.id`, reads real caches and the mock bot's id rather than stubs.
|
|
782
|
+
- **`getAttachment()` returns an attachment option.** `createChatInputOptions({ file })` with an `Attachment` makes `options.getAttachment('file')` return it, and `null` for an option not given.
|
|
783
|
+
- **Resolved media never takes the bot's id.** A thumbnail, image or media gallery item an edit resolves gets an id no other mock has, the mock bot's included.
|
|
784
|
+
- **`resolveRoute(app, { content, dm: true })` returns nothing for a handler that works only in a server,** by its `scope` or a `member`, `role` or `channel` param in its pattern, as dispatch answers such a message with its usage and never runs the handler.
|
|
785
|
+
|
|
786
|
+
- [#289](https://github.com/meocord/meocord/pull/289) [`1659e78`](https://github.com/meocord/meocord/commit/1659e78b9c00e004be4c36888daf7a10246edd58) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A build resolves your `paths` from `compilerOptions.baseUrl` when your `tsconfig.json` sets it, as TypeScript does. Before, the build read every `paths` target from the project root, so an alias such as `"@lib/*": ["lib/*"]` with `"baseUrl": "./src"` typechecked but failed to resolve in `meocord build` and `meocord start --dev`. A `baseUrl` your `tsconfig.json` only inherits through `extends` isn't applied to the `paths` it sets itself, so declare those `paths` relative to your project's own `tsconfig.json`. TypeScript 6, which a new app pins, deprecates `baseUrl`, and `tsc` stops with TS5101 where it is set. Without `baseUrl`, TypeScript and the build both read `paths` from the folder `tsconfig.json` is in, as a new app's `"@src/*": ["./src/*"]` does, so write each target from there, or keep `baseUrl` and set `"ignoreDeprecations": "6.0"`.
|
|
787
|
+
|
|
788
|
+
`meocord start --dev` rebuilds when you save `meocord.config.ts` or `tsconfig.json`, and each rebuild writes a copy of your `tsconfig.json`. Those copies now share one temporary directory and one exit listener for the session, instead of one each. On Node, a session with several such saves no longer prints `MaxListenersExceededWarning` for `exit`.
|
|
789
|
+
|
|
790
|
+
A build or `start --dev` that stops before it can clean up, such as one killed, crashed or closed with its terminal, left its directory in your system's temp directory for good. The next build or `start --dev` on the same machine now removes it. Directories left by earlier 4.1 betas, named `meocord-tsconfig-` and six characters, can't be told apart from a build that is still running, so they stay: delete them yourself while no MeoCord build or `start --dev` is running.
|
|
791
|
+
|
|
792
|
+
- [#338](https://github.com/meocord/meocord/pull/338) [`b6a34af`](https://github.com/meocord/meocord/commit/b6a34af3da9535d538a134203d66086d10fc6e31) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `warnUnanswered` now also warns when an interceptor returns without calling `next.handle()` and leaves the interaction unanswered, or deferred by `@Defer()` with no follow-up, as an interceptor that answers from a cache can. The warning names the interceptor: `Shop.buy: its interceptor Cached returned before the handler ran, without answering the interaction, …`. It warned only when the handler itself ran, so this case passed silently while the user saw "The application did not respond".
|
|
793
|
+
|
|
794
|
+
An interceptor that returns before the handler finishes, as one racing `next.handle()` against a timeout does, is named the same way, the outermost when several do, `… returned before the handler finished, …`, rather than the warning blaming the handler for an answer it was still about to send.
|
|
795
|
+
|
|
796
|
+
- [#323](https://github.com/meocord/meocord/pull/323) [`ef58d97`](https://github.com/meocord/meocord/commit/ef58d97c38be3d12c878e682ace20ff2bb613118) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The startup warning about command handlers that Discord never sends now also covers `@Autocomplete` handlers. One is named when:
|
|
797
|
+
|
|
798
|
+
- no builder registers its command;
|
|
799
|
+
- its path isn't a subcommand that its command's builder registers;
|
|
800
|
+
- it names an option the builder doesn't register, or one built without `setAutocomplete(true)`;
|
|
801
|
+
- it completes every option of a subcommand that has no option with autocomplete on.
|
|
802
|
+
|
|
803
|
+
A handler of the whole command is checked against every option of the command, its subcommands' included, since MeoCord falls back to it for all of them. The bot still starts. In the next major version (5.0), these refuse to start. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#a-command-handler-discord-never-sends-logs-a-warning).
|
|
804
|
+
|
|
805
|
+
`MeoCordTestingModule.compile()` names the same cases, apart from a command no builder registers, as it already does for `@Command` handlers.
|
|
806
|
+
|
|
807
|
+
`meocord generate controller autocomplete <name>` now ends by saying what to add to `/<name>`'s builder, the `query` option with `setAutocomplete(true)`, since the generated handler completes that option and Discord never asks it to until the command declares it.
|
|
808
|
+
|
|
809
|
+
- [#307](https://github.com/meocord/meocord/pull/307) [`db19771`](https://github.com/meocord/meocord/commit/db19771873e3c4b66c8c0ee2072f93a21d676422) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A command handler that Discord never sends an interaction to now gets a warning when the app is created, so `meocord start`, `meocord register` and the shard manager report it. Before, such a handler was dead or misrouted with no sign at startup. One warning names every such handler, what is wrong and what to do:
|
|
810
|
+
|
|
811
|
+
- a subcommand path that the command's builder doesn't register: `@Command('settings notfy', CommandType.SLASH)` beside a `settings` builder with `view` and `notify`. Before, `/settings notify` silently ran the `settings` handler;
|
|
812
|
+
- a customId pattern, such as a `route()`, given to a slash, context menu or entry point handler, which is matched by its command name;
|
|
813
|
+
- a builder that registers another name than its `@Command`'s, such as `setName('ping')` under `@Command('pong', PingBuilder)`;
|
|
814
|
+
- a slash, context menu or entry point command that no builder registers at all.
|
|
815
|
+
|
|
816
|
+
The bot still starts. In the next major version (5.0), these refuse to start. See [the upgrade guide](https://meocord.dev/docs/4.1/migrating#a-command-handler-discord-never-sends-logs-a-warning).
|
|
817
|
+
|
|
818
|
+
`MeoCordTestingModule.compile()` gives the same warning, apart from the last case: a handler with a `CommandType` and no builder is how a test fixture is written.
|
|
819
|
+
|
|
820
|
+
- [#339](https://github.com/meocord/meocord/pull/339) [`d2fb9cf`](https://github.com/meocord/meocord/commit/d2fb9cfc09371cd3fccbe2e9c803dd1ed33847b4) Thanks [@l7aromeo](https://github.com/l7aromeo)! - How MeoCord logs and echoes what users send:
|
|
821
|
+
|
|
822
|
+
- **Autocomplete and reactions log their users' outcomes at debug level**, as commands do. An autocomplete call that a guard denies, or whose handler throws a `UserError` or a `ValidationError`, still closes its menu, and is no longer logged as an error with its stack. The same holds for a guard that denies a reaction. `TestingModule.dispatch` resolves for such an autocomplete call or reaction, where it rejected; read what stopped it from the result's `error`.
|
|
823
|
+
- **Log lines quote what a user sent on one line.** A message's text, a reaction's emoji and a component's customId are quoted with line breaks, control characters and quotes escaped, and a message's text is cut short after 200 characters, with its length. So are the messages of the debug lines for a refused, denied or invalid call.
|
|
824
|
+
- **Usage and help replies show the user's words as typed.** A param's value, a flag's name or value, and the command a `help` query names are quoted with their markdown escaped and on one line, so `**up**` or `[text](https://example.com)` reads as written instead of rendering as bold text or a link.
|
|
825
|
+
|
|
3
826
|
## 4.1.0-beta.9
|
|
4
827
|
|
|
5
828
|
### Minor Changes
|