meocord 4.1.0-beta.0 → 4.1.0-beta.3

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 (44) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +106 -3
  3. package/dist/cjs/_shared/{cooldown-store-DTqJP31S.cjs → cooldown-store-DJ7s2HYv.cjs} +7 -4
  4. package/dist/cjs/_shared/{input-runner-CucAPIqF.cjs → providers-CHJHjNb9.cjs} +194 -18
  5. package/dist/cjs/_shared/{shard-context-84GuH11q.cjs → shard-context-_UhaAa3O.cjs} +54 -54
  6. package/dist/cjs/common/index.cjs +31 -1
  7. package/dist/cjs/core/index.cjs +153 -85
  8. package/dist/cjs/decorator/index.cjs +89 -60
  9. package/dist/cjs/testing/index.cjs +329 -71
  10. package/dist/esm/bin/app-template/README.md.template +2 -1
  11. package/dist/esm/bin/app-template/package.json.template +1 -0
  12. package/dist/esm/bin/app-template/tsconfig.eslint.json.template +1 -1
  13. package/dist/esm/bin/app-template/vitest.config.ts.template +2 -1
  14. package/dist/esm/bin/app-template/vitest.setup.ts.template +6 -0
  15. package/dist/esm/common/index.js +1 -0
  16. package/dist/esm/common/response/response-state.js +6 -3
  17. package/dist/esm/common/token.js +30 -0
  18. package/dist/esm/core/guard-runner.js +13 -1
  19. package/dist/esm/core/handler-pipeline.js +1 -1
  20. package/dist/esm/core/lifecycle-order.js +7 -16
  21. package/dist/esm/core/meocord-factory.js +69 -20
  22. package/dist/esm/core/meocord.app.js +47 -26
  23. package/dist/esm/core/providers.js +170 -0
  24. package/dist/esm/decorator/app.decorator.js +8 -0
  25. package/dist/esm/decorator/index.js +1 -0
  26. package/dist/esm/decorator/inject.decorator.js +24 -0
  27. package/dist/esm/package.json.js +1 -1
  28. package/dist/esm/testing/index.js +1 -1
  29. package/dist/esm/testing/meocord-testing-module.js +57 -39
  30. package/dist/esm/testing/mock-fn.js +38 -1
  31. package/dist/esm/testing/mock-interaction.js +226 -24
  32. package/dist/types/common/index.d.cts +31 -2
  33. package/dist/types/common/index.d.ts +31 -2
  34. package/dist/types/decorator/index.d.cts +27 -2
  35. package/dist/types/decorator/index.d.ts +27 -2
  36. package/dist/types/{errors-DwvuoFVM.d.ts → errors-C6xmj0MA.d.cts} +57 -3
  37. package/dist/types/{errors-DwvuoFVM.d.cts → errors-C6xmj0MA.d.ts} +57 -3
  38. package/dist/types/interface/index.d.cts +10 -8
  39. package/dist/types/interface/index.d.ts +10 -8
  40. package/dist/types/testing/index.d.cts +113 -27
  41. package/dist/types/testing/index.d.ts +113 -27
  42. package/meocord.eslint.cjs +22 -5
  43. package/meocord.eslint.mjs +22 -5
  44. package/package.json +7 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # meocord
2
2
 
3
+ ## 4.1.0-beta.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#133](https://github.com/meocord/meocord/pull/133) [`9153786`](https://github.com/meocord/meocord/commit/9153786e695a7d98c5d6b777e77cc07381e2c22e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Ships every change listed under 4.1.0-beta.2, which was not published on its own. Providers in `@MeoCord({ providers })` and in the testing module's `providers` can be listed in any order: a class listed there is bound once, even when a provider earlier in the list injects it.
8
+
9
+ ## 4.1.0-beta.2
10
+
11
+ Not published to npm: these changes first ship in 4.1.0-beta.3.
12
+
13
+ ### Minor Changes
14
+
15
+ - [#123](https://github.com/meocord/meocord/pull/123) [`3924763`](https://github.com/meocord/meocord/commit/3924763805de2c1b6c44ca2d94e72e823956b67d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `@MeoCord({ providers })` provides values, classes and sync or async factories under a class, string, symbol or `createToken` token, injected with the new `@Inject(token)`, with their `onReady` and `onShutdown` hooks run in dependency order; `MeoCordTestingModule` takes the same providers.
16
+
17
+ - [#132](https://github.com/meocord/meocord/pull/132) [`9dcfec5`](https://github.com/meocord/meocord/commit/9dcfec5d37b7a2f0e13a8fdf9193be80f39e994f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing` exports `clearAllMocks()` and `resetAllMocks()`, which reach every mock it made: `createMockFn`, and the mocks inside `createMockInteraction`, `createMockClient` and the rest. Vitest's `clearMocks` and jest's `clearAllMocks()` only reach their own `vi.fn()` and `jest.fn()`. New projects call `resetAllMocks()` after every test from `vitest.setup.ts`. To do the same in an existing project, add `afterEach(() => resetAllMocks())` to a Vitest setup file, and set what a mock returns in the test, or in `beforeEach`, that relies on it.
18
+
19
+ ### Patch Changes
20
+
21
+ - [#125](https://github.com/meocord/meocord/pull/125) [`c8584a6`](https://github.com/meocord/meocord/commit/c8584a63e9b9d0a4db629338c137bf26a96fa765) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `createMockChannel` takes `ThreadChannel`, stubs `threads.create` on text, announcement, forum and media channels, and gives a subclass the managers of the channel class it extends.
22
+
23
+ - [#130](https://github.com/meocord/meocord/pull/130) [`1b81e6d`](https://github.com/meocord/meocord/commit/1b81e6d4035e41a02d5b2cabaed503927c8e09af) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Mocks from `meocord/testing` behave more like discord.js. An interaction mock has a `locale` of `'en-US'`, and a `guildLocale` of `'en-US'` with a `guildId` and `null` without, so `t.for(interaction, { public: true })` works on a default mock. A method that returns a promise in discord.js now resolves instead of returning `undefined`, so `await` and `.catch()` work without setup. `send()` and `reply()` resolve to a mock message. A manager's `fetch(id)`, `create()` and `edit()` resolve to a mock of its item, and a list fetch to an empty `Collection`. `createDM()` resolves to a DM channel, and a structure's own `edit()`, `fetch()` and setters to the structure itself. `mockResolvedValue` and `mockRejectedValue` still override them.
24
+
25
+ - [#126](https://github.com/meocord/meocord/pull/126) [`8cee75d`](https://github.com/meocord/meocord/commit/8cee75d56a250cc86ced77325cdffa20557431eb) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `respond()` reads discord.js's deprecated `ephemeral: true` as `flags: MessageFlags.Ephemeral`, so a private follow-up on a public deferred reply is no longer shown to everyone as an edit of that reply, and tests see the privacy the bot sends; `flags: MessageFlags.Ephemeral` is the supported form.
26
+
27
+ - [#131](https://github.com/meocord/meocord/pull/131) [`b5e7e63`](https://github.com/meocord/meocord/commit/b5e7e6358bdb325fac4a21b0415922a50ec1727a) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A controller or service whose constructor parameter has no runtime type now stops `MeoCordFactory.create`, and the testing module, before anything is bound. The error names the class, the parameter and the classes that inject it, and says how to fix it. This happens when two services import each other, or a parameter is typed with an interface or an `import type`; the error inversify raised before pointed at the compiler options instead. `meocord/eslint` now warns on import cycles (`import-x/no-cycle`, type-only imports ignored) in projects that have `eslint-import-resolver-typescript`, which it needs to follow imports through `@src`. New projects include it; in a project without it the check stays off and lint is unchanged. Add it with `npm i -D eslint-import-resolver-typescript` to get the warning.
28
+
29
+ ## 4.1.0-beta.1
30
+
31
+ ### Minor Changes
32
+
33
+ - [#120](https://github.com/meocord/meocord/pull/120) [`9d435d9`](https://github.com/meocord/meocord/commit/9d435d9da2bc62eab012ea2b80df2c1e001a926e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `createMockMessage()` takes an `id`, `content`, `components`, `embeds` and `flags`, so a test can put controls on the message a button sits on, such as to check what `@Defer` locks; components and embeds may be API JSON, builders or discord.js instances.
34
+
35
+ ### Patch Changes
36
+
37
+ - [#118](https://github.com/meocord/meocord/pull/118) [`5c636aa`](https://github.com/meocord/meocord/commit/5c636aa930224aef9b4670a5e16b234a3f57397d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Tools that read the installed version with `require('meocord/package.json')` no longer fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
38
+
39
+ - [#121](https://github.com/meocord/meocord/pull/121) [`c6990c2`](https://github.com/meocord/meocord/commit/c6990c250f7694ca5adb9963e89f151b7ed5dc4b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `inGuild()`, `inCachedGuild()` and `inRawGuild()` on a mock answer from its `guildId` and `guild`, so a mock created without a `guildId` is a DM and a guard that requires a guild returns `false`, not `undefined`.
40
+
3
41
  ## 4.1.0-beta.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # MeoCord Framework
2
2
 
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/meocord/meocord/main/docs/assets/brand/banner-dark.webp">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/meocord/meocord/main/docs/assets/brand/banner-light.webp">
6
+ <img alt="MeoCord: a controller method with @Command, @UseGuard and @Cooldown, answering with respond()" src="https://raw.githubusercontent.com/meocord/meocord/main/docs/assets/brand/banner.webp" width="1280">
7
+ </picture>
8
+
3
9
  [![npm version](https://img.shields.io/npm/v/meocord.svg)](https://www.npmjs.com/package/meocord)
4
10
  [![CI](https://github.com/meocord/meocord/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/meocord/meocord/actions/workflows/release.yml)
5
11
  [![node](https://img.shields.io/node/v/meocord)](https://www.npmjs.com/package/meocord)
@@ -45,6 +51,7 @@
45
51
  - [Custom Decorators](#custom-decorators)
46
52
  - [Gateway Events](#gateway-events)
47
53
  - [Handler Discovery](#handler-discovery)
54
+ - [Providers](#providers)
48
55
  - [Lifecycle Hooks](#lifecycle-hooks)
49
56
  - [Localisation](#localisation)
50
57
  - [Testing](#testing)
@@ -64,7 +71,7 @@
64
71
  ## Features
65
72
 
66
73
  - **Decorator-based controllers** — Handle every Discord interaction type — slash commands and their subcommands, autocomplete, buttons, modals, all five select menus, context menus, activity entry points, messages, and reactions — with `@Command`, `@Autocomplete`, `@MessageHandler` and `@ReactionHandler`. No routing boilerplate.
67
- - **Dependency injection** — Built on Inversify. Services are wired into controllers automatically; no manual instantiation or service locators.
74
+ - **Dependency injection** — Built on Inversify. Services are wired into controllers automatically; no manual instantiation or service locators. Providers add values, classes and async factories under any token, injected with `@Inject`.
68
75
  - **A request pipeline** — [Guards](#guards) decide whether a handler runs, [interceptors](#interceptors) wrap it, [validation and pipes](#validation-and-pipes) check and transform its input, [cooldowns](#cooldowns) limit how often it runs, and [exception filters](#exception-filters) decide what the user is told when something throws. Each applies to a method, a controller, or the whole bot.
69
76
  - **Interaction responses** — `respond(interaction)` answers every interaction type correctly from any state, deferred or replied, wherever a user-installed app is used; a presenter styles MeoCord's own answers.
70
77
  - **Cooldowns** — `@Cooldown` limits how often a handler runs, per user, server, channel or for everyone, with a pluggable store to share the count across shards.
@@ -755,7 +762,7 @@ async profile(interaction: ChatInputCommandInteraction) {
755
762
 
756
763
  `state` tells where the answer stands (`'unanswered'`, `'deferred'` or `'replied'`), re-read from the interaction on every call, so answers made directly with discord.js or by a collector still count. `message` is the message last sent or edited, `location` is what [`getInstallContext`](#where-the-interaction-happened) reports, and `original` holds the message's components and embeds from before `@Defer` locked it. `lock()` is `@Defer`'s second step, for a handler that acknowledges on its own. Interceptors and filters reach the same state as `context.response`.
757
764
 
758
- Each call takes only the flags Discord accepts for it, computed afresh: an ephemeral follow-up never makes later messages ephemeral. `send()` and `followUp()` payloads are typed so an impossible flag does not compile. Once a message uses Components V2 its edits keep the flag, and content and embeds are dropped from them. When an edit re-sends an embed or Components V2 media whose image is one of the message's own Discord attachments, the URL is pointed at `attachment://` so the image survives the edit.
765
+ Each call takes only the flags Discord accepts for it, computed afresh: an ephemeral follow-up never makes later messages ephemeral. `send()` and `followUp()` payloads are typed so an impossible flag does not compile. Make a message private with `flags: MessageFlags.Ephemeral`; discord.js's deprecated `ephemeral: true` is read as that flag, under the same rules. Once a message uses Components V2 its edits keep the flag, and content and embeds are dropped from them. When an edit re-sends an embed or Components V2 media whose image is one of the message's own Discord attachments, the URL is pointed at `attachment://` so the image survives the edit.
759
766
 
760
767
  ### `@Defer`
761
768
 
@@ -1323,6 +1330,62 @@ export class HelpService {
1323
1330
 
1324
1331
  ---
1325
1332
 
1333
+ ## Providers
1334
+
1335
+ A class that a controller or service injects by its type is bound for you. For anything else — a database pool, a configured client, settings, or the implementation behind an abstract class — list a provider in `@MeoCord({ providers })`, and inject it with `@Inject(token)`:
1336
+
1337
+ ```typescript
1338
+ import { createToken } from 'meocord/common'
1339
+ import { Inject, MeoCord, Service } from 'meocord/decorator'
1340
+ import { GatewayIntentBits } from 'discord.js'
1341
+ import pg from 'pg'
1342
+
1343
+ export const DATABASE = createToken<pg.Pool>('Database')
1344
+
1345
+ @Service()
1346
+ export class NotesStore {
1347
+ constructor(@Inject(DATABASE) private readonly db: pg.Pool) {}
1348
+
1349
+ async list(userId: string) {
1350
+ const { rows } = await this.db.query('SELECT text FROM notes WHERE user_id = $1', [userId])
1351
+ return rows
1352
+ }
1353
+ }
1354
+
1355
+ @MeoCord({
1356
+ controllers: [NotesController],
1357
+ providers: [
1358
+ { provide: 'databaseUrl', useValue: process.env.DATABASE_URL },
1359
+ {
1360
+ provide: DATABASE,
1361
+ useFactory: async (url: string) => {
1362
+ const pool = new pg.Pool({ connectionString: url })
1363
+ await pool.query('SELECT 1') // fail at startup, not at the first command
1364
+ return pool
1365
+ },
1366
+ inject: ['databaseUrl'],
1367
+ },
1368
+ ],
1369
+ clientOptions: { intents: [GatewayIntentBits.Guilds] },
1370
+ })
1371
+ export default class App {}
1372
+ ```
1373
+
1374
+ | Provider | Provides |
1375
+ | ---------------------------------- | -------------------------------------------------------------------------------------- |
1376
+ | `{ provide, useValue }` | The value, as it is. |
1377
+ | `{ provide, useClass }` | One instance of the class, with its own constructor dependencies injected. |
1378
+ | `{ provide, useFactory, inject? }` | What the function returns, called once with the values of `inject`. It may be `async`. |
1379
+
1380
+ - **Tokens** are a class, a string, a symbol, or a token from `createToken<T>(description)`: a symbol whose description names it in errors, and whose type `TestingModule.get` returns. A parameter typed as a class needs no `@Inject`; `{ provide: Storage, useClass: RedisStorage }` makes every `Storage` parameter a `RedisStorage`.
1381
+ - **When**: every factory runs once, in dependency order, when `app.start()` begins, before the listed services are made and before login, awaiting those that return a promise. Anything that injects the value gets the resolved one.
1382
+ - **Lifecycle**: a provided value that implements `onReady` or `onShutdown` gets them called like a service's, in dependency order, so it closes after the services that use it. `pg.Pool` has no such hook; end it from the `onShutdown` of the service that owns it, or provide a class that wraps it.
1383
+ - **Failures**: a factory that throws or rejects stops the bot before it logs in. The log names the token and the cause, `app.start()` rejects with an error `isExplainedError` recognises, and the exit code is 1. A class or factory that injects a token nothing provides stops `MeoCordFactory.create()`, naming both. A token may be provided once, and the tokens MeoCord binds itself, such as `Client`, cannot be provided.
1384
+
1385
+ In a test, `MeoCordTestingModule` takes the same providers, and `overrideProvider(token)` replaces one under any token. `invoke` and `emit` resolve asynchronous factories first; before calling `get` on something that depends on one, `await module.init()`.
1386
+
1387
+ ---
1388
+
1326
1389
  ## Lifecycle Hooks
1327
1390
 
1328
1391
  A controller or service can do work once the bot is online, and clean up before it stops, by implementing `OnReady` and `OnShutdown` from `meocord/interface`:
@@ -1354,7 +1417,7 @@ export class ReminderScheduler implements OnReady, OnShutdown {
1354
1417
  }
1355
1418
  ```
1356
1419
 
1357
- - **Which classes**: every controller and every service the app binds — the ones listed in `@MeoCord({ controllers, services })` and everything they depend on — including a service no handler has used yet. Guards are created per call and get no hooks.
1420
+ - **Which classes**: every controller and every service the app binds — the ones listed in `@MeoCord({ controllers, services })` and everything they depend on — including a service no handler has used yet, and every [provided](#providers) value that implements a hook. Guards are created per call and get no hooks.
1358
1421
  - **`onReady`** runs once the client is ready. It receives the client and `{ primary }`, which says whether this process should do one-off work: `true` for a bot running in one process, and with [process sharding](#sharding) only in the process running shard 0.
1359
1422
  - **Dependency order.** `onReady` hooks run one at a time, each class after the classes it injects: a `DatabaseService` is ready before the `ReminderScheduler` that injects it. Classes with no dependency between them run in declaration order, the `services` first, then the `controllers`. Command registration runs alongside and never delays the hooks. A hook still running after 10 seconds is named in a warning, and the hooks after it wait for it.
1360
1423
  - **`onShutdown`** runs on SIGINT or SIGTERM, before the client is destroyed, in reverse order, so a class stops before the classes it uses. The bot waits for the whole sequence up to `shutdownTimeout` from `meocord.config.ts` (10 seconds by default), then shuts down whether or not it finished. A second signal more than a second after the first exits at once; one sooner is taken as the same request, since a terminal's Ctrl+C can arrive twice. If the bot never became ready, for example because the login failed, no `onShutdown` hook runs. A signal that arrives while the `onReady` hooks are still running shuts down only the classes whose `onReady` finished, and those without one; the class still starting, and those after it, are skipped, and no further `onReady` starts.
@@ -1470,6 +1533,8 @@ const module = MeoCordTestingModule.create({
1470
1533
  const controller = module.get(GreetingSlashController)
1471
1534
  ```
1472
1535
 
1536
+ `providers` takes the same shapes as `@MeoCord({ providers })`: `useValue`, `useClass` and `useFactory`, under a class, string, symbol or `createToken` token. A factory that returns a promise is resolved by `await module.init()`, which `invoke` and `emit` call for you.
1537
+
1473
1538
  ### Running a handler with `invoke`
1474
1539
 
1475
1540
  `module.invoke(Controller, 'method', ...args)` runs a handler through the [pipeline](#how-a-handler-runs) dispatch runs: `@Defer`, its guards, class guards first and each once, then its interceptors around validation, pipes, cooldowns and the handler, all inside its exception filters. Guards resolve from the module, so `overrideGuard` stubs and injected `ExecutionContext` work as they do in the bot. Pass the arguments dispatch would: the interaction, message or reaction, then the handler's params. An interaction must be one dispatch could route to the handler: a customId its pattern matches, or the command or subcommand path it handles. One that could not, such as `'something/else'` for `'profile/{id}'`, rejects with a message naming both, so a typo in a test does not pass silently. A mock built without a customId or command name is not checked.
@@ -1524,6 +1589,8 @@ Creates a smart mock instance of any discord.js class. The full prototype chain
1524
1589
 
1525
1590
  **Type guards run real logic** — `isButton()`, `isRepliable()`, `isChatInputCommand()`, etc. are backed by the actual discord.js prototype methods. The right fields (`type`, `componentType`, `commandType`) are set based on the class you pass in, so no manual `.mockReturnValue(true)` setup is needed. All type guard methods are still mock functions and can be overridden per test.
1526
1591
 
1592
+ **Guild checks read the mock's data** — `inGuild()`, `inCachedGuild()` and `inRawGuild()` answer from the `guildId` and `guild` the mock is given, as discord.js does: a `guildId` means a guild, a `guildId` with a `guild` (such as `createMockGuild()`) a cached one, and a mock created without a `guildId` is a DM, where all three return `false`. A `member` set to `null` or `undefined` makes all three `false` too. Its `locale` is `'en-US'`, and its `guildLocale` is `'en-US'` with a `guildId` and `null` without, as Discord sends them; pass either to change it.
1593
+
1527
1594
  **Reply state machine** — for repliable interactions, `replied` and `deferred` start as `false`. Calling `reply()` or `deferReply()` twice throws, just like a real interaction. `followUp()`, `editReply()`, and `deleteReply()` throw if called before any reply. The ephemeral flag is tracked on `interaction.ephemeral`, read from `flags` only — the deprecated `ephemeral: true` reply option is not honoured. All reply methods are still mock functions so call assertions work normally.
1528
1595
 
1529
1596
  Autocomplete interactions are not repliable but get the equivalent for their own single-shot response: `responded` starts as `false`, `respond()` sets it, and a second call throws.
@@ -1531,6 +1598,8 @@ Autocomplete interactions are not repliable but get the equivalent for their own
1531
1598
  Guards discord.js has deprecated are deliberately left unwired — `isSelectMenu()` returns `undefined` rather than reproducing behaviour the library is removing. Use `isStringSelectMenu()`.
1532
1599
 
1533
1600
  > **Framework-agnostic** — the mocks returned here are plain mock functions that stamp `_isMockFunction` and expose `.mock.calls`, the exact contract both `jest` and `vitest` check. Use them with either framework's `expect(...).toHaveBeenCalledWith(...)` / `toHaveBeenCalledTimes(...)` — no jest or vitest import is required to produce them. For typed stubs in your own code, import `MockedFunction`, `createMockFn`, and `DeepMocked` from `meocord/testing`.
1601
+ >
1602
+ > **Resetting between tests** — Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()`. `clearAllMocks()` from `meocord/testing` clears the calls of every mock it made, and `resetAllMocks()` also undoes what a test set with `mockReturnValue`, `mockResolvedValue` and the rest, back to how each mock was created. Generated projects call `resetAllMocks()` after every test from `vitest.setup.ts`, so set a mock's behaviour in the test, or in `beforeEach`, that relies on it.
1534
1603
 
1535
1604
  ```typescript
1536
1605
  import { createMockInteraction } from 'meocord/testing'
@@ -1547,6 +1616,10 @@ interaction.isChatInputCommand() // → true
1547
1616
  interaction.isRepliable() // → true
1548
1617
  interaction.isButton() // → false
1549
1618
 
1619
+ // created without a guildId, the mock is a DM
1620
+ interaction.inGuild() // → false
1621
+ interaction.inCachedGuild() // → false
1622
+
1550
1623
  // reply state machine
1551
1624
  interaction.replied // → false
1552
1625
  await interaction.reply({ content: 'hi' })
@@ -1650,6 +1723,8 @@ All methods are mock functions — override any per test with `.mockReturnValue(
1650
1723
 
1651
1724
  Convenience wrappers for common discord.js classes. All methods are auto-stubbed as mock functions. Nested managers (`client.users`, `guild.members`, etc.) are independent nested stubs.
1652
1725
 
1726
+ A method that returns a promise in discord.js resolves, so `await` and `.catch()` work without setup: `send()` and `reply()` to a mock `Message`, a manager's `fetch(id)`, `create()` and `edit()` to a mock of its item (`users.fetch(id)` to a `User`, `guild.members.fetch(id)` to a `GuildMember`), a list fetch such as `members.fetch()` to an empty `Collection`, `createDM()` to a `DMChannel`, and a structure's own `edit()`, `fetch()` and setters to the structure itself. Any other such method resolves to `undefined`. `mockResolvedValue` and `mockRejectedValue` still decide per test.
1727
+
1653
1728
  ```typescript
1654
1729
  import { createMockFn, createMockUser, createMockClient, createMockGuild, createMockChannel } from 'meocord/testing'
1655
1730
  import { TextChannel } from 'discord.js'
@@ -1665,6 +1740,17 @@ await (client.users as any).fetch('user-123')
1665
1740
  expect((client.users as any).fetch).toHaveBeenCalledWith('user-123')
1666
1741
  ```
1667
1742
 
1743
+ `createMockChannel` sets up the managers each channel class has: `messages`; `threads` on text, announcement, forum and media channels; and `members` on a `ThreadChannel`. A subclass gets those of the class it extends.
1744
+
1745
+ ```typescript
1746
+ import { TextChannel, ThreadChannel } from 'discord.js'
1747
+
1748
+ const text = createMockChannel(TextChannel)
1749
+ const thread = createMockChannel(ThreadChannel)
1750
+ // discord.js types a created thread as public or private, not as the ThreadChannel class
1751
+ text.threads.create.mockResolvedValue(thread as never)
1752
+ ```
1753
+
1668
1754
  </details>
1669
1755
 
1670
1756
  <details>
@@ -1691,6 +1777,23 @@ edited.delete // → a mock fn
1691
1777
  expect(msg.delete).toHaveBeenCalledTimes(1)
1692
1778
  ```
1693
1779
 
1780
+ Without arguments the message is empty. Give it an `id`, `content`, `components`, `embeds` and `flags` to test code that reads them, such as a button on a message whose controls `@Defer` locks. Components and embeds may be API JSON, builders or discord.js instances, and `flags` a number, flag names or a `MessageFlagsBitField`:
1781
+
1782
+ ```typescript
1783
+ const message = createMockMessage({
1784
+ components: [
1785
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
1786
+ new ButtonBuilder().setCustomId('card/refresh').setLabel('Refresh').setStyle(ButtonStyle.Primary),
1787
+ ),
1788
+ ],
1789
+ embeds: [{ title: 'Card' }],
1790
+ flags: MessageFlags.Ephemeral,
1791
+ })
1792
+ const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/refresh', message })
1793
+ ```
1794
+
1795
+ API JSON and builders are kept as their JSON behind `toJSON()`, which is what `respond()` and `@Defer` read; discord.js instances are kept as they are.
1796
+
1694
1797
  </details>
1695
1798
 
1696
1799
  <details>
@@ -33,7 +33,7 @@ let configLoaded = false;
33
33
  try {
34
34
  // Called through a variable so a bundler does not try to resolve the path while building the
35
35
  // application; the file is only there once the build is done.
36
- const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/cooldown-store-DTqJP31S.cjs', document.baseURI).href)));
36
+ const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/cooldown-store-DJ7s2HYv.cjs', document.baseURI).href)));
37
37
  const loaded = load(compiledPath);
38
38
  return loaded.default ?? loaded;
39
39
  } catch (error) {
@@ -485,11 +485,14 @@ function errorCode(error) {
485
485
  return interaction.isCommand() || interaction.isModalSubmit() && !interaction.isFromMessage();
486
486
  }
487
487
  function toBody(payload) {
488
- return typeof payload === 'string' ? {
488
+ if (typeof payload === 'string') return {
489
489
  content: payload
490
- } : {
491
- ...payload
492
490
  };
491
+ // discord.js's deprecated option, read as the flag it stands for before anything decides on flags, and
492
+ // kept out of the call, where discord.js would add Ephemeral back to an edit or an update
493
+ const { ephemeral, ...body } = payload;
494
+ if (ephemeral) body.flags = Number(discord_js.MessageFlagsBitField.resolve(body.flags ?? 0)) | discord_js.MessageFlags.Ephemeral;
495
+ return body;
493
496
  }
494
497
  /** Drops what a Components V2 message cannot carry. */ function forMode(body, v2) {
495
498
  if (!v2) return body;
@@ -4,7 +4,7 @@ require('reflect-metadata');
4
4
  var enum_index = require('../enum/index.cjs');
5
5
  var metadataKey_enum = require('./metadata-key.enum-CXFdtEX9.cjs');
6
6
  var discord_js = require('discord.js');
7
- var cooldownStore = require('./cooldown-store-DTqJP31S.cjs');
7
+ var cooldownStore = require('./cooldown-store-DJ7s2HYv.cjs');
8
8
  var inversify = require('inversify');
9
9
 
10
10
  /**
@@ -588,6 +588,18 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
588
588
  }
589
589
  return tokens;
590
590
  }
591
+ /**
592
+ * The position of the first constructor parameter with no runtime type and no `@Inject` token, or -1.
593
+ * Such a parameter reads as `Object` when it is typed with an interface or an `import type`, and as
594
+ * `Object` or `undefined` when its class's module had not finished loading, as when two classes
595
+ * import each other.
596
+ */ function untypedParameter(cls) {
597
+ const types = Reflect.getMetadata(metadataKey_enum.MetadataKey.ParamTypes, cls) ?? [];
598
+ const metadata = Reflect.getMetadata(INVERSIFY_CLASS_METADATA, cls);
599
+ // Once bound, inversify records each parameter here too: an @Inject token, or the parameter's type
600
+ const tokens = types.map((type, index)=>metadata?.constructorArguments?.[index]?.value ?? type);
601
+ return tokens.findIndex((token)=>token === undefined || token === Object);
602
+ }
591
603
  /**
592
604
  * The error for a class resolved once and shared that asks for the per-call `ExecutionContext`,
593
605
  * which would keep the first call's context for every later one.
@@ -834,21 +846,12 @@ function eventDecorator(event, once) {
834
846
  */ function lifecycleDependencies(container, cls) {
835
847
  return injectedTokens(cls).filter((token)=>isAppClassToken(token) && container.isBound(token));
836
848
  }
837
- /**
838
- * The bound classes reachable from `roots`, each after everything it injects: the order lifecycle
839
- * hooks run in. Roots are the listed services, then the controllers, so classes with no dependency
840
- * between them keep that declaration order.
841
- */ function dependencyOrder(container, roots) {
842
- const ordered = [];
843
- const seen = new Set();
844
- const visit = (cls)=>{
845
- if (seen.has(cls)) return;
846
- seen.add(cls);
847
- lifecycleDependencies(container, cls).forEach(visit);
848
- ordered.push(cls);
849
- };
850
- roots.forEach(visit);
851
- return ordered;
849
+ /** The units for a list of classes already in dependency order, as the app runs them without providers. */ function classUnits(container, classes) {
850
+ return classes.map((cls)=>({
851
+ token: cls,
852
+ name: cls.name,
853
+ dependencies: lifecycleDependencies(container, cls)
854
+ }));
852
855
  }
853
856
 
854
857
  function isInterceptorWithParams(entry) {
@@ -1124,6 +1127,170 @@ const asList = (entries)=>Array.isArray(entries) ? entries : [
1124
1127
  ];
1125
1128
  }
1126
1129
 
1130
+ const isValueProvider = (provider)=>'useValue' in provider;
1131
+ const isClassProvider = (provider)=>'useClass' in provider;
1132
+ const isFactoryProvider = (provider)=>'useFactory' in provider;
1133
+ /** A token as logs and errors name it: a class by its name, a symbol by its description, a string quoted. */ function tokenName(token) {
1134
+ if (typeof token === 'function') return token.name || 'an anonymous class';
1135
+ if (typeof token === 'symbol') return token.description ? `Symbol(${token.description})` : 'a symbol';
1136
+ return `'${String(token)}'`;
1137
+ }
1138
+ const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
1139
+ /**
1140
+ * Checks each provider's shape and indexes them by token, throwing on the first that cannot be bound:
1141
+ * no token, not exactly one of `useValue`, `useClass` and `useFactory`, or a token provided twice.
1142
+ */ function providerMap(providers, where) {
1143
+ const map = new Map();
1144
+ for (const provider of providers){
1145
+ const entry = provider;
1146
+ if (!entry || typeof entry !== 'object' || !isToken(entry.provide)) {
1147
+ throw new Error(`${where} has a provider without a token: set provide to a class, a string or a symbol.`);
1148
+ }
1149
+ const name = tokenName(entry.provide);
1150
+ const kinds = [
1151
+ 'useValue',
1152
+ 'useClass',
1153
+ 'useFactory'
1154
+ ].filter((kind)=>kind in entry);
1155
+ if (kinds.length !== 1) {
1156
+ throw new Error(`The provider for ${name} in ${where} needs exactly one of useValue, useClass and useFactory.`);
1157
+ }
1158
+ if ('useClass' in entry && typeof entry.useClass !== 'function') {
1159
+ throw new Error(`The provider for ${name} in ${where} has a useClass that is not a class.`);
1160
+ }
1161
+ if ('useFactory' in entry) {
1162
+ if (typeof entry.useFactory !== 'function') {
1163
+ throw new Error(`The provider for ${name} in ${where} has a useFactory that is not a function.`);
1164
+ }
1165
+ if (entry.inject !== undefined && !(Array.isArray(entry.inject) && entry.inject.every(isToken))) {
1166
+ throw new Error(`The provider for ${name} in ${where} has an inject that is not a list of tokens.`);
1167
+ }
1168
+ }
1169
+ if (map.has(entry.provide)) throw new Error(`${name} is provided twice in ${where}.`);
1170
+ map.set(entry.provide, provider);
1171
+ }
1172
+ return map;
1173
+ }
1174
+ /**
1175
+ * Binds one provider into `container` as a singleton. `bindClass` binds a class a provider depends
1176
+ * on, as the app binds its own, so a provided class or a factory's `inject` can name any class.
1177
+ */ function bindProvider(container, provider, bindClass) {
1178
+ const token = provider.provide;
1179
+ if (isValueProvider(provider)) {
1180
+ container.bind(token).toConstantValue(provider.useValue);
1181
+ return;
1182
+ }
1183
+ if (isClassProvider(provider)) {
1184
+ const cls = provider.useClass;
1185
+ if (injectedTokens(cls).includes(cooldownStore.ExecutionContext)) throw singletonContextError(cls);
1186
+ makeInjectable(cls);
1187
+ container.bind(token).to(cls).inSingletonScope();
1188
+ for (const dependency of injectedTokens(cls))if (isAppClassToken(dependency)) bindClass(dependency);
1189
+ return;
1190
+ }
1191
+ const { useFactory, inject = [] } = provider;
1192
+ // Resolved in dependency order before anything needs it, so each injected value is already made
1193
+ container.bind(token).toDynamicValue((context)=>useFactory(...inject.map((dependency)=>context.get(dependency)))).inSingletonScope();
1194
+ for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
1195
+ }
1196
+ /**
1197
+ * Throws when a class or a factory asks for a string or symbol token that nothing binds, naming both,
1198
+ * rather than failing when the class is first resolved. A class token is bound on demand, so it is
1199
+ * never missing.
1200
+ */ function assertProvided(container, providers, classes, where) {
1201
+ const missing = (token)=>(typeof token === 'string' || typeof token === 'symbol') && !container.isBound(token);
1202
+ for (const cls of classes){
1203
+ const token = injectedTokens(cls).find(missing);
1204
+ if (token !== undefined) {
1205
+ throw new Error(`${cls.name} injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`);
1206
+ }
1207
+ }
1208
+ for (const [provided, provider] of providers){
1209
+ const dependencies = isFactoryProvider(provider) ? provider.inject ?? [] : isClassProvider(provider) ? injectedTokens(provider.useClass) : [];
1210
+ const token = dependencies.find(missing);
1211
+ if (token !== undefined) {
1212
+ throw new Error(`The provider for ${tokenName(provided)} injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`);
1213
+ }
1214
+ }
1215
+ }
1216
+ /**
1217
+ * The classes the app constructs, found from `roots` through what each injects, before anything is
1218
+ * bound: a token a provider stands in for is not constructed as itself, so it is only followed when
1219
+ * it is its own `useClass`. Providers' classes and factory dependencies are roots too.
1220
+ */ function reachableClasses(roots, providers) {
1221
+ const found = [];
1222
+ const visit = (token)=>{
1223
+ if (!isAppClassToken(token) || found.includes(token)) return;
1224
+ const provider = providers.get(token);
1225
+ if (provider && !(isClassProvider(provider) && provider.useClass === token)) return;
1226
+ found.push(token);
1227
+ injectedTokens(token).forEach(visit);
1228
+ };
1229
+ for (const provider of providers.values()){
1230
+ if (isClassProvider(provider)) visit(provider.useClass);
1231
+ if (isFactoryProvider(provider)) (provider.inject ?? []).forEach(visit);
1232
+ }
1233
+ roots.forEach(visit);
1234
+ return found;
1235
+ }
1236
+ /**
1237
+ * Throws for the first of `classes` with a constructor parameter nothing can inject: one with no
1238
+ * runtime type and no `@Inject` token. It names the classes among `classes` that inject it, the likely
1239
+ * other half when two classes import each other, rather than leaving inversify's error to point at
1240
+ * the compiler options.
1241
+ */ function assertTypedParameters(classes) {
1242
+ for (const cls of classes){
1243
+ const index = untypedParameter(cls);
1244
+ if (index === -1) continue;
1245
+ const injectors = classes.filter((other)=>other !== cls && injectedTokens(other).includes(cls)).map((other)=>other.name);
1246
+ const injectedBy = injectors.length === 0 ? '' : ` (${injectors.length === 1 ? injectors[0] : `${injectors.slice(0, -1).join(', ')} and ${injectors.at(-1)}`} ` + `inject${injectors.length === 1 ? 's' : ''} ${cls.name})`;
1247
+ throw new Error(`${cls.name} cannot be created: parameter ${index + 1} of its constructor has no runtime type. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).');
1248
+ }
1249
+ }
1250
+ /**
1251
+ * What must exist before `token`: a factory's `inject`, a provided or bound class's constructor
1252
+ * dependencies, nothing for a value. Only the app's classes and provided tokens count.
1253
+ */ function tokenDependencies(container, providers, token) {
1254
+ const provider = providers.get(token);
1255
+ const dependencies = provider ? isFactoryProvider(provider) ? provider.inject ?? [] : isClassProvider(provider) ? injectedTokens(provider.useClass) : [] : isAppClassToken(token) ? injectedTokens(token) : [];
1256
+ return dependencies.filter((dependency)=>(providers.has(dependency) || isAppClassToken(dependency)) && container.isBound(dependency));
1257
+ }
1258
+ /**
1259
+ * The bound tokens reachable from `roots`, each after everything it depends on: the order factories
1260
+ * are resolved and lifecycle hooks run in. Tokens with no dependency between them keep the roots' order.
1261
+ */ function resolutionOrder(container, providers, roots) {
1262
+ const ordered = [];
1263
+ const seen = new Set();
1264
+ const visit = (token)=>{
1265
+ if (seen.has(token)) return;
1266
+ seen.add(token);
1267
+ tokenDependencies(container, providers, token).forEach(visit);
1268
+ ordered.push(token);
1269
+ };
1270
+ roots.forEach(visit);
1271
+ return ordered;
1272
+ }
1273
+ /** The error for a factory that threw or rejected, naming its token and carrying the original as `cause`. */ function providerFailure(token, error) {
1274
+ const reason = error instanceof Error ? error.message : String(error);
1275
+ return new Error(`The factory providing ${tokenName(token)} failed: ${reason}`, {
1276
+ cause: error
1277
+ });
1278
+ }
1279
+ /**
1280
+ * Runs every factory in `order`, awaiting those that return a promise, so each value is made before
1281
+ * anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails.
1282
+ */ async function resolveProviders(container, providers, order) {
1283
+ for (const token of order){
1284
+ const provider = providers.get(token);
1285
+ if (!provider || !isFactoryProvider(provider)) continue;
1286
+ try {
1287
+ await container.getAsync(token);
1288
+ } catch (error) {
1289
+ throw providerFailure(token, error);
1290
+ }
1291
+ }
1292
+ }
1293
+
1127
1294
  exports.Autocomplete = Autocomplete;
1128
1295
  exports.CATCH_TYPES = CATCH_TYPES;
1129
1296
  exports.CLASS_COOLDOWNS = CLASS_COOLDOWNS;
@@ -1147,15 +1314,18 @@ exports.Once = Once;
1147
1314
  exports.PARAM_SEPARATOR = PARAM_SEPARATOR;
1148
1315
  exports.ReactionHandler = ReactionHandler;
1149
1316
  exports.appliesTo = appliesTo;
1317
+ exports.assertProvided = assertProvided;
1150
1318
  exports.assertStageEntries = assertStageEntries;
1319
+ exports.assertTypedParameters = assertTypedParameters;
1320
+ exports.bindProvider = bindProvider;
1151
1321
  exports.bindShared = bindShared;
1152
1322
  exports.callFilter = callFilter;
1153
1323
  exports.callGuardedHandler = callGuardedHandler;
1324
+ exports.classUnits = classUnits;
1154
1325
  exports.consumeDispatchMark = consumeDispatchMark;
1155
1326
  exports.declaringPrototype = declaringPrototype;
1156
1327
  exports.deferMisuseError = deferMisuseError;
1157
1328
  exports.defineStageTypes = defineStageTypes;
1158
- exports.dependencyOrder = dependencyOrder;
1159
1329
  exports.describeInteraction = describeInteraction;
1160
1330
  exports.findAmbiguousRoutes = findAmbiguousRoutes;
1161
1331
  exports.focusedOptionName = focusedOptionName;
@@ -1173,8 +1343,8 @@ exports.handlerInterceptors = handlerInterceptors;
1173
1343
  exports.hasCustomId = hasCustomId;
1174
1344
  exports.injectedTokens = injectedTokens;
1175
1345
  exports.isAppClassToken = isAppClassToken;
1346
+ exports.isClassProvider = isClassProvider;
1176
1347
  exports.isCustomIdRouted = isCustomIdRouted;
1177
- exports.lifecycleDependencies = lifecycleDependencies;
1178
1348
  exports.makeInjectable = makeInjectable;
1179
1349
  exports.matchFilter = matchFilter;
1180
1350
  exports.matchesCommandType = matchesCommandType;
@@ -1184,11 +1354,17 @@ exports.prepareFilter = prepareFilter;
1184
1354
  exports.prepareHandlerArgs = prepareHandlerArgs;
1185
1355
  exports.prepareInterceptor = prepareInterceptor;
1186
1356
  exports.preparePipe = preparePipe;
1357
+ exports.providerMap = providerMap;
1358
+ exports.reachableClasses = reachableClasses;
1359
+ exports.resolutionOrder = resolutionOrder;
1187
1360
  exports.resolveCommandPaths = resolveCommandPaths;
1188
1361
  exports.resolveOptionParams = resolveOptionParams;
1362
+ exports.resolveProviders = resolveProviders;
1189
1363
  exports.runGuards = runGuards;
1190
1364
  exports.runInterceptors = runInterceptors;
1191
1365
  exports.singletonContextError = singletonContextError;
1192
1366
  exports.stageClass = stageClass;
1193
1367
  exports.stageTypes = stageTypes;
1194
1368
  exports.startDefer = startDefer;
1369
+ exports.tokenDependencies = tokenDependencies;
1370
+ exports.tokenName = tokenName;