meocord 4.2.1 → 4.2.2

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 (36) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-U2NBWcoU.cjs} +45 -13
  3. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-B4MOFBrW.cjs} +58 -15
  4. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +1 -1
  5. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → startup-roots-lA5xkBFq.cjs} +12 -11
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-CbBHUu5P.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +3 -3
  8. package/dist/cjs/core/index.cjs +8 -6
  9. package/dist/cjs/decorator/index.cjs +33 -3
  10. package/dist/cjs/testing/index.cjs +295 -60
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/common/response/response-state.js +5 -5
  13. package/dist/esm/common/translator.js +15 -5
  14. package/dist/esm/core/dispatcher.js +9 -8
  15. package/dist/esm/core/filter-runner.js +2 -2
  16. package/dist/esm/core/handler-pipeline.js +10 -4
  17. package/dist/esm/core/meocord-factory.js +4 -2
  18. package/dist/esm/core/message-help.js +32 -6
  19. package/dist/esm/core/observer-runner.js +2 -1
  20. package/dist/esm/core/providers.js +12 -2
  21. package/dist/esm/core/stage-scope.js +2 -1
  22. package/dist/esm/core/theme-scope.js +4 -1
  23. package/dist/esm/decorator/app.decorator.js +31 -0
  24. package/dist/esm/decorator/filter.decorator.js +2 -2
  25. package/dist/esm/package.json.js +1 -1
  26. package/dist/esm/testing/catalog.js +4 -13
  27. package/dist/esm/testing/discord-defaults.js +21 -1
  28. package/dist/esm/testing/meocord-testing-module.js +24 -16
  29. package/dist/esm/testing/mock-interaction.js +242 -28
  30. package/dist/esm/testing/strict-mocks.js +7 -4
  31. package/dist/esm/util/value.util.js +19 -2
  32. package/dist/types/common/index.d.cts +4 -1
  33. package/dist/types/common/index.d.ts +4 -1
  34. package/dist/types/testing/index.d.cts +28 -14
  35. package/dist/types/testing/index.d.ts +28 -14
  36. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # meocord
2
2
 
3
+ ## 4.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#658](https://github.com/meocord/meocord/pull/658) [`e67cd51`](https://github.com/meocord/meocord/commit/e67cd51a6fdef4a21f1db6541e54b2bb421fbeb1) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A button, select menu or message modal whose guard returns `false` is now acknowledged invisibly, with or without `@Defer`, so the user no longer sees "This interaction failed". The call still reports `ran: false`, and observers still see the outcome `'denied'`. A command, and a modal opened by a command, are left as they were: their only acknowledgement is a reply of their own.
8
+
9
+ A test that expects `getResponse(interaction).calls` to be `[]` after a denied click now sees `['deferUpdate']`, as a test of a handler with `@Defer` already did.
10
+
11
+ A guard that answers the click itself before returning `false` should await that answer. A reply still on its way when the guard returns races the acknowledgement, and Discord refuses whichever comes second, possibly the reply.
12
+
13
+ An app with both a route and a collector for one customId, where the guard returns `false` so the collector answers the click, should leave the click to one of them. Otherwise the two acknowledgements race, and the collector's can fail with "already acknowledged".
14
+
15
+ - [#653](https://github.com/meocord/meocord/pull/653) [`fcd7cbf`](https://github.com/meocord/meocord/commit/fcd7cbf703618541ff3f42497254cfa33b729939) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `warnUnanswered` now names an `@Autocomplete` handler that returns without calling `interaction.respond()`, as it names any other handler that leaves its interaction unanswered. The user sees the autocomplete options fail to load in that case. The warning is shown once per handler: under `meocord start --dev`, and in tests whose app sets `warnUnanswered: true`. `@MeoCord({ warnUnanswered: false })` turns it off. Nothing about the answer itself changes.
16
+
17
+ - [#656](https://github.com/meocord/meocord/pull/656) [`b951297`](https://github.com/meocord/meocord/commit/b9512974067160eddb0029a69e52ff228ba36e76) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `bindTheme`'s documentation now says that a bound function runs in the whole theme of the call that bound it, the server's and user's themes included. Every later call, such as another member's click on a collector, is answered in the starting member's theme. Its example comment said "the handler's theme", which left that out. Nothing about the behaviour changes.
18
+
19
+ - [#652](https://github.com/meocord/meocord/pull/652) [`f8369b6`](https://github.com/meocord/meocord/commit/f8369b6e556ab317331ca70a14a093a0dbc4af2d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A catalog group whose keys include `other`, such as `reasons: { spam: 'Spam', other: 'Other' }`, is read as a group, as the types and `expectCompleteCatalog` read it. Reading the group's own key, `t('reasons')`, shows the key, and in development logs the warning, as any other group does, where it showed the `other` text. Only untyped code, such as code reading a JSON catalog, can make that call; to show the text, read `t('reasons.other')`. A plural is an object whose every key is a plural category, and a translation's plural without an `other` form still falls back to the next catalog.
20
+
21
+ - [#659](https://github.com/meocord/meocord/pull/659) [`e9d25a4`](https://github.com/meocord/meocord/commit/e9d25a4bd27bf1e3adaa0506c1b165e8fe6ff20c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The built-in `help` reply over 2,000 characters is now split into messages Discord shows as written:
22
+
23
+ - A line longer than a message is cut after the last whole character that fits. An emoji, a flag, a joined emoji or a letter with its accent is no longer split across two messages.
24
+ - A message that would be only whitespace isn't sent. Discord refused it as empty, and the rest of the reply was lost with it.
25
+
26
+ A help text with no long line and no blank stretch at a cut is split as before.
27
+
28
+ - [#665](https://github.com/meocord/meocord/pull/665) [`1a224e8`](https://github.com/meocord/meocord/commit/1a224e89d747b1dc0a29858f5bae8969d8657091) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Under `useStrictMocks()`, a mock interaction is marked `replied`, `deferred` or `responded` once its answer resolves, as discord.js marks it after Discord takes the answer, rather than when the answer is called. Code that doesn't await an answer now fails in a test as it would on a bot:
29
+
30
+ - an `editReply()`, `followUp()` or `deleteReply()` made before the first answer resolves rejects with `InteractionNotReplied`;
31
+ - a second answer sent while the first is in flight passes discord.js's check and is refused with Discord's 40060, unless the first one failed;
32
+ - `replied`, `deferred`, `responded` and `ephemeral` read in the same run as an un-awaited answer are as before it.
33
+
34
+ This catches overlaps within one synchronous run of the test's code. A mock answer resolves on the next microtask, so an answer still in flight across an `await`, as a guard's `void interaction.reply()` followed by `return false` is on a bot, isn't caught.
35
+
36
+ In both modes, an answer that fails no longer undoes an answer made after it. A test could make the first of two un-awaited replies reject, and the interaction then read as unanswered though the second reply went through; it now reads as answered by the second. A failed answer's change is undone once every change after it is undone too, so an interaction whose answers all fail still reads as unanswered.
37
+
38
+ In default mode the flags are still set at the call. The first time an interaction does what a bot would refuse, an edit or a second answer while its first is in flight, a warning says what the bot would do. In the next major version (5.0), every mock interaction marks its answers when they resolve, as strict mocks do now.
39
+
40
+ - [#663](https://github.com/meocord/meocord/pull/663) [`3894f77`](https://github.com/meocord/meocord/commit/3894f77b099bc2bb82bed37b43944f9e6dc09feb) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Under `useStrictMocks()`, a mock interaction's `ephemeral` is `null` until `reply()` or `deferReply()` decides it, as discord.js has it; `update()` and `deferUpdate()` leave it `null`. In default mode it still reads `false` before an answer, and a test reading it there logs a warning once. MeoCord's own reads while it answers log nothing. The next major version (5.0) reads `null` without the call.
41
+
42
+ - [#651](https://github.com/meocord/meocord/pull/651) [`8a56464`](https://github.com/meocord/meocord/commit/8a56464a42c47bddc3959bddc0748a710f656ffd) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A mock guild's `emojis`, `stickers`, `scheduledEvents`, `autoModerationRules` and `invites` are managers, in both modes, as on a message's guild. Before, calling `create()`, `fetch()` or `cache.get()` on one threw a `TypeError`.
43
+
44
+ - `create()` resolves to a mock emoji, sticker, scheduled event or AutoMod rule in the guild, and caches it, as discord.js does.
45
+ - `invites.create(channel)` resolves to an invite for that channel in the guild, keyed by its `code`, and caches nothing, as discord.js does.
46
+ - `fetch(id)` resolves to the cached item, or makes and caches one; a list fetch resolves to an empty collection.
47
+ - Each item is named, and what discord.js formats from it computes: an emoji's mention (`String(emoji)`) and `identifier`, and a scheduled event's and an invite's `url`.
48
+
49
+ - [#657](https://github.com/meocord/meocord/pull/657) [`8d1d3fc`](https://github.com/meocord/meocord/commit/8d1d3fc6eb0d4f59139ffc3033b1c416a507f804) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `MockedFunction`'s and `MockInstance`'s documentation says what their types check: calling a mock is checked against the function it mocks, and the values its mock API takes and records, such as `mockResolvedValue`'s and each of `mock.calls`' arguments, are not, for any function. A partial fake such as `guild.members.fetch.mockResolvedValue({ id })` type-checks. Types are unchanged.
50
+
51
+ - [#654](https://github.com/meocord/meocord/pull/654) [`1cc5a4f`](https://github.com/meocord/meocord/commit/1cc5a4fd2243ff00be9ae3d89b389f11df3b337c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Wherever MeoCord takes a class and is given something else, it now refuses the value and names where it was given, instead of crashing with an empty `TypeError` or reporting "This is a bug in MeoCord". TypeScript already rejects these values, so the refusal matters for plain JavaScript, `any` and casts.
52
+
53
+ - **`@MeoCord`'s `presenter`, `cooldownStore`, `controllers` and `services`** refuse an object or a function that isn't a class where the app is declared, for example `App: @MeoCord({ presenter }) takes a class implementing ResponsePresenter, not an object.`
54
+ - **A service listed by a string or symbol token** that no provider provides is refused as the app is created, naming the token.
55
+ - **Stage entries** given to `@UseGuard`, `@UseInterceptor`, `@UseFilter`, `@UsePipe` and `@MeoCord({ guards, interceptors, filters })` refuse an arrow function, a method, an async function or a generator function: "a function is not a class". A `function` declaration still works as a class.
56
+ - **`@Catch`** warns about an arrow function, as it already does for any other value that isn't a class, and treats it as matching no error. Before this fix, every call whose error reached that filter rejected with a `TypeError` from `instanceof`. A bound error class still matches.
57
+ - **A testing module** refuses a controller or an observer that isn't a class, and reports the refusal together with the other startup errors. After `reportAllStartupErrors()`, `fromApp()` on an app whose `@MeoCord` was refused throws the reason, not "is not a @MeoCord app".
58
+
59
+ A bot that started before starts unchanged.
60
+
3
61
  ## 4.2.1
4
62
 
5
63
  ### Patch Changes
@@ -128,7 +128,7 @@ let problem;
128
128
  try {
129
129
  // Called through a variable so a bundler does not try to resolve the path while building the
130
130
  // application; the file is only there once the build is done.
131
- const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-XKMlcUZz.cjs', document.baseURI).href)));
131
+ const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-U2NBWcoU.cjs', document.baseURI).href)));
132
132
  const loaded = load(compiledPath);
133
133
  return loaded.default ?? loaded;
134
134
  } catch (error) {
@@ -403,7 +403,7 @@ const warned = new Set();
403
403
  }
404
404
  });
405
405
 
406
- var version = "4.2.1";
406
+ var version = "4.2.2";
407
407
  var packageJson = {
408
408
  version: version};
409
409
 
@@ -595,6 +595,9 @@ let themeOutsideCalls;
595
595
  * Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
596
596
  * Timers and promises the handler starts keep its theme without it.
597
597
  *
598
+ * The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
599
+ * it is for, is answered in the starting member's theme.
600
+ *
598
601
  * @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
599
602
  * @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
600
603
  *
@@ -604,7 +607,7 @@ let themeOutsideCalls;
604
607
  * async poll(message: Message) {
605
608
  * const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
606
609
  * collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
607
- * const { emojis } = useTheme() // the handler's theme, @UseTheme included
610
+ * const { emojis } = useTheme() // the starting member's theme, @UseTheme included
608
611
  * await click.reply(`${emojis.success} Picked`)
609
612
  * }))
610
613
  * }
@@ -1246,16 +1249,26 @@ const logger$5 = new Logger('SetMetadata');
1246
1249
  }
1247
1250
  const DISCORD_LOCALES = new Set(Object.values(discord_js.Locale));
1248
1251
  const languageOf = (locale)=>locale.split('-')[0];
1249
- /** A message by its dotted key, or undefined when the catalog lacks it or the key names a group. */ function lookup(catalog, key) {
1252
+ const PLURAL_CATEGORIES = new Set([
1253
+ 'zero',
1254
+ 'one',
1255
+ 'two',
1256
+ 'few',
1257
+ 'many',
1258
+ 'other'
1259
+ ]);
1260
+ /** A plural's shape: a non-empty object whose every key is a plural category, as the types read one; any other is a group. */ const isPluralShape = (value)=>typeof value === 'object' && value !== null && Object.keys(value).length > 0 && Object.keys(value).every((key)=>PLURAL_CATEGORIES.has(key));
1261
+ /**
1262
+ * A message by its dotted key, or undefined when the catalog lacks it or the key names a group. A plural without an
1263
+ * `other` form reads as missing, so a translation that left it out falls back to the next catalog.
1264
+ */ function lookup(catalog, key) {
1250
1265
  let current = catalog;
1251
1266
  for (const part of key.split('.')){
1252
1267
  if (typeof current !== 'object' || current === null) return undefined;
1253
1268
  current = current[part];
1254
1269
  }
1255
1270
  if (typeof current === 'string') return current;
1256
- if (typeof current === 'object' && current !== null && typeof current.other === 'string') {
1257
- return current;
1258
- }
1271
+ if (isPluralShape(current) && typeof current.other === 'string') return current;
1259
1272
  return undefined;
1260
1273
  }
1261
1274
  const plurals = new Map();
@@ -1978,7 +1991,24 @@ brand(CooldownStoreError, 'CooldownStoreError');
1978
1991
  else logger.error(`Could not ${what}:`, error);
1979
1992
  }
1980
1993
 
1981
- /** Whether a value is a class, which a stage entry or a `@Catch` type must be: what `new` and `instanceof` take. */ const isConstructor = (value)=>typeof value === 'function';
1994
+ /** Whether `new` takes a value, as `instanceof` needs of its right side, such as a `@Catch` type. */ function isConstructible(value) {
1995
+ if (typeof value !== 'function') return false;
1996
+ try {
1997
+ // Fails unless value can be new.target, which only a constructor can; none of value's constructor code runs
1998
+ Reflect.construct(String, [], value);
1999
+ return true;
2000
+ } catch {
2001
+ return false;
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Whether a value is a class, which a stage entry or an app option must be: constructible, with an object prototype for
2006
+ * the metadata MeoCord reads. An arrow function, a method, an async or a generator function is none.
2007
+ */ function isConstructor(value) {
2008
+ if (!isConstructible(value)) return false;
2009
+ const prototype = value.prototype;
2010
+ return typeof prototype === 'object' && prototype !== null;
2011
+ }
1982
2012
  /** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
1983
2013
  if (value === null || value === undefined) return String(value);
1984
2014
  if (typeof value === 'string') return quoteForLog(value);
@@ -3383,15 +3413,15 @@ function toJson(value) {
3383
3413
  });
3384
3414
  }
3385
3415
  /**
3386
- * Undoes `@Defer`'s acknowledgement after a guard denied the call silently: a command's deferred
3387
- * reply is deleted while nothing was sent into it; a component's invisible acknowledgement needs
3388
- * nothing. Never throws.
3416
+ * Ends a call a guard denied silently. A component still unanswered is acknowledged invisibly, with or without
3417
+ * `@Defer`, since Discord tells the user an unanswered call failed; so is a command `@Defer`'s 'auto' timer was
3418
+ * waiting on, whose deferred reply is then deleted, as `@Defer`'s is while nothing was sent into it. Never throws.
3389
3419
  */ abandon() {
3390
3420
  const waiting = this.timer !== undefined;
3391
3421
  this.cancelScheduled();
3392
3422
  return this.queued(async ()=>{
3393
- // Denied before the 'auto' timer: acknowledged here, since Discord tells the user an unanswered call failed
3394
- if (waiting) await this.acknowledgeUnanswered({
3423
+ // A command's acknowledgement shows "thinking…", so one is made only for the timer that would have made it
3424
+ if (waiting || !answersWithOwnMessage(this.interaction)) await this.acknowledgeUnanswered({
3395
3425
  ephemeral: true
3396
3426
  });
3397
3427
  this.sync();
@@ -5559,9 +5589,11 @@ exports.hideInLogs = hideInLogs;
5559
5589
  exports.inferContextType = inferContextType;
5560
5590
  exports.interactionLocale = interactionLocale;
5561
5591
  exports.isBuiltApplication = isBuiltApplication;
5592
+ exports.isConstructible = isConstructible;
5562
5593
  exports.isConstructor = isConstructor;
5563
5594
  exports.isCustomIdRouted = isCustomIdRouted;
5564
5595
  exports.isExplainedError = isExplainedError;
5596
+ exports.isPluralShape = isPluralShape;
5565
5597
  exports.isRefusal = isRefusal;
5566
5598
  exports.isTooLarge = isTooLarge;
5567
5599
  exports.isUserOutcome = isUserOutcome;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
3
+ var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
4
4
  require('reflect-metadata');
5
5
  var node_async_hooks = require('node:async_hooks');
6
6
  var inversify = require('inversify');
@@ -90,7 +90,8 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
90
90
  }
91
91
  }
92
92
  /** The class of a stage entry. */ function stageClass(entry) {
93
- return controller_decorator.isConstructor(entry) ? entry : entry.provide;
93
+ // Entries were checked as they were declared, so a function is the class itself
94
+ return typeof entry === 'function' ? entry : entry.provide;
94
95
  }
95
96
  /** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
96
97
  return Reflect.getMetadata(controller_decorator.META.stageTypes, stageClass(entry));
@@ -1684,7 +1685,7 @@ function isInterceptorWithParams(entry) {
1684
1685
  // Every filter reaching here was checked for @Catch at startup, so its types are always recorded
1685
1686
  const types = Reflect.getOwnMetadata(controller_decorator.META.catchTypes, filterClass(entry));
1686
1687
  // Only classes match: anything else given to @Catch, warned of as it applied, matches no error
1687
- if (types.length === 0 || types.some((type)=>controller_decorator.isConstructor(type) && error instanceof type)) return entry;
1688
+ if (types.length === 0 || types.some((type)=>controller_decorator.isConstructible(type) && error instanceof type)) return entry;
1688
1689
  }
1689
1690
  }
1690
1691
  return undefined;
@@ -1789,7 +1790,7 @@ const logger$1 = new controller_decorator.Logger('Observer');
1789
1790
  /** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
1790
1791
  for (const observer of observers){
1791
1792
  if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
1792
- const name = typeof observer === 'function' ? observer.name : String(observer);
1793
+ const name = controller_decorator.isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
1793
1794
  controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
1794
1795
  continue;
1795
1796
  }
@@ -2332,7 +2333,9 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
2332
2333
  denial
2333
2334
  })) {
2334
2335
  outcome = 'denied';
2335
- await response?.abandon();
2336
+ // Without @Defer too, so a denied component is acknowledged rather than left to fail
2337
+ const denied = response ?? (type === 'interaction' && first?.isRepliable?.() ? controller_decorator.responseOf(first) : undefined);
2338
+ await denied?.abandon();
2336
2339
  return {
2337
2340
  ran: false
2338
2341
  };
@@ -2388,14 +2391,18 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
2388
2391
  };
2389
2392
  } finally{
2390
2393
  await response?.release();
2391
- if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && type === 'interaction') {
2394
+ if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && (type === 'interaction' || type === 'autocomplete')) {
2392
2395
  const phase = responsePhaseOf(contextOf());
2393
2396
  // The handler is to blame only once it has finished; until then, the interceptor that ended the call is
2394
- const blamed = !finished && returnedEarly ? {
2397
+ const returnedBy = !finished && returnedEarly ? {
2395
2398
  interceptor: returnedEarly,
2396
2399
  handlerStarted: ran
2397
2400
  } : undefined;
2398
- if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered(phase, blamed);
2401
+ if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered({
2402
+ type,
2403
+ phase,
2404
+ returnedBy
2405
+ });
2399
2406
  }
2400
2407
  if (options.awaitObservers) await starting;
2401
2408
  // After the answer and the release, so the duration covers the whole call
@@ -4190,22 +4197,48 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
4190
4197
  scope
4191
4198
  ].filter((line)=>line !== undefined).join('\n');
4192
4199
  }
4193
- /** Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. */ function splitReply(text) {
4200
+ // Built on first use, for a line longer than a message
4201
+ let graphemes;
4202
+ /**
4203
+ * Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
4204
+ * or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
4205
+ */ function fittingCut(piece) {
4206
+ let cut = 0;
4207
+ graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
4208
+ granularity: 'grapheme'
4209
+ }));
4210
+ for (const { index, segment } of graphemes.segment(piece)){
4211
+ if (index + segment.length > MAX_REPLY_LENGTH) break;
4212
+ cut = index + segment.length;
4213
+ }
4214
+ if (cut > 0) return cut;
4215
+ const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
4216
+ return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
4217
+ }
4218
+ /**
4219
+ * Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
4220
+ * message of only whitespace, which Discord refuses as empty, is left out.
4221
+ */ function splitReply(text) {
4194
4222
  const chunks = [];
4223
+ const push = (chunk)=>{
4224
+ if (chunk.trim() !== '') chunks.push(chunk);
4225
+ };
4195
4226
  let current = '';
4196
4227
  for (const line of text.split('\n')){
4197
- for(let piece = line;; piece = piece.slice(MAX_REPLY_LENGTH)){
4198
- const part = piece.slice(0, MAX_REPLY_LENGTH);
4228
+ for(let piece = line;;){
4229
+ const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
4230
+ const part = piece.slice(0, cut);
4199
4231
  const joined = current ? `${current}\n${part}` : part;
4200
4232
  if (joined.length <= MAX_REPLY_LENGTH) current = joined;
4201
4233
  else {
4202
- chunks.push(current);
4234
+ push(current);
4203
4235
  current = part;
4204
4236
  }
4205
- if (piece.length <= MAX_REPLY_LENGTH) break;
4237
+ if (cut === piece.length) break;
4238
+ piece = piece.slice(cut);
4206
4239
  }
4207
4240
  }
4208
- if (current) chunks.push(current);
4241
+ push(current);
4209
4242
  return chunks;
4210
4243
  }
4211
4244
 
@@ -4747,7 +4780,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4747
4780
  controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
4748
4781
  continue;
4749
4782
  }
4750
- if ('useClass' in entry && typeof entry.useClass !== 'function') {
4783
+ if ('useClass' in entry && !controller_decorator.isConstructor(entry.useClass)) {
4751
4784
  controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
4752
4785
  continue;
4753
4786
  }
@@ -4824,6 +4857,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4824
4857
  });
4825
4858
  for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
4826
4859
  }
4860
+ /**
4861
+ * Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
4862
+ * as if it were a class.
4863
+ */ function assertProvidedServices(appName, services, providers, where) {
4864
+ for (const service of services){
4865
+ if (typeof service === 'function' || providers.has(service)) continue;
4866
+ controller_decorator.startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
4867
+ }
4868
+ }
4827
4869
  /**
4828
4870
  * Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
4829
4871
  * when the class is first resolved. A class token is bound on demand, so it is never missing.
@@ -4991,6 +5033,7 @@ exports.appPresenterOf = appPresenterOf;
4991
5033
  exports.appStages = appStages;
4992
5034
  exports.assertObservers = assertObservers;
4993
5035
  exports.assertProvided = assertProvided;
5036
+ exports.assertProvidedServices = assertProvidedServices;
4994
5037
  exports.assertStageEntries = assertStageEntries;
4995
5038
  exports.assertStartupChecked = assertStartupChecked;
4996
5039
  exports.assertTypedParameters = assertTypedParameters;
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var node_crypto = require('node:crypto');
4
- var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
4
+ var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
5
5
 
6
6
  /** The `fatal` code of a shard whose app MeoCord refused, which every shard would refuse alike. */ const REFUSED_CODE = 'Refused';
7
7
  /** Whether an IPC message is one of MeoCord's own, rather than one of discord.js's. */ function isShardMessage(message) {
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var discord_js = require('discord.js');
4
- var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
5
- var providers = require('./providers-BZgJyfZS.cjs');
4
+ var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
5
+ var providers = require('./providers-B4MOFBrW.cjs');
6
6
  var enum_index = require('../enum/index.cjs');
7
7
  var node_async_hooks = require('node:async_hooks');
8
- var translateError = require('./translate-error-BUAR1WYG.cjs');
8
+ var translateError = require('./translate-error-CbBHUu5P.cjs');
9
9
 
10
10
  /**
11
11
  * How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
@@ -1480,10 +1480,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1480
1480
  * says whether the handler ran. Its guard wrappers let this call through, so each guard runs once.
1481
1481
  */ async invokeHandler(instance, methodName, args, call, hooks = {}) {
1482
1482
  const handler = `${instance.constructor.name}.${methodName}`;
1483
- const onUnanswered = this.options.warnUnanswered ? (phase, returnedBy)=>this.warnUnansweredOnce(handler, phase, returnedBy && {
1484
- name: returnedBy.interceptor.name,
1485
- handlerStarted: returnedBy.handlerStarted
1486
- }) : undefined;
1483
+ const onUnanswered = this.options.warnUnanswered ? (unanswered)=>this.warnUnansweredOnce(handler, unanswered) : undefined;
1487
1484
  const outcome = await providers.runHandler(this.container, instance, methodName, args, {
1488
1485
  ...this.runOptions(call),
1489
1486
  onUnanswered,
@@ -1493,13 +1490,17 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1493
1490
  return outcome.ran;
1494
1491
  }
1495
1492
  /**
1496
- * Warns, once per handler, that it left its interaction unanswered or deferred without a follow-up, or that
1497
- * `interceptor` returned before the handler ran or finished, and the call ended so.
1498
- */ warnUnansweredOnce(handler, phase, interceptor) {
1493
+ * Warns, once per handler, that it left its interaction or autocomplete unanswered or deferred without a follow-up,
1494
+ * or that an interceptor returned before the handler ran or finished, and the call ended so.
1495
+ */ warnUnansweredOnce(handler, { type, phase, returnedBy }) {
1499
1496
  if (this.warnedUnanswered.has(handler)) return;
1500
1497
  this.warnedUnanswered.add(handler);
1498
+ const interceptor = returnedBy && {
1499
+ name: returnedBy.interceptor.name,
1500
+ handlerStarted: returnedBy.handlerStarted
1501
+ };
1501
1502
  const returned = interceptor && `${handler}: its interceptor ${interceptor.name} returned before the handler ${interceptor.handlerStarted ? 'finished' : 'ran'}`;
1502
- const what = interceptor ? phase === 'unanswered' ? `${returned}, without answering the interaction, so the user saw "The application did not respond". Answer it ` + `in ${interceptor.name}, or await next.handle().` : interceptor.handlerStarted ? `${returned}, and the deferred interaction had no follow-up when the call ended, so @Defer's lock was ` + `released while the user still saw it thinking. Follow up in ${interceptor.name} with respond(interaction).send(), ` + 'or await next.handle().' : `${returned}, and the deferred interaction was never followed up, so the user saw it thinking until Discord ` + `gave up. Follow up in ${interceptor.name} with respond(interaction).send(), or await next.handle().` : phase === 'unanswered' ? `${handler} finished without answering its interaction, so the user saw "The application did not respond". ` + 'Answer it with respond(interaction).send(), or acknowledge it first with @Defer().' : `${handler} deferred its interaction and never followed up, so the user saw it thinking until Discord gave up. ` + 'Follow up with respond(interaction).send().';
1503
+ const what = type === 'autocomplete' ? `${handler} finished without answering its autocomplete, so the user saw its options fail to load. Answer it ` + 'with interaction.respond(choices) within 3 seconds.' : interceptor ? phase === 'unanswered' ? `${returned}, without answering the interaction, so the user saw "The application did not respond". Answer it ` + `in ${interceptor.name}, or await next.handle().` : interceptor.handlerStarted ? `${returned}, and the deferred interaction had no follow-up when the call ended, so @Defer's lock was ` + `released while the user still saw it thinking. Follow up in ${interceptor.name} with respond(interaction).send(), ` + 'or await next.handle().' : `${returned}, and the deferred interaction was never followed up, so the user saw it thinking until Discord ` + `gave up. Follow up in ${interceptor.name} with respond(interaction).send(), or await next.handle().` : phase === 'unanswered' ? `${handler} finished without answering its interaction, so the user saw "The application did not respond". ` + 'Answer it with respond(interaction).send(), or acknowledge it first with @Defer().' : `${handler} deferred its interaction and never followed up, so the user saw it thinking until Discord gave up. ` + 'Follow up with respond(interaction).send().';
1503
1504
  this.logger.warn(`${what} Shown once per handler; @MeoCord({ warnUnanswered: false }) turns it off.`);
1504
1505
  }
1505
1506
  /**
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
3
+ var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
4
4
  var discord_js = require('discord.js');
5
5
 
6
6
  const logger = new controller_decorator.Logger('Route');
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('../_shared/controller.decorator-XKMlcUZz.cjs');
3
+ var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
4
4
  require('reflect-metadata');
5
- var translateError = require('../_shared/translate-error-BUAR1WYG.cjs');
6
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-CsJJyCZd.cjs');
5
+ var translateError = require('../_shared/translate-error-CbBHUu5P.cjs');
6
+ var shardedCooldownStore = require('../_shared/sharded-cooldown-store-HTUb3Ii_.cjs');
7
7
  require('../enum/index.cjs');
8
8
  require('discord.js');
9
9
  require('path');
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  require('reflect-metadata');
4
- var providers = require('../_shared/providers-BZgJyfZS.cjs');
4
+ var providers = require('../_shared/providers-B4MOFBrW.cjs');
5
5
  var inversify = require('inversify');
6
6
  var discord_js = require('discord.js');
7
- var controller_decorator = require('../_shared/controller.decorator-XKMlcUZz.cjs');
7
+ var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
8
8
  require('node:crypto');
9
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-CsJJyCZd.cjs');
10
- var startupRoots = require('../_shared/startup-roots-CgfPQrNl.cjs');
9
+ var shardedCooldownStore = require('../_shared/sharded-cooldown-store-HTUb3Ii_.cjs');
10
+ var startupRoots = require('../_shared/startup-roots-lA5xkBFq.cjs');
11
11
  var enum_index = require('../enum/index.cjs');
12
12
  var node_fs = require('node:fs');
13
13
  var path = require('node:path');
@@ -23,7 +23,7 @@ require('dayjs/plugin/utc.js');
23
23
  require('dayjs/plugin/timezone.js');
24
24
  require('module');
25
25
  require('chalk');
26
- require('../_shared/translate-error-BUAR1WYG.cjs');
26
+ require('../_shared/translate-error-CbBHUu5P.cjs');
27
27
 
28
28
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
29
29
 
@@ -1628,7 +1628,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
1628
1628
  const providers$1 = controller_decorator.collectStartupErrors(()=>{
1629
1629
  declared.forEach(controller_decorator.startupError);
1630
1630
  startupRoots.assertDistinctCommands(options.controllers);
1631
- return providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
1631
+ const providers$1 = providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
1632
+ providers.assertProvidedServices(typeof target === 'function' ? target.name : String(target), options.services ?? [], providers$1, '@MeoCord({ providers })');
1633
+ return providers$1;
1632
1634
  });
1633
1635
  // A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
1634
1636
  const themeResolver = controller_decorator.themeResolverClass(options.themeFor);
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  require('reflect-metadata');
4
- var providers = require('../_shared/providers-BZgJyfZS.cjs');
5
- var controller_decorator = require('../_shared/controller.decorator-XKMlcUZz.cjs');
4
+ var providers = require('../_shared/providers-B4MOFBrW.cjs');
5
+ var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
6
6
  var inversify = require('inversify');
7
7
  require('node:async_hooks');
8
8
  require('../enum/index.cjs');
@@ -447,7 +447,7 @@ require('chalk');
447
447
  controller_decorator.refuseOnMethod('@Catch', target, propertyKey);
448
448
  providers.makeInjectable(target);
449
449
  errorTypes.forEach((type, index)=>{
450
- if (controller_decorator.isConstructor(type)) return;
450
+ if (controller_decorator.isConstructible(type)) return;
451
451
  const entry = ORDINALS[index] ? `${ORDINALS[index]} entry` : `entry ${index + 1}`;
452
452
  controller_decorator.warnDeprecatedBehaviour(logger$1, `${target.name}: @Catch's ${entry}, ${controller_decorator.describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
453
453
  });
@@ -555,6 +555,35 @@ const ORDINALS = [
555
555
  if (type.labelKey !== undefined) assertLabelKey(appName, name, type.labelKey, i18n);
556
556
  }
557
557
  }
558
+ /**
559
+ * Refuses an option that takes a class given anything else, such as an object or an arrow function, where the app is
560
+ * declared: the startup checks read each class's prototype, and would otherwise fail on it without naming the option.
561
+ */ function assertClassOptions(appName, { presenter, cooldownStore, controllers, services }) {
562
+ for (const [name, value, implementing] of [
563
+ [
564
+ 'presenter',
565
+ presenter,
566
+ 'ResponsePresenter'
567
+ ],
568
+ [
569
+ 'cooldownStore',
570
+ cooldownStore,
571
+ 'CooldownStore'
572
+ ]
573
+ ]){
574
+ if (value !== undefined && !controller_decorator.isConstructor(value)) {
575
+ throw controller_decorator.refuse(new TypeError(`${appName}: @MeoCord({ ${name} }) takes a class implementing ${implementing}, not ${controller_decorator.describeValue(value)}.`));
576
+ }
577
+ }
578
+ for (const controller of controllers ?? []){
579
+ if (!controller_decorator.isConstructor(controller)) throw controller_decorator.refuse(new TypeError(`${appName}: @MeoCord({ controllers }) takes classes, not ${controller_decorator.describeValue(controller)}.`));
580
+ }
581
+ // A string or symbol is a token, which a provider has to provide; that is checked as the app is created
582
+ for (const service of services ?? []){
583
+ if (controller_decorator.isConstructor(service) || typeof service === 'string' || typeof service === 'symbol') continue;
584
+ throw controller_decorator.refuse(new TypeError(`${appName}: @MeoCord({ services }) takes classes, or tokens that providers provide, not ${controller_decorator.describeValue(service)}.`));
585
+ }
586
+ }
558
587
  /** Refuses a type's `labelKey` that no message of the app's default catalog has, as it would be shown as the key. */ function assertLabelKey(appName, name, labelKey, i18n) {
559
588
  const where = `${appName}: @MeoCord({ messages: { types } }): "${name}" has labelKey`;
560
589
  if (typeof labelKey !== 'string') throw controller_decorator.refuse(new TypeError(`${where} ${String(labelKey)}; it takes a message key, such as 'types.${name}'.`));
@@ -599,6 +628,7 @@ const ORDINALS = [
599
628
  */ function MeoCord(options) {
600
629
  return controller_decorator.declaring((target, propertyKey)=>{
601
630
  if (controller_decorator.deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
631
+ assertClassOptions(target.name, options);
602
632
  providers.assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
603
633
  providers.assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
604
634
  providers.assertStageEntries('@MeoCord({ filters })', 'filter', target.name, options.filters ?? []);