meocord 4.0.0-beta.2 → 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 (60) hide show
  1. package/CHANGELOG.md +22 -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 +43 -55
  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/src/main.ts.template +1 -5
  13. package/dist/esm/bin/app-template/vitest.config.ts.template +3 -5
  14. package/dist/esm/bin/helper/app-generator.helper.js +6 -24
  15. package/dist/esm/bin/helper/controller-generator.helper.js +8 -35
  16. package/dist/esm/bin/helper/guard-generator.helper.js +2 -7
  17. package/dist/esm/bin/helper/service-generator.helper.js +2 -7
  18. package/dist/esm/bin/meocord.js +9 -31
  19. package/dist/esm/build/native-addons.js +10 -33
  20. package/dist/esm/build/rsbuild-config.js +8 -26
  21. package/dist/esm/core/meocord.app.js +37 -44
  22. package/dist/esm/decorator/app.decorator.js +10 -25
  23. package/dist/esm/decorator/command-builder.decorator.js +6 -9
  24. package/dist/esm/decorator/controller.decorator.js +14 -45
  25. package/dist/esm/decorator/guard.decorator.js +14 -56
  26. package/dist/esm/decorator/index.js +1 -1
  27. package/dist/esm/decorator/service.decorator.js +4 -8
  28. package/dist/esm/enum/controller.enum.js +3 -7
  29. package/dist/esm/enum/metadata-key.enum.js +0 -1
  30. package/dist/esm/package.json.js +1 -1
  31. package/dist/esm/testing/meocord-testing-module.js +9 -5
  32. package/dist/esm/testing/mock-fn.js +14 -25
  33. package/dist/esm/testing/mock-interaction.js +58 -109
  34. package/dist/esm/util/common.util.js +5 -22
  35. package/dist/esm/util/generator-cli.util.js +13 -51
  36. package/dist/esm/util/interaction.util.js +15 -60
  37. package/dist/esm/util/json.util.js +1 -9
  38. package/dist/esm/util/meocord-cli.util.js +4 -31
  39. package/dist/esm/util/meocord-config-loader.util.js +3 -11
  40. package/dist/esm/util/meocord-source-config.util.js +2 -9
  41. package/dist/esm/util/package-manager.util.js +2 -6
  42. package/dist/esm/util/package-version.util.js +2 -11
  43. package/dist/esm/util/platform.util.js +3 -8
  44. package/dist/esm/util/runtime.util.js +10 -38
  45. package/dist/esm/util/tsconfig.util.js +4 -9
  46. package/dist/types/{controller.enum-DYfhYaat.d.ts → controller.enum-BL6wEQls.d.cts} +3 -7
  47. package/dist/types/{controller.enum-DYfhYaat.d.cts → controller.enum-BL6wEQls.d.ts} +3 -7
  48. package/dist/types/core/index.d.cts +27 -50
  49. package/dist/types/core/index.d.ts +27 -50
  50. package/dist/types/decorator/index.d.cts +45 -168
  51. package/dist/types/decorator/index.d.ts +45 -168
  52. package/dist/types/enum/index.d.cts +1 -2
  53. package/dist/types/enum/index.d.ts +1 -2
  54. package/dist/types/interface/index.d.cts +49 -58
  55. package/dist/types/interface/index.d.ts +49 -58
  56. package/dist/types/testing/index.d.cts +85 -87
  57. package/dist/types/testing/index.d.ts +85 -87
  58. package/meocord.eslint.cjs +1 -1
  59. package/meocord.eslint.mjs +1 -1
  60. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
12
+ ## 4.0.0-beta.3
13
+
14
+ ### Patch Changes
15
+
16
+ - [#40](https://github.com/l7aromeo/meocord/pull/40) [`3629edd`](https://github.com/l7aromeo/meocord/commit/3629eddaf20bd0947d24e5582aa167b2c0ca0d47) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Lint `meocord.config.ts`. The shared ESLint config from `meocord/eslint` ignored it, so the config
17
+ alone skipped the rules every other file follows — unused imports, formatting. It is linted like the
18
+ rest now; the typecheck it already gets is unchanged.
19
+
20
+ - [#40](https://github.com/l7aromeo/meocord/pull/40) [`80b8302`](https://github.com/l7aromeo/meocord/commit/80b83021894ef8a964a12ac1610c2ec9b40f344e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Exit with code 1 when the login fails, whatever the entry point does. `app.start()` now sets
21
+ `process.exitCode = 1` before rejecting, so an existing `main.ts` whose `catch` only logs the error no
22
+ longer exits 0 — no `process.exitCode = 1` needs adding to it, and new applications' `main.ts` no
23
+ longer carries one.
24
+
3
25
  ## 4.0.0-beta.2
4
26
 
5
27
  ### Major 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,11 +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.
111
103
  *
112
- * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
113
- * stop with a non-zero exit code. A bot that never came online is a failed start, and a
114
- * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
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.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const app = MeoCordFactory.create(App)
110
+ * await app.start()
111
+ * ```
115
112
  */ async start() {
116
113
  this.logger.log('Starting bot...');
117
114
  this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
@@ -128,16 +125,24 @@ class MeoCordApp {
128
125
  user,
129
126
  action: enum_index.ReactionHandlerAction.REMOVE
130
127
  })));
131
- await this.bot.login(this.discordToken);
128
+ try {
129
+ await this.bot.login(this.discordToken);
130
+ } catch (error) {
131
+ if (process.exitCode === undefined || process.exitCode === 0) {
132
+ process.exitCode = 1;
133
+ MeoCordApp.failedLoginSetExitCode = true;
134
+ }
135
+ throw error;
136
+ }
137
+ if (MeoCordApp.failedLoginSetExitCode && process.exitCode === 1) {
138
+ process.exitCode = undefined;
139
+ MeoCordApp.failedLoginSetExitCode = false;
140
+ }
132
141
  this.logger.log('Bot is online!');
133
142
  }
134
143
  async registerCommands() {
135
- // Keyed by type and name together, because that pair is what Discord treats as one
136
- // command: a user context menu and a message context menu are free to share a name,
137
- // and keying on the name alone would drop one of them from the payload. Within a
138
- // single type the name is unique, so a command whose subcommands live in separate
139
- // methods still contributes its builder once — sending it twice makes Discord
140
- // 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.
141
146
  const buildersByCommand = new Map();
142
147
  for (const controllerClass of this.controllerClasses){
143
148
  const instance = this.getInstance(controllerClass);
@@ -250,13 +255,8 @@ class MeoCordApp {
250
255
  return routes;
251
256
  }
252
257
  /**
253
- * Dispatches an interaction, and makes sure a failure anywhere in that still reaches
254
- * the person who triggered it.
255
- *
256
- * {@link executeCommand} already reports what a handler throws, but everything
257
- * *before* the handler can fail too — resolving a controller through the container
258
- * is the common case — and a component that fails there would otherwise look dead
259
- * 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.
260
260
  */ async handleInteraction(interaction) {
261
261
  try {
262
262
  await this.dispatchInteraction(interaction);
@@ -329,12 +329,8 @@ class MeoCordApp {
329
329
  return [];
330
330
  }
331
331
  /**
332
- * Answers an autocomplete interaction from the `@Autocomplete` handler that claims it.
333
- *
334
- * Discord closes the window after three seconds and shows a loading state until
335
- * something arrives, so an unclaimed option is answered with an empty list rather
336
- * than left to time out -- a visibly empty menu is a better failure than a stuck one,
337
- * 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.
338
334
  */ async handleAutocomplete(interaction) {
339
335
  const focusedName = controller_decorator.focusedOptionName(interaction);
340
336
  for (const path of controller_decorator.resolveCommandPaths(interaction)){
@@ -387,12 +383,8 @@ class MeoCordApp {
387
383
  }
388
384
  }
389
385
  /**
390
- * Tells the user something went wrong, if the interaction can still hear it.
391
- *
392
- * A handler that replies and *then* throws is the common shape of a failure, and
393
- * replying twice throws in turn -- out of the catch block, where nothing is left to
394
- * handle it. Whatever the interaction's state, reporting an error must not be able
395
- * 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.
396
388
  */ async replyWithError(interaction, message) {
397
389
  if (!interaction.isRepliable() || interaction.replied || interaction.deferred) return;
398
390
  try {
@@ -508,6 +500,7 @@ class MeoCordApp {
508
500
  process.on('SIGTERM', ()=>this.gracefulShutdown());
509
501
  }
510
502
  }
503
+ /** Whether a failed login set the process exit code, so a later successful one knows to clear it. */ MeoCordApp.failedLoginSetExitCode = false;
511
504
 
512
505
  /**
513
506
  * File written beside a bundle that carries native addons, naming the platform it was built for.
@@ -545,14 +538,9 @@ class MeoCordApp {
545
538
  return !built.libc || !running.libc || built.libc === running.libc;
546
539
  }
547
540
  /**
548
- * Stops a bundle from starting on a platform its native addons were not built for.
549
- *
550
- * A binary for the wrong platform does not fail at startup. It fails when something first loads
551
- * it -- for an image library, the first command that renders one -- with a dynamic linker error
552
- * that names a file rather than the cause. Checking the manifest up front turns that into one
553
- * clear message before the bot goes online.
554
- *
555
- * @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.
556
544
  */ function assertBuiltForThisPlatform(distDir = process.argv[1] ? path.dirname(process.argv[1]) : undefined) {
557
545
  if (!distDir) return;
558
546
  const manifest = path.join(distDir, PLATFORM_MANIFEST);