meocord 4.1.2 → 4.2.0

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
  3. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
  4. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
  5. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
  6. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
  7. package/dist/cjs/common/index.cjs +4 -4
  8. package/dist/cjs/core/index.cjs +195 -157
  9. package/dist/cjs/decorator/index.cjs +66 -57
  10. package/dist/cjs/testing/index.cjs +733 -301
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/README.md.template +3 -1
  13. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  15. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  17. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  19. package/dist/esm/build/load-config.pre-entry.js +4 -1
  20. package/dist/esm/common/decorator.js +6 -6
  21. package/dist/esm/common/deprecation.js +2 -2
  22. package/dist/esm/common/errors.js +2 -2
  23. package/dist/esm/common/execution-context.js +1 -1
  24. package/dist/esm/common/metadata.js +1 -1
  25. package/dist/esm/common/route.js +10 -1
  26. package/dist/esm/common/translate-error.js +1 -1
  27. package/dist/esm/core/command-conflicts.js +34 -34
  28. package/dist/esm/core/component-routes.js +74 -31
  29. package/dist/esm/core/dispatcher.js +51 -7
  30. package/dist/esm/core/filter-runner.js +2 -2
  31. package/dist/esm/core/handler-pipeline.js +27 -14
  32. package/dist/esm/core/handler-registry.js +1 -1
  33. package/dist/esm/core/interceptor-runner.js +2 -2
  34. package/dist/esm/core/meocord-factory.js +196 -157
  35. package/dist/esm/core/message-routes.js +4 -3
  36. package/dist/esm/core/observer-runner.js +4 -3
  37. package/dist/esm/core/providers.js +38 -21
  38. package/dist/esm/core/route-specificity.js +4 -4
  39. package/dist/esm/core/scalar-types.js +15 -3
  40. package/dist/esm/core/shard-context.js +1 -1
  41. package/dist/esm/core/startup-roots.js +2 -3
  42. package/dist/esm/core/theme-runtime.js +2 -2
  43. package/dist/esm/decorator/app.decorator.js +12 -7
  44. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  45. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  46. package/dist/esm/decorator/controller.decorator.js +110 -67
  47. package/dist/esm/decorator/cooldown.decorator.js +3 -3
  48. package/dist/esm/decorator/defer.decorator.js +4 -4
  49. package/dist/esm/decorator/event.decorator.js +5 -5
  50. package/dist/esm/decorator/filter.decorator.js +7 -7
  51. package/dist/esm/decorator/guard.decorator.js +8 -8
  52. package/dist/esm/decorator/inject.decorator.js +4 -4
  53. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  54. package/dist/esm/decorator/observer.decorator.js +4 -4
  55. package/dist/esm/decorator/service.decorator.js +4 -4
  56. package/dist/esm/decorator/theme.decorator.js +3 -3
  57. package/dist/esm/decorator/validation.decorator.js +10 -10
  58. package/dist/esm/package.json.js +1 -1
  59. package/dist/esm/testing/catalog.js +1 -1
  60. package/dist/esm/testing/cooldown-store-suite.js +18 -10
  61. package/dist/esm/testing/discord-defaults.js +60 -7
  62. package/dist/esm/testing/index.js +4 -2
  63. package/dist/esm/testing/inspect-handler.js +20 -3
  64. package/dist/esm/testing/meocord-testing-module.js +229 -188
  65. package/dist/esm/testing/mock-fn.js +80 -10
  66. package/dist/esm/testing/mock-interaction.js +223 -72
  67. package/dist/esm/testing/response.js +1 -1
  68. package/dist/esm/testing/routing.js +39 -14
  69. package/dist/esm/testing/startup-errors.js +26 -0
  70. package/dist/esm/testing/strict-mocks.js +45 -0
  71. package/dist/esm/testing/theme.js +1 -1
  72. package/dist/esm/util/bundle-entry.util.js +2 -1
  73. package/dist/esm/util/interaction.util.js +1 -1
  74. package/dist/esm/util/meocord-config-validation.util.js +1 -0
  75. package/dist/esm/util/metadata-keys.js +1 -0
  76. package/dist/esm/util/refusal.util.js +148 -8
  77. package/dist/types/common/index.d.cts +8 -8
  78. package/dist/types/common/index.d.ts +8 -8
  79. package/dist/types/core/index.d.cts +8 -6
  80. package/dist/types/core/index.d.ts +8 -6
  81. package/dist/types/decorator/index.d.cts +30 -29
  82. package/dist/types/decorator/index.d.ts +30 -29
  83. package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
  84. package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
  85. package/dist/types/interface/index.d.cts +1 -1
  86. package/dist/types/interface/index.d.ts +1 -1
  87. package/dist/types/{route-C-aMI7Sl.d.cts → route-BZgFoNeB.d.cts} +5 -2
  88. package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
  89. package/dist/types/testing/index.d.cts +245 -31
  90. package/dist/types/testing/index.d.ts +245 -31
  91. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
  92. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
  93. package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
  94. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
  95. package/package.json +3 -2
@@ -1,10 +1,10 @@
1
1
  import { ServiceIdentifier, Container } from 'inversify';
2
- import { Client, Interaction, Message, MessageReaction, PartialMessageReaction, User, PartialUser, ClientEvents, Base, Locale, GuildMember, Role, GuildBasedChannel, Guild, APIMessageTopLevelComponent, JSONEncodable, APIEmbed, MessageFlagsResolvable, TextBasedChannel, BaseChannel, APIAuthorizingIntegrationOwnersMap, CacheType, CommandInteractionOptionResolver, ThreadChannel, AnyThreadChannel, ChatInputCommandInteraction, AutocompleteInteraction, MessageContextMenuCommandInteraction, UserContextMenuCommandInteraction, PrimaryEntryPointCommandInteraction, ContextMenuCommandInteraction, CommandInteraction, ButtonInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ChannelSelectMenuInteraction, MessageComponentInteraction, ModalSubmitInteraction, BaseInteraction, DMChannel, PartialGroupDMChannel, OmitPartialGroupDMChannel, Attachment, ModalSubmitFields, MessagePayload, InteractionReplyOptions, InteractionUpdateOptions, InteractionEditReplyOptions, MessageEditOptions, APIModalInteractionResponseCallbackData, ModalComponentData, InteractionDeferReplyOptions, InteractionDeferUpdateOptions, MessageResolvable, DiscordAPIError } from 'discord.js';
2
+ import { Client, Interaction, Message, MessageReaction, PartialMessageReaction, User, PartialUser, ClientEvents, Base, Locale, GuildMember, Role, GuildBasedChannel, Guild, APIMessageTopLevelComponent, JSONEncodable, APIEmbed, MessageFlagsResolvable, TextBasedChannel, BaseChannel, APIAuthorizingIntegrationOwnersMap, APIInteractionGuildMember, PermissionResolvable, APIUser, CacheType, CommandInteractionOptionResolver, ThreadChannel, AnyThreadChannel, ChatInputCommandInteraction, AutocompleteInteraction, MessageContextMenuCommandInteraction, UserContextMenuCommandInteraction, PrimaryEntryPointCommandInteraction, ContextMenuCommandInteraction, CommandInteraction, ButtonInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ChannelSelectMenuInteraction, MessageComponentInteraction, ModalSubmitInteraction, BaseInteraction, DMChannel, PartialGroupDMChannel, OmitPartialGroupDMChannel, Attachment, ModalSubmitFields, MessagePayload, InteractionReplyOptions, InteractionUpdateOptions, InteractionEditReplyOptions, MessageEditOptions, APIModalInteractionResponseCallbackData, ModalComponentData, InteractionDeferReplyOptions, InteractionDeferUpdateOptions, MessageResolvable, DiscordAPIError } from 'discord.js';
3
3
  import { ReactionHandlerAction, CommandType } from '../enum/index.js';
4
- import { a8 as Provider, P as ProviderToken } from '../index-D7n0FBMm.js';
4
+ import { a8 as Provider, P as ProviderToken } from '../index-CdXsFgjE.js';
5
5
  import { CooldownStore, ExecutionContextType, ExecutionContext, CooldownScope, MetadataDecorator, ResponsePhase, ResponseCall } from '../common/index.js';
6
6
  import { DispatchObserver, GuardInterface, InterceptorInterface, ExceptionFilter, ThemeOverride, ThemeResolvers, ThemeResolver, MessageCommandOptions, MessagePrefix, RootTheme, DeepReadonly, MeoCordTheme } from '../interface/index.js';
7
- export { ao as ClassProvider, F as FactoryProvider, ap as ValueProvider } from '../index-D7n0FBMm.js';
7
+ export { ao as ClassProvider, F as FactoryProvider, ap as ValueProvider } from '../index-CdXsFgjE.js';
8
8
  import { ThemeCache } from '../common/index.js';
9
9
  import { Translator } from '../common/index.js';
10
10
  import '@rsbuild/core';
@@ -194,7 +194,7 @@ interface EmitResult {
194
194
  * @category Module
195
195
  * @see {@link MeoCordTestingModule}
196
196
  * @see {@link getResponse}
197
- * @see {@link https://meocord.dev/docs/4.1/invoke-and-dispatch | Invoke and dispatch}
197
+ * @see {@link https://meocord.dev/docs/4.2/invoke-and-dispatch | Invoke and dispatch}
198
198
  */
199
199
  declare class TestingModule {
200
200
  private readonly container;
@@ -470,7 +470,7 @@ declare class TestingModule {
470
470
  * @group Testing
471
471
  * @category Module
472
472
  * @see {@link MeoCordTestingModule}
473
- * @see {@link https://meocord.dev/docs/4.1/testing | The testing module}
473
+ * @see {@link https://meocord.dev/docs/4.2/testing | The testing module}
474
474
  */
475
475
  declare class TestingModuleBuilder {
476
476
  private readonly options;
@@ -578,7 +578,12 @@ declare class TestingModuleBuilder {
578
578
  * its command's builder does not register, a customId pattern given as a command name, a builder that registers
579
579
  * another name, and component patterns that can match the same customId are named, as the bot names them.
580
580
  *
581
+ * Each check reports every startup error it finds, and the first is thrown, unchanged; when there are several, each
582
+ * is logged first, with the file it comes from. After {@link reportAllStartupErrors}, the errors decorators kept on
583
+ * the classes the module runs are among them.
584
+ *
581
585
  * @returns The compiled module.
586
+ * @throws Error for the first startup error the checks find.
582
587
  */
583
588
  compile(): TestingModule;
584
589
  }
@@ -609,7 +614,7 @@ declare class TestingModuleBuilder {
609
614
  * @category Module
610
615
  * @see {@link TestingModuleBuilder}
611
616
  * @see {@link TestingModule}
612
- * @see {@link https://meocord.dev/docs/4.1/testing | The testing module}
617
+ * @see {@link https://meocord.dev/docs/4.2/testing | The testing module}
613
618
  */
614
619
  declare class MeoCordTestingModule {
615
620
  /**
@@ -805,12 +810,76 @@ type Mock<T extends (...args: any[]) => any = (...args: any[]) => any> = MockedF
805
810
  * @see {@link createMockFn}
806
811
  */
807
812
  declare function isMockFunction(fn: unknown): fn is MockInstance;
813
+ /**
814
+ * A mock function a test runner makes, as {@link useMockFn} takes it.
815
+ *
816
+ * It is callable, records its calls in `mock.calls`, and has `mockImplementation`, `mockClear` and `mockReset`.
817
+ * Vitest's `vi.fn`, jest's `jest.fn` and bun's `mock` make one.
818
+ *
819
+ * @group Testing
820
+ * @category Mocks
821
+ * @see {@link MockFnFactory}
822
+ */
823
+ interface RunnerMock {
824
+ (...args: any[]): any;
825
+ /** The calls the mock has recorded, each an array of its arguments. */
826
+ mock: {
827
+ calls: readonly unknown[][];
828
+ };
829
+ /** Sets the implementation the mock runs. */
830
+ mockImplementation(fn: (...args: any[]) => any): unknown;
831
+ /** Forgets the calls the mock has recorded. */
832
+ mockClear(): unknown;
833
+ /** Forgets the calls and what the mock was told to do. */
834
+ mockReset(): unknown;
835
+ }
836
+ /**
837
+ * A test runner's mock function factory, such as `vi.fn`.
838
+ *
839
+ * Called with the implementation a mock starts with, it returns the runner's mock. See {@link useMockFn}.
840
+ *
841
+ * @group Testing
842
+ * @category Mocks
843
+ */
844
+ type MockFnFactory = (impl?: (...args: any[]) => any) => RunnerMock;
845
+ /**
846
+ * Makes every mock `meocord/testing` creates with the test runner's own mock function, such as Vitest's `vi.fn`.
847
+ *
848
+ * Call it once in the runner's setup file. The runner then treats meocord's mocks as its own: its `clearMocks` and
849
+ * `mockReset` config and `vi.clearAllMocks()` reach them, its matchers read them, and its whole mock API is there.
850
+ * Without it, mocks are {@link createMockFn}'s own, which jest's and Vitest's `expect` read too.
851
+ *
852
+ * @remarks
853
+ * Each mock starts with the runner's mock holding meocord's behaviour, such as an interaction's replies, as its own
854
+ * implementation. {@link resetAllMocks} puts that behaviour back where the runner's `mockReset` drops it, as jest's and
855
+ * bun's do; a runner's own reset-all, such as jest's `resetMocks`, can't, so under jest and bun reset with
856
+ * {@link resetAllMocks}. node:test's `mock.fn` records calls in a shape of its own, so under node:test leave meocord's
857
+ * own function in place. Calling it again with the same factory does nothing; another, once a mock exists, throws.
858
+ *
859
+ * @param factory - The runner's mock function factory: `vi.fn`, `jest.fn` or bun's `mock`.
860
+ * @throws Error when a mock already exists from another factory, or when `factory` doesn't make a mock with jest's and
861
+ * Vitest's API.
862
+ *
863
+ * @example
864
+ * ```ts
865
+ * // vitest.setup.ts
866
+ * import { vi } from 'vitest'
867
+ *
868
+ * useMockFn(vi.fn)
869
+ * ```
870
+ *
871
+ * @group Testing
872
+ * @category Mocks
873
+ * @see {@link createMockFn}
874
+ */
875
+ declare function useMockFn(factory: MockFnFactory): void;
808
876
  /**
809
877
  * Clears the calls every mock from `meocord/testing` has recorded, keeping what each was told to do.
810
878
  *
811
- * Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()` mocks. This one
812
- * covers `createMockFn` and everything built on it, such as `createMockInteraction` and `createMockClient`. To also
813
- * undo what a test set, use {@link resetAllMocks}.
879
+ * Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()` mocks, and
880
+ * meocord's too once {@link useMockFn} makes them with the runner's function. This one covers `createMockFn` and
881
+ * everything built on it, such as `createMockInteraction` and `createMockClient`, either way. To also undo what a test
882
+ * set, use {@link resetAllMocks}.
814
883
  *
815
884
  * @example
816
885
  * ```ts
@@ -850,7 +919,8 @@ declare function resetAllMocks(): void;
850
919
  * Creates a mock function that jest's and Vitest's `expect` both read.
851
920
  *
852
921
  * Use it for a function double in code that must not depend on one test runner, such as a shared test helper; in a
853
- * Vitest test, `vi.fn()` works as well. {@link clearAllMocks} and {@link resetAllMocks} reach it.
922
+ * Vitest test, `vi.fn()` works as well. {@link clearAllMocks} and {@link resetAllMocks} reach it. Once
923
+ * {@link useMockFn} registers a runner's function, it returns that runner's mock.
854
924
  *
855
925
  * @remarks
856
926
  * `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue` and `mockImplementation` share one implementation, so
@@ -973,9 +1043,35 @@ type OutsideServer = {
973
1043
  * @category Mocks
974
1044
  * @see {@link MockProps}
975
1045
  * @see {@link createChatInputOptions}
976
- * @see {@link https://meocord.dev/docs/4.1/mocks | Mocks}
1046
+ * @see {@link https://meocord.dev/docs/4.2/mocks | Mocks}
977
1047
  */
978
1048
  declare function createMockInteraction<T extends object>(Class: InteractionClass<T>, props?: MockProps<T>): DeepMocked<T>;
1049
+ /**
1050
+ * Creates a mock interaction from a server the bot isn't in: one given a {@link createMockRawMember | raw member} and
1051
+ * that server's `guildId`, as a user-installed command run there arrives.
1052
+ *
1053
+ * It is typed as discord.js types an interaction from such a server, `'raw'`: its `guild` is `null` and its `member`
1054
+ * the raw member. Everything else is as {@link createMockInteraction} builds it.
1055
+ *
1056
+ * @param Class - The discord.js interaction class to mock.
1057
+ * @param props - The interaction's properties, with the raw `member` and the server's `guildId`; see {@link MockProps}.
1058
+ *
1059
+ * @example
1060
+ * ```ts
1061
+ * import { expect } from 'vitest'
1062
+ *
1063
+ * const command = createMockInteraction(ChatInputCommandInteraction, { guildId: '100000000000000001', member: createMockRawMember() })
1064
+ * expect(command.inRawGuild()).toBe(true)
1065
+ * ```
1066
+ *
1067
+ * @group Testing
1068
+ * @category Mocks
1069
+ */
1070
+ declare function createMockInteraction<T extends object>(Class: InteractionClass<T>, props: Omit<MockProps<WithCache<T, 'raw'>>, 'options'> & {
1071
+ options?: object;
1072
+ guildId: string;
1073
+ member: APIInteractionGuildMember;
1074
+ }): DeepMocked<WithCache<T, 'raw'>>;
979
1075
  /**
980
1076
  * Creates a mock interaction from outside a server: one given `guild: null`, `member: null` or a DM channel, as from a
981
1077
  * direct message or a server the bot isn't in.
@@ -1171,6 +1267,48 @@ type ChannelOf<T> = T extends ThreadChannel ? AnyThreadChannel : T;
1171
1267
  * @see {@link createMockGuild}
1172
1268
  */
1173
1269
  declare function createMockChannel<T extends BaseChannel>(Class: InteractionClass<T>, props?: MockProps<T>): DeepMocked<ChannelOf<T>>;
1270
+ /**
1271
+ * What {@link createMockRawMember} builds a member with: any field Discord sends, with `permissions` as a permission
1272
+ * set and `user` as the fields of the user given.
1273
+ *
1274
+ * @group Testing
1275
+ * @category Mocks
1276
+ */
1277
+ type MockRawMemberOverrides = Partial<Omit<APIInteractionGuildMember, 'permissions' | 'user'>> & {
1278
+ /** The member's permissions in the channel; unless given, what @everyone has in a server Discord makes today. */
1279
+ permissions?: PermissionResolvable;
1280
+ /** The member's user; a new person, `username` `'user'`, unless given. */
1281
+ user?: Partial<APIUser>;
1282
+ };
1283
+ /**
1284
+ * Creates the member Discord sends with an interaction from a server the bot isn't in, as discord.js keeps it.
1285
+ *
1286
+ * Give it as an interaction's `member`, with the server's `guildId` and no `guild`, to test a user-installed command
1287
+ * run there: the interaction reads `inRawGuild()` true, `guild` and `channel` null, `user` the member's user and
1288
+ * `memberPermissions` the member's, and a user option's member is the plain member Discord resolves too.
1289
+ *
1290
+ * @remarks
1291
+ * It is plain data, not a `GuildMember`: `roles` are role ids, `permissions` is the permission bitfield as a string,
1292
+ * and the rest are as Discord sends them, `null` for what may be absent and `false` for a flag. Code that reads
1293
+ * `member.roles.cache` throws on it, as it does in Discord.
1294
+ *
1295
+ * @param overrides - Fields for the member; see {@link MockRawMemberOverrides}.
1296
+ * @returns The member, as an `APIInteractionGuildMember`.
1297
+ *
1298
+ * @example
1299
+ * ```ts
1300
+ * import { expect } from 'vitest'
1301
+ *
1302
+ * const member = createMockRawMember({ roles: ['300000000000000001'], permissions: [PermissionFlagsBits.SendMessages] })
1303
+ * const command = createMockInteraction(ChatInputCommandInteraction, { guildId: '100000000000000001', member })
1304
+ * expect(command.inRawGuild()).toBe(true)
1305
+ * ```
1306
+ *
1307
+ * @group Testing
1308
+ * @category Mocks
1309
+ * @see {@link createMockMember}
1310
+ */
1311
+ declare function createMockRawMember(overrides?: MockRawMemberOverrides): APIInteractionGuildMember;
1174
1312
  /**
1175
1313
  * What {@link createMockMember} builds a member with.
1176
1314
  *
@@ -1365,6 +1503,60 @@ interface ChatInputOptions {
1365
1503
  */
1366
1504
  declare function createChatInputOptions<Cached extends CacheType = any>(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver<Cached>>;
1367
1505
 
1506
+ /**
1507
+ * Has every mock from `meocord/testing` compute the values discord.js computes, where it reads a placeholder otherwise.
1508
+ *
1509
+ * Call it once in the test runner's setup file. A message's `editable`, `deletable`, `pinnable`, `crosspostable`,
1510
+ * `bulkDeletable`, `hasThread` and `partial`, a member's `manageable`, `kickable`, `bannable` and `moderatable`, a
1511
+ * role's `editable`, a channel's and a thread's `viewable`, `manageable`, `deletable`, `joinable` and the rest, and
1512
+ * `partial` on users, channels and reactions then run discord.js's own getters against the mock, and `message.thread`
1513
+ * is `null` when the channel caches no thread under the message's id. No placeholder warning is logged.
1514
+ *
1515
+ * @remarks
1516
+ * Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in
1517
+ * the server's member cache from the start, @everyone has the permissions Discord gives it in a new server, and a
1518
+ * channel or thread made without a server has one of its own, a thread with a text channel as its parent. A message
1519
+ * has empty `content`, a thread is neither archived nor locked, and a reaction's `count` is 1. Give the bot's member
1520
+ * a role to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the
1521
+ * computed one. The next major version (5.0) computes these values without the call.
1522
+ *
1523
+ * @throws Error when a mock already exists, which was made without it.
1524
+ *
1525
+ * @example
1526
+ * ```ts
1527
+ * // vitest.setup.ts
1528
+ * useStrictMocks()
1529
+ * ```
1530
+ *
1531
+ * @group Testing
1532
+ * @category Mocks
1533
+ * @see {@link useMockFn}
1534
+ * @see {@link https://meocord.dev/docs/4.2/mocks | Mocks}
1535
+ */
1536
+ declare function useStrictMocks(): void;
1537
+
1538
+ /**
1539
+ * Has decorators keep the startup errors they find, as a bot built with `startupErrors: 'all'` does, so a testing
1540
+ * module reports every one at once.
1541
+ *
1542
+ * Call it once in the test runner's setup file, before any test imports a controller: a decorator runs when its class
1543
+ * is defined, as its file is imported. A decorator given what it cannot use then throws nothing as the file loads, and
1544
+ * `MeoCordTestingModule.create(...).compile()` logs every startup error of the classes it runs, each with its handler
1545
+ * and file, then throws the first. Without it, a decorator throws its error as its class is defined.
1546
+ *
1547
+ * @example
1548
+ * ```ts
1549
+ * // vitest.setup.ts
1550
+ * reportAllStartupErrors()
1551
+ * ```
1552
+ *
1553
+ * @group Testing
1554
+ * @category Module
1555
+ * @see {@link MeoCordTestingModule}
1556
+ * @see {@link https://meocord.dev/docs/4.2/configuration | Configuration}
1557
+ */
1558
+ declare function reportAllStartupErrors(): void;
1559
+
1368
1560
  /**
1369
1561
  * The command types routed by a `customId` pattern: buttons, select menus and modals.
1370
1562
  *
@@ -1423,9 +1615,16 @@ interface ResolvedRoute {
1423
1615
  * handler receives `params`.
1424
1616
  */
1425
1617
  values?: Record<string, string | number | boolean>;
1618
+ /**
1619
+ * For a component route, the other patterns of its type that match the `customId` too, in the order they rank
1620
+ * behind it: the ones that lost to it. Absent when no other pattern matches. A test reads it; `toEqual` doesn't
1621
+ * compare it, so an assertion on the other fields holds either way.
1622
+ */
1623
+ alsoMatches?: string[];
1426
1624
  }
1427
1625
  /**
1428
- * Two patterns of one component type that can both match a `customId`, as {@link findRouteConflicts} reports them.
1626
+ * Two patterns of one component type that rank equally and can both match a `customId`, as {@link findRouteConflicts}
1627
+ * reports them.
1429
1628
  *
1430
1629
  * @group Testing
1431
1630
  * @category Inspection
@@ -1435,6 +1634,11 @@ interface RouteConflict {
1435
1634
  type: ComponentCommandType;
1436
1635
  /** The two patterns. */
1437
1636
  patterns: [string, string];
1637
+ /**
1638
+ * The pattern that runs for the `customId`s both match: the one listed first. A test reads it; `toEqual` doesn't
1639
+ * compare it, so an assertion on `type` and `patterns` alone holds.
1640
+ */
1641
+ runs: string;
1438
1642
  }
1439
1643
  /**
1440
1644
  * Resolves which handler a component's `customId` or a message's content reaches, as dispatch routes it.
@@ -1471,33 +1675,36 @@ interface RouteConflict {
1471
1675
  * @group Testing
1472
1676
  * @category Inspection
1473
1677
  * @see {@link findRouteConflicts}
1474
- * @see {@link https://meocord.dev/docs/4.1/components | Buttons, selects and modals}
1678
+ * @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
1475
1679
  */
1476
1680
  declare function resolveRoute(app: ControllerClass, input: {
1477
1681
  type: ComponentCommandType;
1478
1682
  customId: string;
1479
1683
  } | MessageToResolve): ResolvedRoute | undefined;
1480
1684
  /**
1481
- * Finds component patterns that can match the same `customId`, which MeoCord otherwise only warns about at startup.
1685
+ * Finds component patterns that rank equally and can match the same `customId`, which MeoCord otherwise only warns
1686
+ * about at startup.
1482
1687
  *
1483
- * Use it in a test to keep the warning from reaching production: two patterns that trade a literal for a parameter
1484
- * in opposite places both take one id, and neither is more specific. Patterns are compared within a component type.
1688
+ * Use it in a test to keep the warning from reaching production. Patterns rank segment by segment, a literal before a
1689
+ * param and then the narrower type, so two collide only with the same literals and equally narrow params at each
1690
+ * position, such as `{action:close|reopen}` and `{step:close|confirm}`, which both take `close`; the one listed
1691
+ * first runs. Patterns are compared within a component type.
1485
1692
  *
1486
1693
  * @param app - The application class decorated with `@MeoCord`.
1487
- * @returns Each pair of patterns that can match the same `customId`, with its component type.
1694
+ * @returns Each pair of patterns that rank equally and can match the same `customId`, with its component type.
1488
1695
  *
1489
1696
  * @example
1490
1697
  * ```ts
1491
1698
  * import { expect } from 'vitest'
1492
1699
  *
1493
1700
  * @Controller()
1494
- * class CardController {
1495
- * @Command('card/{id}/open', CommandType.BUTTON)
1496
- * async open(interaction: ButtonInteraction) { await respond(interaction).send('Opened.') }
1497
- * @Command('card/new/{kind}', CommandType.BUTTON)
1498
- * async create(interaction: ButtonInteraction) { await respond(interaction).send('Created.') }
1701
+ * class TicketController {
1702
+ * @Command('ticket/{action:close|reopen}', CommandType.BUTTON)
1703
+ * async act(interaction: ButtonInteraction) { await respond(interaction).send('Done.') }
1704
+ * @Command('ticket/{step:close|confirm}', CommandType.BUTTON)
1705
+ * async step(interaction: ButtonInteraction) { await respond(interaction).send('Next.') }
1499
1706
  * }
1500
- * @MeoCord({ controllers: [CardController], clientOptions: { intents: [] } })
1707
+ * @MeoCord({ controllers: [TicketController], clientOptions: { intents: [] } })
1501
1708
  * class App {}
1502
1709
  * expect(findRouteConflicts(App)).toHaveLength(1)
1503
1710
  * ```
@@ -1584,7 +1791,7 @@ declare function createModalFields(values: Record<string, string | string[] | At
1584
1791
  * @group Testing
1585
1792
  * @category Inspection
1586
1793
  * @see {@link createTranslator}
1587
- * @see {@link https://meocord.dev/docs/4.1/localisation | Localisation}
1794
+ * @see {@link https://meocord.dev/docs/4.2/localisation | Localisation}
1588
1795
  */
1589
1796
  declare function expectCompleteCatalog(translator: Translator<any>, options?: {
1590
1797
  meocord?: boolean;
@@ -1622,7 +1829,8 @@ interface CooldownStoreSuiteFramework {
1622
1829
  * The cases cover what a shared store most often gets wrong: a sliding window rather than fixed buckets,
1623
1830
  * `retryAfterMs` from the oldest call still in the window, calls in the same millisecond kept apart, and concurrent
1624
1831
  * calls at the limit where exactly one may pass. They use real time, with windows short enough that the suite takes
1625
- * a few seconds, and each case counts under keys of its own. Each window is a whole number of milliseconds, as
1832
+ * a few seconds and margins wide enough that a correct store passes when timers fire late, as on a busy CI runner,
1833
+ * and each case counts under keys of its own. Each window is a whole number of milliseconds, as
1626
1834
  * `@Cooldown` gives a store: it rounds its `seconds` to the millisecond.
1627
1835
  *
1628
1836
  * @param name - What the store is called in the report.
@@ -1639,7 +1847,7 @@ interface CooldownStoreSuiteFramework {
1639
1847
  * @group Testing
1640
1848
  * @category Inspection
1641
1849
  * @see {@link CooldownStore}
1642
- * @see {@link https://meocord.dev/docs/4.1/recipes/cooldown-stores | Cooldown stores}
1850
+ * @see {@link https://meocord.dev/docs/4.2/recipes/cooldown-stores | Cooldown stores}
1643
1851
  */
1644
1852
  declare function testCooldownStore(name: string, factory: () => CooldownStore | Promise<CooldownStore>, { describe, it, expect }: CooldownStoreSuiteFramework): void;
1645
1853
 
@@ -1751,6 +1959,12 @@ interface HandlerInspection {
1751
1959
  readonly cooldowns: readonly InspectedCooldown[];
1752
1960
  /** The `@MessageHandler` pattern, or `undefined` for a listener and for any other kind of handler. */
1753
1961
  readonly pattern: string | undefined;
1962
+ /**
1963
+ * The routes the handler answers because a class its controller extends declares them, such as `slash "ping"`:
1964
+ * those its own class doesn't declare for it. Empty for a handler that declares all it answers. A test reads it;
1965
+ * `toEqual` doesn't compare it, so an assertion on the other fields holds either way.
1966
+ */
1967
+ readonly inheritedRoutes: readonly string[];
1754
1968
  /** The `app`'s observers, in the order they are told about the call; empty without an `app`. */
1755
1969
  readonly observers: readonly (new (...args: any[]) => DispatchObserver)[];
1756
1970
  /**
@@ -1847,7 +2061,7 @@ interface InspectHandlerOptions {
1847
2061
  * @group Testing
1848
2062
  * @category Inspection
1849
2063
  * @see {@link createExecutionContext}
1850
- * @see {@link https://meocord.dev/docs/4.1/testing-recipes | Testing recipes}
2064
+ * @see {@link https://meocord.dev/docs/4.2/testing-recipes | Testing recipes}
1851
2065
  */
1852
2066
  declare function inspectHandler<C extends new (...args: any[]) => unknown>(controller: C, methodName: keyof InstanceType<C> & string, options?: InspectHandlerOptions): HandlerInspection;
1853
2067
 
@@ -1925,7 +2139,7 @@ interface ResponseReport {
1925
2139
  * @group Testing
1926
2140
  * @category Inspection
1927
2141
  * @see {@link TestingModule}
1928
- * @see {@link https://meocord.dev/docs/4.1/invoke-and-dispatch | Invoke and dispatch}
2142
+ * @see {@link https://meocord.dev/docs/4.2/invoke-and-dispatch | Invoke and dispatch}
1929
2143
  */
1930
2144
  declare function getResponse(interaction: Interaction): ResponseReport;
1931
2145
  /**
@@ -1982,7 +2196,7 @@ type MockThemeArgs = Partial<RootTheme> extends RootTheme ? [overrides?: RootThe
1982
2196
  * @group Testing
1983
2197
  * @category Mocks
1984
2198
  * @see {@link withTheme}
1985
- * @see {@link https://meocord.dev/docs/4.1/theming | Theming}
2199
+ * @see {@link https://meocord.dev/docs/4.2/theming | Theming}
1986
2200
  */
1987
2201
  declare function createMockTheme(...[overrides]: MockThemeArgs): DeepReadonly<MeoCordTheme>;
1988
2202
  /**
@@ -2011,5 +2225,5 @@ declare function createMockTheme(...[overrides]: MockThemeArgs): DeepReadonly<Me
2011
2225
  */
2012
2226
  declare function withTheme<T>(theme: DeepReadonly<MeoCordTheme> | RootTheme, fn: () => T): T;
2013
2227
 
2014
- export { MeoCordTestingModule, Provider, ProviderToken, TestingModule, TestingModuleBuilder, clearAllMocks, createChatInputOptions, createDiscordError, createExecutionContext, createMock, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockTheme, createMockUser, createModalFields, expectCompleteCatalog, findRouteConflicts, getResponse, inspectHandler, isMockFunction, resetAllMocks, resolveRoute, testCooldownStore, withTheme };
2015
- export type { ChatInputOptions, ComponentCommandType, CooldownStoreSuiteFramework, DeepMocked, DispatchedCall, DispatchedHandler, EmitResult, ExecutionContextOptions, FromAppOptions, HandlerInspection, HandlerName, InspectHandlerOptions, InspectedCooldown, InspectedFilter, InspectedGuard, InspectedInterceptor, InvocationResult, MessageToResolve, Mock, MockGuildOverrides, MockInstance, MockMemberOverrides, MockMessageOverrides, MockProps, MockResult, MockState, MockedFunction, ResolvedRoute, ResponseReport, RouteConflict, TestingModuleInitOptions, TestingModuleOptions };
2228
+ export { MeoCordTestingModule, Provider, ProviderToken, TestingModule, TestingModuleBuilder, clearAllMocks, createChatInputOptions, createDiscordError, createExecutionContext, createMock, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockTheme, createMockUser, createModalFields, expectCompleteCatalog, findRouteConflicts, getResponse, inspectHandler, isMockFunction, reportAllStartupErrors, resetAllMocks, resolveRoute, testCooldownStore, useMockFn, useStrictMocks, withTheme };
2229
+ export type { ChatInputOptions, ComponentCommandType, CooldownStoreSuiteFramework, DeepMocked, DispatchedCall, DispatchedHandler, EmitResult, ExecutionContextOptions, FromAppOptions, HandlerInspection, HandlerName, InspectHandlerOptions, InspectedCooldown, InspectedFilter, InspectedGuard, InspectedInterceptor, InvocationResult, MessageToResolve, Mock, MockFnFactory, MockGuildOverrides, MockInstance, MockMemberOverrides, MockMessageOverrides, MockProps, MockRawMemberOverrides, MockResult, MockState, MockedFunction, ResolvedRoute, ResponseReport, RouteConflict, RunnerMock, TestingModuleInitOptions, TestingModuleOptions };
@@ -1,4 +1,4 @@
1
- import './index-D7n0FBMm.js';
1
+ import './index-CdXsFgjE.js';
2
2
 
3
3
  /**
4
4
  * An app's cache of the themes `@MeoCord({ themeFor })` looked up, by server and by user.
@@ -1,4 +1,4 @@
1
- import './index-DEP0NYrU.cjs';
1
+ import './index-nn5p7OoX.cjs';
2
2
 
3
3
  /**
4
4
  * An app's cache of the themes `@MeoCord({ themeFor })` looked up, by server and by user.
@@ -1,5 +1,5 @@
1
1
  import { Locale, Interaction, Guild } from 'discord.js';
2
- import { aq as MeoCordMessages } from './index-D7n0FBMm.js';
2
+ import { aq as MeoCordMessages } from './index-CdXsFgjE.js';
3
3
 
4
4
  /**
5
5
  * The plural categories `Intl.PluralRules` selects between.
@@ -1,5 +1,5 @@
1
1
  import { Locale, Interaction, Guild } from 'discord.js';
2
- import { aq as MeoCordMessages } from './index-DEP0NYrU.cjs';
2
+ import { aq as MeoCordMessages } from './index-nn5p7OoX.cjs';
3
3
 
4
4
  /**
5
5
  * The plural categories `Intl.PluralRules` selects between.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "meocord",
3
3
  "description": "Decorator-based framework for Discord bots on discord.js, with dependency injection, guards, a CLI and a testing toolkit.",
4
- "version": "4.1.2",
4
+ "version": "4.2.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.13"
@@ -24,11 +24,12 @@
24
24
  "bench:messages": "bun scripts/bench-messages.ts",
25
25
  "bench:theme": "bun scripts/bench-theme.ts",
26
26
  "changeset": "changeset",
27
- "release:version": "changeset version && bun scripts/release-notes.ts && prettier --write CHANGELOG.md",
27
+ "release:version": "changeset version && bun scripts/check-jsdoc-examples.ts --fix && bun scripts/release-notes.ts && prettier --write CHANGELOG.md",
28
28
  "notices": "bun scripts/third-party-notices.ts",
29
29
  "notices:check": "bun scripts/third-party-notices.ts --check",
30
30
  "check:jsdoc-examples": "bun scripts/check-jsdoc-examples.ts",
31
31
  "check:dist-types": "bun scripts/check-dist-types.ts",
32
+ "check:api-compat": "bun scripts/check-api-compat.ts",
32
33
  "check:dist-cjs": "bun scripts/check-dist-cjs.ts",
33
34
  "brand": "bun tools/brand/brand.ts"
34
35
  },