meocord 4.0.0-beta.3 → 4.0.0-beta.4

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 (57) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +3 -1
  3. package/dist/cjs/_shared/{controller.decorator-9nIxOp9y.cjs → controller.decorator-CqgGgjj3.cjs} +30 -106
  4. package/dist/cjs/_shared/{metadata-key.enum-aABGdodb.cjs → metadata-key.enum-DG7cVMc4.cjs} +0 -1
  5. package/dist/cjs/_shared/{theme-Dqwlnz0m.cjs → theme-C8w8ivsR.cjs} +4 -12
  6. package/dist/cjs/common/index.cjs +1 -1
  7. package/dist/cjs/core/index.cjs +29 -57
  8. package/dist/cjs/decorator/index.cjs +36 -106
  9. package/dist/cjs/enum/index.cjs +4 -8
  10. package/dist/cjs/testing/index.cjs +82 -140
  11. package/dist/esm/bin/app-template/README.md.template +3 -1
  12. package/dist/esm/bin/app-template/vitest.config.ts.template +3 -5
  13. package/dist/esm/bin/helper/app-generator.helper.js +6 -24
  14. package/dist/esm/bin/helper/controller-generator.helper.js +8 -35
  15. package/dist/esm/bin/helper/guard-generator.helper.js +2 -7
  16. package/dist/esm/bin/helper/service-generator.helper.js +2 -7
  17. package/dist/esm/bin/meocord.js +9 -31
  18. package/dist/esm/build/native-addons.js +10 -33
  19. package/dist/esm/build/rsbuild-config.js +8 -26
  20. package/dist/esm/core/meocord.app.js +23 -46
  21. package/dist/esm/decorator/app.decorator.js +10 -25
  22. package/dist/esm/decorator/command-builder.decorator.js +6 -9
  23. package/dist/esm/decorator/controller.decorator.js +14 -45
  24. package/dist/esm/decorator/guard.decorator.js +14 -56
  25. package/dist/esm/decorator/index.js +1 -1
  26. package/dist/esm/decorator/service.decorator.js +4 -8
  27. package/dist/esm/enum/controller.enum.js +3 -7
  28. package/dist/esm/enum/metadata-key.enum.js +0 -1
  29. package/dist/esm/package.json.js +1 -1
  30. package/dist/esm/testing/meocord-testing-module.js +9 -5
  31. package/dist/esm/testing/mock-fn.js +14 -25
  32. package/dist/esm/testing/mock-interaction.js +58 -109
  33. package/dist/esm/util/common.util.js +5 -22
  34. package/dist/esm/util/generator-cli.util.js +13 -51
  35. package/dist/esm/util/interaction.util.js +15 -60
  36. package/dist/esm/util/json.util.js +1 -9
  37. package/dist/esm/util/meocord-cli.util.js +4 -31
  38. package/dist/esm/util/meocord-config-loader.util.js +3 -11
  39. package/dist/esm/util/meocord-source-config.util.js +2 -9
  40. package/dist/esm/util/package-manager.util.js +2 -6
  41. package/dist/esm/util/package-version.util.js +2 -11
  42. package/dist/esm/util/platform.util.js +3 -8
  43. package/dist/esm/util/runtime.util.js +10 -38
  44. package/dist/esm/util/tsconfig.util.js +4 -9
  45. package/dist/types/{controller.enum-DYfhYaat.d.ts → controller.enum-BL6wEQls.d.cts} +3 -7
  46. package/dist/types/{controller.enum-DYfhYaat.d.cts → controller.enum-BL6wEQls.d.ts} +3 -7
  47. package/dist/types/core/index.d.cts +25 -53
  48. package/dist/types/core/index.d.ts +25 -53
  49. package/dist/types/decorator/index.d.cts +45 -168
  50. package/dist/types/decorator/index.d.ts +45 -168
  51. package/dist/types/enum/index.d.cts +1 -2
  52. package/dist/types/enum/index.d.ts +1 -2
  53. package/dist/types/interface/index.d.cts +49 -58
  54. package/dist/types/interface/index.d.ts +49 -58
  55. package/dist/types/testing/index.d.cts +85 -87
  56. package/dist/types/testing/index.d.ts +85 -87
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # meocord
2
2
 
3
+ ## 4.0.0-beta.4
4
+
5
+ ### Major Changes
6
+
7
+ - [#44](https://github.com/l7aromeo/meocord/pull/44) [`e48472e`](https://github.com/l7aromeo/meocord/commit/e48472e9ffee2f0371a7a2a71a4063e99ce61674) Thanks [@l7aromeo](https://github.com/l7aromeo)! - **Breaking:** `meocord/decorator` exports only the decorators. The routing helpers it also exported
8
+ — `getCommandMap`, `getMessageHandlers`, `getReactionHandlers`, `getAutocompleteHandlers`,
9
+ `findAmbiguousRoutes` and `PARAM_SEPARATOR` — are internal to MeoCord now. See the
10
+ [migration guide](https://github.com/l7aromeo/meocord/blob/main/docs/MIGRATING.md#internal-helpers-are-no-longer-exported).
11
+
3
12
  ## 4.0.0-beta.3
4
13
 
5
14
  ### Patch Changes
package/README.md CHANGED
@@ -273,7 +273,9 @@ See [Self-contained builds](#self-contained-builds) for when to turn on `bundleD
273
273
 
274
274
  ### ESLint
275
275
 
276
- MeoCord exports a base ESLint config from `meocord/eslint`. Extend it as needed:
276
+ MeoCord exports a base ESLint config from `meocord/eslint`. It lints your TypeScript, `meocord.config.ts`
277
+ included, with type information from `tsconfig.json`, `tsconfig.test.json` and `tsconfig.eslint.json`; a
278
+ file ESLint reports as not included in any of them needs listing in one. Extend it as needed:
277
279
 
278
280
  ```javascript
279
281
  import meocordEslint, { typescriptConfig } from 'meocord/eslint'
@@ -3,18 +3,12 @@
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
5
  var enum_index = require('../enum/index.cjs');
6
- var metadataKey_enum = require('./metadata-key.enum-aABGdodb.cjs');
6
+ var metadataKey_enum = require('./metadata-key.enum-DG7cVMc4.cjs');
7
7
  var discord_js = require('discord.js');
8
8
 
9
9
  /**
10
- * The discord.js class each command type is handled by.
11
- *
12
- * One table rather than a chain of `isButton() || isStringSelectMenu() || ...`: the
13
- * registration guard in `@Command`, the dispatcher, and the type-level
14
- * `CommandInteractionType` all have to agree on what a command type accepts, and a
15
- * chain repeated in three files drifts the moment a fifth select menu appears. Adding
16
- * a `CommandType` member without an entry here is a compile error, not a silent
17
- * fall-through to "Command not found!".
10
+ * The discord.js class check for each command type, shared by `@Command`, the dispatcher and
11
+ * `CommandInteractionType`. A `CommandType` without an entry here fails to compile.
18
12
  */ const INTERACTION_MATCHERS = {
19
13
  [enum_index.CommandType.SLASH]: (interaction)=>interaction instanceof discord_js.ChatInputCommandInteraction,
20
14
  [enum_index.CommandType.CONTEXT_MENU]: (interaction)=>interaction instanceof discord_js.ContextMenuCommandInteraction,
@@ -32,47 +26,24 @@ var discord_js = require('discord.js');
32
26
  enum_index.CommandType.CONTEXT_MENU,
33
27
  enum_index.CommandType.PRIMARY_ENTRY_POINT
34
28
  ]);
35
- /**
36
- * Whether an interaction is the kind the given command type handles.
37
- *
38
- * @param type - The command type declared on `@Command`.
39
- * @param interaction - The interaction being dispatched.
40
- */ function matchesCommandType(type, interaction) {
29
+ /** Whether an interaction is the kind the given command type handles. */ function matchesCommandType(type, interaction) {
41
30
  const matches = INTERACTION_MATCHERS[type];
42
31
  return matches !== undefined && matches(interaction);
43
32
  }
44
33
  /**
45
- * Whether the command type is routed by matching a customId pattern.
46
- *
47
- * Components carry an application-defined customId and so are matched by pattern;
48
- * commands carry a name Discord itself registered and are matched exactly.
49
- *
50
- * @param type - The command type declared on `@Command`.
34
+ * Whether a command type is routed by customId pattern: components carry an application-defined
35
+ * customId, while commands are matched by the name Discord registered.
51
36
  */ function isCustomIdRouted(type) {
52
37
  return !NAME_ROUTED_TYPES.has(type);
53
38
  }
54
- /**
55
- * Whether an interaction carries a customId, and so can be routed by pattern.
56
- *
57
- * @param interaction - The interaction being dispatched.
58
- */ function hasCustomId(interaction) {
39
+ /** Whether an interaction carries a customId, and so can be routed by pattern. */ function hasCustomId(interaction) {
59
40
  return interaction instanceof discord_js.MessageComponentInteraction || interaction instanceof discord_js.ModalSubmitInteraction;
60
41
  }
61
42
  /** Separates a command from its subcommand group and subcommand in a route key. */ const COMMAND_PATH_SEPARATOR = ' ';
62
43
  /**
63
- * The route keys a chat input interaction can be handled by, most specific first.
64
- *
65
- * Discord sends `/settings notify email` as one interaction named `settings`, so
66
- * routing on `commandName` alone gives every subcommand of a command the same handler
67
- * — and the framework would run whichever one was declared first. The full path is
68
- * tried before the bare name so a command can either split its subcommands across
69
- * methods or keep handling them in one, but never both by accident.
70
- *
71
- * A group is never dropped on the way down: `settings notify email` does not fall back
72
- * to `settings email`, because a second group could declare its own `email` and the
73
- * two would be indistinguishable.
74
- *
75
- * @param interaction - The chat input or autocomplete interaction being dispatched.
44
+ * The route keys a chat input interaction can be handled by, most specific first:
45
+ * `settings notify email`, then `settings`. Discord names the whole interaction `settings`, so the
46
+ * full path comes first; a group is never skipped, since two groups may share a subcommand name.
76
47
  * @returns The keys to look up, most specific first.
77
48
  */ function resolveCommandPaths(interaction) {
78
49
  const { commandName } = interaction;
@@ -99,24 +70,14 @@ var discord_js = require('discord.js');
99
70
  discord_js.ApplicationCommandOptionType.SubcommandGroup
100
71
  ]);
101
72
  /**
102
- * The value a handler should receive for one option.
103
- *
104
- * Discord sends entity options as a snowflake plus a `resolved` payload, and discord.js
105
- * puts that payload on the option as `user`/`role`/`channel`/`attachment`. Passing
106
- * `value` alone would hand the handler a bare id string for `@user`, forcing every
107
- * handler to re-fetch what the gateway already delivered.
73
+ * The value a handler receives for one option: the resolved user, role, channel or attachment
74
+ * discord.js attaches, rather than a bare snowflake to re-fetch.
108
75
  */ function resolveOptionValue(option) {
109
76
  return option.attachment ?? option.channel ?? option.role ?? option.user ?? option.member ?? option.value;
110
77
  }
111
78
  /**
112
- * Flattens a chat input interaction's options into the params record handlers receive.
113
- *
114
- * Subcommand and subcommand-group options are containers, not values — for
115
- * `/settings notify email true` the top level holds only `notify`. Recursing past them
116
- * means a subcommand handler sees `{ email: true }`, the same shape a flat command's
117
- * handler sees.
118
- *
119
- * @param interaction - The chat input or autocomplete interaction being dispatched.
79
+ * Flattens an interaction's options into the params record handlers receive, looking through
80
+ * subcommand and group containers, so `/settings notify email true` yields `{ email: true }`.
120
81
  * @returns Each supplied option keyed by name, with entity options resolved.
121
82
  */ function resolveOptionParams(interaction) {
122
83
  const data = interaction.options?.data;
@@ -135,14 +96,8 @@ var discord_js = require('discord.js');
135
96
  return params;
136
97
  }
137
98
  /**
138
- * The name of the option the user is currently typing, or `undefined`.
139
- *
140
- * `getFocused` throws when nothing is focused rather than returning null, and it is
141
- * absent altogether on a hand-built test double. Neither is worth failing a dispatch
142
- * over — an autocomplete with no focused option simply matches no option-specific
143
- * handler.
144
- *
145
- * @param interaction - The autocomplete interaction being dispatched.
99
+ * The name of the option the user is typing, or undefined when nothing is focused; `getFocused`
100
+ * throws then, and hand-built test doubles lack it entirely.
146
101
  */ function focusedOptionName(interaction) {
147
102
  if (typeof interaction.options?.getFocused !== 'function') return undefined;
148
103
  try {
@@ -254,21 +209,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
254
209
  /** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
255
210
  /** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
256
211
  /**
257
- * Helper function to create regex and parameter mappings from a pattern string.
258
- *
259
- * A `{name}` matches anything up to the next `/`, the same rule Express and Rails use
260
- * for a path segment. That is what lets a value the application does not control — a
261
- * uuid, an opaque vendor id, a slug — be captured whole without the author annotating
262
- * anything, since a hyphen inside it is data rather than structure.
263
- *
264
- * It also keeps neighbouring patterns apart: `profile/{uuid}` and `profile/{uuid}/{id}`
265
- * cannot both match one id, because a parameter cannot swallow the separator between
266
- * them. Patterns separated by `-` instead have no such boundary, so a pair like
267
- * `profile-{uuid}` and `profile-{uuid}-{id}` is ambiguous — {@link findAmbiguousRoutes}
268
- * reports those at registration.
269
- *
270
- * @param pattern - The pattern string to parse.
271
- * @returns The regex, the parameter names, and how specific the pattern is.
212
+ * Compiles a pattern into a regex, its parameter names and its specificity. A `{name}` matches up to
213
+ * the next `/`, so a uuid is captured whole and `profile/{uuid}` never overlaps `profile/{uuid}/{id}`;
214
+ * `-`-separated patterns can, which {@link findAmbiguousRoutes} reports at registration.
272
215
  */ function createRegexFromPattern(pattern) {
273
216
  const params = [];
274
217
  let regexPattern = '';
@@ -405,19 +348,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
405
348
  return Reflect.getMetadata(COMMAND_METADATA_KEY, controller);
406
349
  }
407
350
  /**
408
- * Decorator to register an autocomplete handler for a chat input command's option.
351
+ * Registers an autocomplete handler for an option of a chat input command.
409
352
  *
410
- * Autocomplete is a separate interaction from the command it belongs to, and Discord
411
- * sends it while the user is still typing. It is not a `@Command`: nothing is
412
- * registered for it — the option's own `setAutocomplete(true)` is what turns it on —
413
- * and it is answered with `interaction.respond()` rather than a reply. Leaving it
414
- * unhandled is not silent to the user: the client shows a loading state until the
415
- * three-second window closes.
353
+ * Enable it on the option with `setAutocomplete(true)` and answer with `interaction.respond()`.
416
354
  *
417
- * @param commandPath - The command to complete, e.g. `settings` or `settings notify email`
418
- * for a subcommand. Parts are separated by a single space, as Discord displays them.
419
- * @param optionName - The option to complete. Omit to handle every option of the command,
420
- * branching on `interaction.options.getFocused(true)`.
355
+ * @param commandPath - The command, such as `search` or `settings notify email` for a subcommand.
356
+ * @param optionName - The option to complete. Omit to handle every option, branching on
357
+ * `interaction.options.getFocused(true)`.
421
358
  *
422
359
  * @example
423
360
  * ```typescript
@@ -439,13 +376,8 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
439
376
  };
440
377
  }
441
378
  /**
442
- * Retrieves autocomplete handler metadata from a given controller.
443
- *
444
- * Handlers naming an option come first, so a command-wide handler acts as the fallback
445
- * for options no specific handler claimed rather than shadowing them by declaration order.
446
- *
447
- * @param controller - The controller class instance.
448
- * @returns The registered autocomplete handlers, most specific first.
379
+ * Returns a controller's autocomplete handlers, option-specific ones first.
380
+ * @param controller - The controller instance.
449
381
  */ function getAutocompleteHandlers(controller) {
450
382
  const handlers = Reflect.getMetadata(AUTOCOMPLETE_METADATA_KEY, controller) || [];
451
383
  return [
@@ -453,7 +385,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
453
385
  ].sort((a, b)=>Number(Boolean(b.optionName)) - Number(Boolean(a.optionName)));
454
386
  }
455
387
  /**
456
- * Decorator to mark a class as a controller that can later be registered to the App class `(app.ts)` using the `@MeoCord` decorator.
388
+ * Marks a class as a controller, to be listed in `@MeoCord({ controllers })`.
457
389
  *
458
390
  * @example
459
391
  * ```typescript
@@ -463,8 +395,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
463
395
  *
464
396
  * @Command('ping', PingCommandBuilder)
465
397
  * async ping(interaction: ChatInputCommandInteraction) {
466
- * const response = await this.pingService.handlePing()
467
- * await interaction.reply(response)
398
+ * await interaction.reply(await this.pingService.handlePing())
468
399
  * }
469
400
  * }
470
401
  * ```
@@ -476,15 +407,8 @@ const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
476
407
  };
477
408
  }
478
409
  /**
479
- * Finds pattern pairs that can both match one customId.
480
- *
481
- * Patterns of different segment counts are disjoint, because a parameter cannot cross
482
- * `/`. Within the same count, two patterns overlap unless some position holds literals
483
- * that differ: `a/{x}/c` and `a/b/{y}` both take `a/b/c`, and neither is more literal
484
- * than the other, so ranking cannot settle it either.
485
- *
486
- * @param patterns - The registered patterns.
487
- * @returns Each ambiguous pair, once, in the order the patterns were given.
410
+ * Finds pairs of customId patterns that can both match one id, such as `a/{x}/c` and `a/b/{y}`.
411
+ * @returns Each ambiguous pair once, in the order the patterns were given.
488
412
  */ function findAmbiguousRoutes(patterns) {
489
413
  const isParam = (segment)=>PLACEHOLDER_PATTERN.test(segment);
490
414
  const segmentsOf = (pattern)=>pattern.split(PARAM_SEPARATOR);
@@ -8,7 +8,6 @@
8
8
  */ var MetadataKey = /*#__PURE__*/ function(MetadataKey) {
9
9
  /**
10
10
  * Set by Inversify 8's `injectable()` decorator.
11
- * Renamed from the legacy `'inversify:injectable'` string used in older versions.
12
11
  */ MetadataKey["Injectable"] = "@inversifyjs/core/classIsInjectableFlagReflectKey";
13
12
  /**
14
13
  * Stores the Inversify `Container` instance on a controller class.
@@ -13,17 +13,9 @@ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentS
13
13
  let cachedConfig;
14
14
  let configLoaded = false;
15
15
  /**
16
- * Loads the configuration a built application runs with: `dist/meocord.config.mjs`, which
17
- * `meocord build` compiles from `meocord.config.ts`.
18
- *
19
- * Reads only the compiled file, which is plain JavaScript and needs no transpiler, tsconfig or
20
- * source. Reading `meocord.config.ts` is {@link loadMeoCordSourceConfig}'s job, and it lives in its
21
- * own module because it needs jiti: this one is imported by the logger and the factory, so a
22
- * bot built with `bundleDependencies` bundles whatever it imports.
23
- *
24
- * The result is cached after the first load.
25
- *
26
- * @returns The configuration, or undefined when there is no compiled config or it fails to load.
16
+ * Loads the configuration a built application runs with, `dist/meocord.config.mjs`, and caches it.
17
+ * Imports no transpiler, since the logger and the factory import this module into every bot.
18
+ * @returns The configuration, or undefined when the compiled config is missing or fails to load.
27
19
  */ function loadMeoCordConfig() {
28
20
  if (configLoaded) return cachedConfig;
29
21
  configLoaded = true;
@@ -39,7 +31,7 @@ let configLoaded = false;
39
31
  try {
40
32
  // Called through a variable so a bundler does not try to resolve the path while building the
41
33
  // application; the file is only there once the build is done.
42
- const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/theme-Dqwlnz0m.cjs', document.baseURI).href)));
34
+ const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/theme-C8w8ivsR.cjs', document.baseURI).href)));
43
35
  const loaded = load(compiledPath);
44
36
  return loaded.default ?? loaded;
45
37
  } catch (error) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var theme = require('../_shared/theme-Dqwlnz0m.cjs');
3
+ var theme = require('../_shared/theme-C8w8ivsR.cjs');
4
4
  require('node:util');
5
5
  require('dayjs');
6
6
  require('dayjs/plugin/utc.js');
@@ -3,14 +3,14 @@
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
5
  var discord_js = require('discord.js');
6
- var theme = require('../_shared/theme-Dqwlnz0m.cjs');
7
- var controller_decorator = require('../_shared/controller.decorator-9nIxOp9y.cjs');
6
+ var theme = require('../_shared/theme-C8w8ivsR.cjs');
7
+ var controller_decorator = require('../_shared/controller.decorator-CqgGgjj3.cjs');
8
8
  var lodashEs = require('lodash-es');
9
9
  var enum_index = require('../enum/index.cjs');
10
10
  var Table = require('cli-table3');
11
11
  var fs = require('fs');
12
12
  var path = require('path');
13
- var metadataKey_enum = require('../_shared/metadata-key.enum-aABGdodb.cjs');
13
+ var metadataKey_enum = require('../_shared/metadata-key.enum-DG7cVMc4.cjs');
14
14
  require('node:util');
15
15
  require('dayjs');
16
16
  require('dayjs/plugin/utc.js');
@@ -66,20 +66,8 @@ const createErrorEmbed = (description)=>{
66
66
  }
67
67
  class MeoCordApp {
68
68
  /**
69
- * Runs an event handler so a failure inside it cannot take the process down.
70
- *
71
- * discord.js calls listeners without awaiting them, so a rejection escaping one has
72
- * nothing left to settle it: Node reports an unhandled rejection, which terminates
73
- * the process by default. Losing the whole bot because one reaction landed on a
74
- * deleted message, or one controller could not be resolved, is a worse failure than
75
- * the one that caused it -- every other user is served by the same process.
76
- *
77
- * Nothing is silenced. The error is logged against the event that produced it, so a
78
- * genuine misconfiguration -- an unbound controller, a missing dependency -- shows
79
- * up on the very first interaction rather than staying hidden.
80
- *
81
- * @param event - The gateway event being handled, named in the log.
82
- * @param run - The handler to run.
69
+ * Runs an event handler so its failure is logged against the event instead of surfacing as an
70
+ * unhandled rejection, which would terminate the whole bot.
83
71
  */ async runListener(event, run) {
84
72
  try {
85
73
  await run();
@@ -107,14 +95,20 @@ class MeoCordApp {
107
95
  return this.controllerInstancesCache.get(controllerClass);
108
96
  }
109
97
  /**
110
- * Registers the Discord event handlers and logs in.
98
+ * Registers the Discord event handlers and logs the bot in.
99
+ *
100
+ * If the login fails, the process exit code is set to `1` before the promise rejects, so the
101
+ * process exits non-zero even when the caller catches the error to log it. A later `start()`
102
+ * that logs in clears that code again.
103
+ *
104
+ * @returns A promise that resolves once the bot is logged in.
105
+ * @throws The login error, such as an invalid token or Discord being unreachable.
111
106
  *
112
- * Rejects when the login fails -- an invalid token, or Discord unreachable -- and sets the exit
113
- * code to 1 first. A bot that never came online is a failed start, and a supervisor such as
114
- * Docker's `restart: on-failure` or systemd can only tell if the process says so. The exit code is
115
- * set here rather than left to the entry point because an entry point that catches the rejection
116
- * to log it has handled it, and the process would otherwise end with 0. A later `start()` that
117
- * logs in -- an entry point retrying -- clears the code again, if this is what set it.
107
+ * @example
108
+ * ```ts
109
+ * const app = MeoCordFactory.create(App)
110
+ * await app.start()
111
+ * ```
118
112
  */ async start() {
119
113
  this.logger.log('Starting bot...');
120
114
  this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
@@ -147,12 +141,8 @@ class MeoCordApp {
147
141
  this.logger.log('Bot is online!');
148
142
  }
149
143
  async registerCommands() {
150
- // Keyed by type and name together, because that pair is what Discord treats as one
151
- // command: a user context menu and a message context menu are free to share a name,
152
- // and keying on the name alone would drop one of them from the payload. Within a
153
- // single type the name is unique, so a command whose subcommands live in separate
154
- // methods still contributes its builder once — sending it twice makes Discord
155
- // reject the whole payload.
144
+ // Keyed by type and name: Discord treats that pair as one command, so a user and a message context
145
+ // menu may share a name, and a command split across methods sends its builder once.
156
146
  const buildersByCommand = new Map();
157
147
  for (const controllerClass of this.controllerClasses){
158
148
  const instance = this.getInstance(controllerClass);
@@ -265,13 +255,8 @@ class MeoCordApp {
265
255
  return routes;
266
256
  }
267
257
  /**
268
- * Dispatches an interaction, and makes sure a failure anywhere in that still reaches
269
- * the person who triggered it.
270
- *
271
- * {@link executeCommand} already reports what a handler throws, but everything
272
- * *before* the handler can fail too — resolving a controller through the container
273
- * is the common case — and a component that fails there would otherwise look dead
274
- * with nothing said to the user and nothing in the log.
258
+ * Dispatches an interaction, reporting a failure before the handler, such as resolving the
259
+ * controller, to the user and the log as well.
275
260
  */ async handleInteraction(interaction) {
276
261
  try {
277
262
  await this.dispatchInteraction(interaction);
@@ -344,12 +329,8 @@ class MeoCordApp {
344
329
  return [];
345
330
  }
346
331
  /**
347
- * Answers an autocomplete interaction from the `@Autocomplete` handler that claims it.
348
- *
349
- * Discord closes the window after three seconds and shows a loading state until
350
- * something arrives, so an unclaimed option is answered with an empty list rather
351
- * than left to time out -- a visibly empty menu is a better failure than a stuck one,
352
- * and the warning says which option is missing a handler.
332
+ * Answers an autocomplete interaction from the `@Autocomplete` handler that claims it. An unclaimed
333
+ * option gets an empty list and a warning, rather than a menu stuck loading until Discord times out.
353
334
  */ async handleAutocomplete(interaction) {
354
335
  const focusedName = controller_decorator.focusedOptionName(interaction);
355
336
  for (const path of controller_decorator.resolveCommandPaths(interaction)){
@@ -402,12 +383,8 @@ class MeoCordApp {
402
383
  }
403
384
  }
404
385
  /**
405
- * Tells the user something went wrong, if the interaction can still hear it.
406
- *
407
- * A handler that replies and *then* throws is the common shape of a failure, and
408
- * replying twice throws in turn -- out of the catch block, where nothing is left to
409
- * handle it. Whatever the interaction's state, reporting an error must not be able
410
- * to become a second, worse one.
386
+ * Tells the user something went wrong, if the interaction can still take a reply. Never throws, since
387
+ * a handler that already replied would otherwise turn one error into two.
411
388
  */ async replyWithError(interaction, message) {
412
389
  if (!interaction.isRepliable() || interaction.replied || interaction.deferred) return;
413
390
  try {
@@ -561,14 +538,9 @@ class MeoCordApp {
561
538
  return !built.libc || !running.libc || built.libc === running.libc;
562
539
  }
563
540
  /**
564
- * Stops a bundle from starting on a platform its native addons were not built for.
565
- *
566
- * A binary for the wrong platform does not fail at startup. It fails when something first loads
567
- * it -- for an image library, the first command that renders one -- with a dynamic linker error
568
- * that names a file rather than the cause. Checking the manifest up front turns that into one
569
- * clear message before the bot goes online.
570
- *
571
- * @param distDir - Directory holding the manifest. Defaults to the directory of the entry script.
541
+ * Stops a bundle from starting on a platform its native addons were not built for, with one clear
542
+ * message instead of a linker error on the first command that loads an addon.
543
+ * @param distDir - Directory holding the manifest. Defaults to the entry script's directory.
572
544
  */ function assertBuiltForThisPlatform(distDir = process.argv[1] ? path.dirname(process.argv[1]) : undefined) {
573
545
  if (!distDir) return;
574
546
  const manifest = path.join(distDir, PLATFORM_MANIFEST);