meocord 4.1.2 → 4.2.1

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 (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -2,12 +2,13 @@ import 'reflect-metadata';
2
2
  import { UserContextMenuCommandInteraction, ApplicationCommandType, MessageContextMenuCommandInteraction } from 'discord.js';
3
3
  import { CommandType } from '../enum/controller.enum.js';
4
4
  import { MetadataKey } from '../enum/metadata-key.enum.js';
5
- import { matchesCommandType, interactionClassName, isCustomIdRouted } from '../util/interaction.util.js';
5
+ import { isCustomIdRouted, matchesCommandType, interactionClassName } from '../util/interaction.util.js';
6
+ import { wrapsMethod } from '../util/method-wrapper.util.js';
6
7
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
7
8
  import { Logger } from '../common/logger.js';
8
9
  import { routeSpecificity } from '../core/route-specificity.js';
9
- import { isSegmentType, choicesOf, lookupTable, parseSegment } from '../core/scalar-types.js';
10
- import { refuseOnClass, refuse } from '../util/refusal.util.js';
10
+ import { lookupTable, isSegmentType, choicesOf, parseSegment } from '../core/scalar-types.js';
11
+ import { declaring, refuse, refuseOnClass } from '../util/refusal.util.js';
11
12
  import { withArticle, describeValue } from '../util/value.util.js';
12
13
  import { META } from '../util/metadata-keys.js';
13
14
 
@@ -69,19 +70,60 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
69
70
  else handlers[index] = entry;
70
71
  Reflect.defineMetadata(key, handlers, target);
71
72
  }
72
- /** The class's own command map, started from a copy of the inherited one, for the same reason. */ function ownCommandMap(target) {
73
+ /**
74
+ * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
75
+ * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
76
+ */ function dropInheritedRoutes(prototype) {
77
+ const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
78
+ if (redeclared.size === 0) return;
79
+ const base = Object.getPrototypeOf(prototype);
80
+ // An entry the class takes from its base, for a method it re-declares
81
+ const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
82
+ const commands = ownCommandMap(prototype);
83
+ const baseCommands = Reflect.getMetadata(META.commands, base) ?? Object.create(null);
84
+ for (const [name, metas] of Object.entries(commands)){
85
+ const kept = metas.filter((meta)=>!inheritedFor(commandsNamed(baseCommands, name), (entry)=>entry.methodName)(meta));
86
+ if (kept.length > 0) commands[name] = kept;
87
+ else delete commands[name];
88
+ }
89
+ Reflect.defineMetadata(META.commands, commands, prototype);
90
+ const lists = [
91
+ [
92
+ META.messageHandlers,
93
+ (entry)=>entry.method
94
+ ],
95
+ [
96
+ META.reactionHandlers,
97
+ (entry)=>entry.method
98
+ ],
99
+ [
100
+ META.autocompleteHandlers,
101
+ (entry)=>entry.methodName
102
+ ]
103
+ ];
104
+ for (const [key, method] of lists){
105
+ const inherited = Reflect.getMetadata(key, base) ?? [];
106
+ Reflect.defineMetadata(key, ownHandlerList(key, prototype).filter((entry)=>!inheritedFor(inherited, method)(entry)), prototype);
107
+ }
108
+ }
109
+ /**
110
+ * The class's own command map, started from a copy of the inherited one, for the same reason. Keyed by command name
111
+ * without a prototype, so a command named like an inherited key, such as `constructor`, is a key like any other.
112
+ */ function ownCommandMap(target) {
73
113
  const own = Reflect.getOwnMetadata(META.commands, target);
74
114
  if (own) return own;
75
115
  const inherited = Reflect.getMetadata(META.commands, target) ?? {};
76
- return Object.fromEntries(Object.entries(inherited).map(([name, metas])=>[
77
- name,
78
- [
79
- ...metas
80
- ]
81
- ]));
116
+ const copy = Object.create(null);
117
+ for (const [name, metas] of Object.entries(inherited))copy[name] = [
118
+ ...metas
119
+ ];
120
+ return copy;
121
+ }
122
+ /** The handlers a command map holds for `name`: its own entry only, never one `Object.prototype` holds. */ function commandsNamed(map, name) {
123
+ return map && Object.hasOwn(map, name) ? map[name] : [];
82
124
  }
83
125
  function MessageHandler(pattern, options = {}) {
84
- return function(target, propertyKey) {
126
+ return declaring(function(target, propertyKey) {
85
127
  refuseOnClass('@MessageHandler', target, propertyKey);
86
128
  // An empty pattern means every message, as no pattern does
87
129
  if (pattern === '') {
@@ -95,7 +137,7 @@ function MessageHandler(pattern, options = {}) {
95
137
  };
96
138
  addOwnHandler(META.messageHandlers, target, declared, (other)=>other.method === method && other.pattern === declared.pattern);
97
139
  declareRoute(target, messageRoute(method, declared.pattern));
98
- };
140
+ });
99
141
  }
100
142
  function ReactionHandler(emojiOrSettings, settings = {}) {
101
143
  const [emoji, own] = typeof emojiOrSettings === 'object' ? [
@@ -105,7 +147,7 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
105
147
  emojiOrSettings,
106
148
  settings
107
149
  ];
108
- return function(target, propertyKey) {
150
+ return declaring(function(target, propertyKey) {
109
151
  refuseOnClass('@ReactionHandler', target, propertyKey);
110
152
  const method = propertyKey.toString();
111
153
  addOwnHandler(META.reactionHandlers, target, {
@@ -114,7 +156,7 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
114
156
  settings: own
115
157
  }, (other)=>other.method === method && other.emoji === emoji);
116
158
  declareRoute(target, reactionRoute(method, emoji));
117
- };
159
+ });
118
160
  }
119
161
  /** A custom emoji as Discord writes it in a message: `<:name:id>`, or `<a:name:id>` for an animated one. */ const CUSTOM_EMOJI = /^<a?:\w+:(\d+)>$/;
120
162
  /**
@@ -143,12 +185,24 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
143
185
  }
144
186
  // `{name}`, or `{name:type}` with the type read as far as the brace, so a type no segment can hold is named
145
187
  const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
188
+ // The whole of a brace pair that is a param, as PLACEHOLDER_PATTERN reads one
189
+ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
190
+ /**
191
+ * Why a pattern's brace pairs that aren't params are matched as literal text, or undefined when it has none: a
192
+ * param's name is ASCII letters, digits and _, so `{café}` is text.
193
+ */ function literalBraces(pattern) {
194
+ const braces = (pattern.match(/\{[^{}]*}/g) ?? []).filter((brace)=>!WHOLE_PLACEHOLDER.test(brace));
195
+ if (braces.length === 0) return undefined;
196
+ const one = braces.length === 1;
197
+ return `${braces.join(', ')} ${one ? "isn't a param" : "aren't params"}, since a param's name is ASCII letters, digits and _; ` + `${one ? 'it is' : 'they are'} matched as literal text.`;
198
+ }
146
199
  /** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
200
+ /** The longest customId Discord accepts; the shortest is one character. */ const MAX_CUSTOM_ID_LENGTH = 100;
147
201
  /** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
148
202
  /**
149
- * Compiles a pattern into a regex, its parameter names, their types and its specificity. A `{name}` takes a
150
- * whole `/`-separated segment, so a uuid is captured whole and `profile/{uuid}` never overlaps
151
- * `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
203
+ * Compiles a pattern into a regex, its parameter names, their types, its 4.1 specificity and the length of the
204
+ * shortest customId it matches. A `{name}` takes a whole `/`-separated segment, so a uuid is captured whole and
205
+ * `profile/{uuid}` never overlaps `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
152
206
  */ function createRegexFromPattern(pattern) {
153
207
  const params = [];
154
208
  // By the param's own name, so one named like an inherited key, such as `__proto__`, keeps its type
@@ -169,6 +223,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
169
223
  if (literal !== '' && !literal.endsWith(PARAM_SEPARATOR) || after !== undefined && after !== PARAM_SEPARATOR) {
170
224
  throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} must occupy a whole segment, so it has to be ` + `preceded and followed by "${PARAM_SEPARATOR}" or by the ends of the pattern. ` + `Write "a${PARAM_SEPARATOR}{${param}}" rather than "a-{${param}}".`));
171
225
  }
226
+ // A regex group's name, which the engine would refuse with the compiled regex in place of the pattern
227
+ if (params.includes(param)) {
228
+ throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} appears twice; give each param its own name.`));
229
+ }
230
+ if (/^\d/.test(param)) {
231
+ throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} starts with a digit; start a param's name with a letter or _.`));
232
+ }
172
233
  if (type !== undefined) {
173
234
  if (!isSegmentType(type)) throw refuse(new Error(`Invalid pattern "${pattern}": ${segmentTypeProblem(param, type)}`));
174
235
  if (choicesOf(type)?.includes('')) {
@@ -186,25 +247,23 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
186
247
  literalLength += trailing.length;
187
248
  regexPattern += escapeLiteral(trailing);
188
249
  const regex = new RegExp(`^${regexPattern}$`);
189
- // Literal text is the signal: a pattern spelling out more of the id describes it
190
- // more exactly than one leaving it to a parameter. Fewer parameters breaks a tie
191
- // between equal-length patterns, then narrower typed params; the order the controllers
192
- // and handlers are listed in settles what is left, and the startup warning names the pair.
250
+ // What 4.1 ranked by, kept for code that reads CommandMeta.specificity; routing ranks segment by segment
193
251
  const specificity = routeSpecificity({
194
252
  literals: literalLength,
195
253
  params: params.length,
196
- typed: Object.values(types).reduce((sum, type)=>sum + narrowness(type), 0)
254
+ typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
197
255
  });
256
+ // Each param takes at least one character
198
257
  return {
199
258
  regex,
200
259
  params,
201
260
  types: lookupTable(types),
202
- specificity
261
+ specificity,
262
+ shortest: literalLength + params.length
203
263
  };
204
264
  }
205
- /** How few values a segment type takes, so a narrower type ranks first: words to choose from, then bool, int, number. */ const narrowness = (type)=>choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1;
206
265
  /** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
207
- const kinds = 'string, int, number, bool, or words to choose from such as {mode:on|off}';
266
+ const kinds = 'string, int, number, bool, snowflake, uuid, or words to choose from such as {mode:on|off}';
208
267
  if ([
209
268
  'member',
210
269
  'user',
@@ -228,10 +287,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
228
287
  * the keys the handler's params require are checked against the pattern or route, a typed segment's value against
229
288
  * its type, and a select menu's choices, such as `values: string[]`, against what discord.js gives; a key a pipe
230
289
  * produces is declared `Piped<T>`, and left to the pipe. Two component handlers of one type whose patterns match
231
- * exactly the same ids stop the bot at startup; patterns that only overlap are warned about, naming the one that
232
- * runs: the more specific, or between equally specific ones, the one listed first. In the next major version (5.0),
233
- * the one that spells out the first segment where two equally specific patterns differ runs instead, and the warning
234
- * names the pairs that changes. A context menu handler receives the kind its builder's `setType()` names, and one
290
+ * exactly the same ids stop the bot at startup. Patterns that both match an id rank segment by segment, left to
291
+ * right: at the first segment one spells out as literal text and the other leaves to a param, the literal one runs,
292
+ * so `profile/me/{section}` takes `profile/me/edit` from `profile/{userId}/edit`; between patterns that leaves tied,
293
+ * the narrower type at the first param where they differ. Two still tied are warned about as the bot starts, and the
294
+ * one listed first runs. A context menu handler receives the kind its builder's `setType()` names, and one
235
295
  * declaring the other kind fails to compile; when the compiler cannot tell the kind, `@Command` checks the parameter
236
296
  * type it emits as it applies. A subclass that re-declares an inherited handler on the same name or pattern takes its
237
297
  * own builder and options; on another it still answers the inherited one too, which the bot warns about as it
@@ -264,12 +324,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
264
324
  * @category Handlers
265
325
  * @see {@link CommandBuilder}
266
326
  * @see {@link route}
267
- * @see {@link https://meocord.dev/docs/4.1/slash-commands | Slash commands}
268
- * @see {@link https://meocord.dev/docs/4.1/components | Buttons, selects and modals}
327
+ * @see {@link https://meocord.dev/docs/4.2/slash-commands | Slash commands}
328
+ * @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
269
329
  */ function Command(name, builderOrType) {
270
330
  const commandName = typeof name === 'string' ? name : name.pattern;
271
- return function(target, propertyKey, _descriptor) {
272
- var _commands, _commandName;
331
+ return declaring(function(target, propertyKey, _descriptor) {
273
332
  refuseOnClass('@Command', target, propertyKey);
274
333
  const originalMethod = _descriptor.value;
275
334
  if (!originalMethod) {
@@ -277,7 +336,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
277
336
  }
278
337
  // Wrap original method for interaction type validation
279
338
  const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
280
- _descriptor.value = function(interaction, params) {
339
+ const checked = function(interaction, params) {
281
340
  if (!matchesCommandType(commandType, interaction)) {
282
341
  throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
283
342
  }
@@ -286,6 +345,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
286
345
  params
287
346
  ]);
288
347
  };
348
+ _descriptor.value = wrapsMethod(checked, originalMethod);
289
349
  // This class's own map, inherited routes included
290
350
  const commands = ownCommandMap(target);
291
351
  let builderInstance;
@@ -321,7 +381,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
321
381
  }));
322
382
  }
323
383
  // discord.js builders validate as they are set, and their errors name neither the command nor the field.
324
- throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` + `localizations, which Discord limits to 32 and 100 characters.`, {
384
+ throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` + 'localizations, which Discord limits to 32 characters for a name and 100 for a description. A name is letters, ' + 'numbers, `-` and `_`, lowercase where the script has case, and without spaces, in every locale.', {
325
385
  cause: error
326
386
  }));
327
387
  }
@@ -341,6 +401,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
341
401
  commandType = builderOrType;
342
402
  }
343
403
  if (isCustomIdRouted(commandType)) {
404
+ // A route warned as it was made
405
+ const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
406
+ if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
344
407
  let pattern;
345
408
  try {
346
409
  pattern = createRegexFromPattern(commandName);
@@ -349,10 +412,14 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
349
412
  cause: error
350
413
  }));
351
414
  }
352
- const { regex: generatedRegex, params, specificity: patternSpecificity } = pattern;
353
- regex = generatedRegex;
354
- dynamicParams = params;
355
- specificity = patternSpecificity;
415
+ regex = pattern.regex;
416
+ dynamicParams = pattern.params;
417
+ specificity = pattern.specificity;
418
+ // Warned rather than refused, so a bot that carries such a handler still starts
419
+ if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
420
+ const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
421
+ logger.warn(`${target.constructor.name}.${propertyKey}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
422
+ }
356
423
  }
357
424
  const declared = {
358
425
  methodName: propertyKey,
@@ -369,14 +436,15 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
369
436
  }
370
437
  };
371
438
  // One the class inherits for this method and route is replaced where it stands, so this one's options apply
372
- const metas = (_commands = commands)[_commandName = commandName] ?? (_commands[_commandName] = []);
373
- const inherited = getCommandMap(Object.getPrototypeOf(target))?.[commandName] ?? [];
439
+ if (!Object.hasOwn(commands, commandName)) commands[commandName] = [];
440
+ const metas = commands[commandName];
441
+ const inherited = commandsNamed(getCommandMap(Object.getPrototypeOf(target)), commandName);
374
442
  const replaced = metas.findIndex((meta)=>inherited.includes(meta) && meta.methodName === propertyKey && meta.type === commandType);
375
443
  if (replaced === -1) metas.push(declared);
376
444
  else metas[replaced] = declared;
377
445
  Reflect.defineMetadata(META.commands, commands, target);
378
446
  declareRoute(target, commandRoute(propertyKey, commandType, commandName));
379
- };
447
+ });
380
448
  }
381
449
  /** The interaction class Discord sends for each context menu kind. */ const CONTEXT_MENU_INTERACTIONS = new Map([
382
450
  [
@@ -448,9 +516,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
448
516
  * @group Decorators
449
517
  * @category Handlers
450
518
  * @see {@link Command}
451
- * @see {@link https://meocord.dev/docs/4.1/autocomplete | Autocomplete}
519
+ * @see {@link https://meocord.dev/docs/4.2/autocomplete | Autocomplete}
452
520
  */ function Autocomplete(commandPath, optionName) {
453
- return function(target, propertyKey, _descriptor) {
521
+ return declaring(function(target, propertyKey, _descriptor) {
454
522
  refuseOnClass('@Autocomplete', target, propertyKey);
455
523
  const methodName = propertyKey.toString();
456
524
  addOwnHandler(META.autocompleteHandlers, target, {
@@ -459,7 +527,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
459
527
  methodName
460
528
  }, (other)=>other.methodName === methodName && other.commandPath === commandPath && other.optionName === optionName);
461
529
  declareRoute(target, autocompleteRoute(methodName, commandPath, optionName));
462
- };
530
+ });
463
531
  }
464
532
  /**
465
533
  * Returns a controller's autocomplete handlers, option-specific ones first.
@@ -481,52 +549,63 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
481
549
  const match = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`).exec(segment);
482
550
  return match ? typeKey(match[2]) : undefined;
483
551
  }
484
- /** Whether a segment's two readings can both take one value: a literal, text, or a typed param. */ function segmentsOverlap(left, right) {
552
+ /**
553
+ * How few values a segment type takes, so a narrower type ranks first: words to choose from, then bool, uuid,
554
+ * snowflake, int, number, and text last. A snowflake is narrower than a number, which takes all its values.
555
+ */ const NARROWNESS = lookupTable({
556
+ bool: 5,
557
+ uuid: 4,
558
+ snowflake: 3,
559
+ int: 2,
560
+ number: 1,
561
+ '': 0
562
+ });
563
+ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
564
+ /**
565
+ * How narrow a pattern segment that is a param, `{name}` or `{name:type}`, is: from `0` for text to `4` for words to
566
+ * choose from. `undefined` for a segment of literal text.
567
+ */ function paramNarrowness(segment) {
568
+ const type = segmentParamType(segment);
569
+ return type === undefined ? undefined : narrowness(type);
570
+ }
571
+ /** A value of each scalar segment type: an int, a bool, a snowflake and a uuid; text takes any. */ const SAMPLE_VALUES = [
572
+ '1',
573
+ 'true',
574
+ '10000000000000000',
575
+ '00000000-0000-0000-0000-000000000000'
576
+ ];
577
+ /** A value both readings of a segment take: a literal, text, or a typed param; `undefined` when they share none. */ function sharedValue(left, right) {
485
578
  const [a, b] = [
486
579
  segmentParamType(left),
487
580
  segmentParamType(right)
488
581
  ];
489
- if (a === undefined && b === undefined) return left === right;
490
- if (a === undefined || b === undefined) {
491
- const [type, literal] = a === undefined ? [
492
- b,
582
+ const takes = (type, segment, value)=>type === undefined ? segment === value : type === '' || parseSegment(type, value) !== undefined;
583
+ // Every type takes one of these when it takes anything another reading takes: a literal, a listed word, or a value
584
+ // of each scalar type
585
+ const candidates = [
586
+ ...a === undefined ? [
493
587
  left
494
- ] : [
495
- a,
588
+ ] : [],
589
+ ...b === undefined ? [
496
590
  right
497
- ];
498
- return type === '' || parseSegment(type, literal) !== undefined;
499
- }
500
- if (a === '' || b === '' || a === b) return true;
501
- // Words to choose from overlap another type when one of them is a value of it
502
- const [wordsA, wordsB] = [
503
- choicesOf(a),
504
- choicesOf(b)
591
+ ] : [],
592
+ ...choicesOf(a ?? '') ?? [],
593
+ ...choicesOf(b ?? '') ?? [],
594
+ ...SAMPLE_VALUES
505
595
  ];
506
- if (wordsA) return wordsA.some((word)=>parseSegment(b, word) !== undefined);
507
- if (wordsB) return wordsB.some((word)=>parseSegment(a, word) !== undefined);
508
- // Of the scalar types, only a whole number and a number share values
509
- return a === 'int' && b === 'number' || a === 'number' && b === 'int';
596
+ return candidates.find((value)=>takes(a, left, value) && takes(b, right, value));
510
597
  }
511
598
  /**
512
- * Finds pairs of customId patterns that can both match one id, such as `a/{x}/c` and `a/b/{y}`.
513
- * @returns Each ambiguous pair once, in the order the patterns were given.
514
- */ function findAmbiguousRoutes(patterns) {
515
- const segmentsOf = (pattern)=>pattern.split(PARAM_SEPARATOR);
516
- const collisions = [];
517
- for(let i = 0; i < patterns.length; i++){
518
- for(let j = i + 1; j < patterns.length; j++){
519
- const left = segmentsOf(patterns[i]);
520
- const right = segmentsOf(patterns[j]);
521
- if (left.length !== right.length) continue;
522
- const disjoint = left.some((segment, index)=>!segmentsOverlap(segment, right[index]));
523
- if (!disjoint) collisions.push([
524
- patterns[i],
525
- patterns[j]
526
- ]);
527
- }
528
- }
529
- return collisions;
599
+ * A customId both patterns match, such as `ticket/1/close` for `ticket/{id}/close` and `ticket/{n:int}/close`;
600
+ * `undefined` when no id matches both.
601
+ */ function sharedCustomId(left, right) {
602
+ const [a, b] = [
603
+ left.split(PARAM_SEPARATOR),
604
+ right.split(PARAM_SEPARATOR)
605
+ ];
606
+ if (a.length !== b.length) return undefined;
607
+ const values = a.map((segment, index)=>sharedValue(segment, b[index]));
608
+ return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
530
609
  }
531
610
 
532
- export { Autocomplete, Command, MessageHandler, PARAM_SEPARATOR, ReactionHandler, createRegexFromPattern, findAmbiguousRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, matchesEmoji, ownHandlerList, patternShape };
611
+ export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
@@ -1,5 +1,7 @@
1
1
  import 'reflect-metadata';
2
- import { refuse, decoratedName } from '../util/refusal.util.js';
2
+ import { Logger } from '../common/logger.js';
3
+ import { declaring, refuse, decoratedName } from '../util/refusal.util.js';
4
+ import { describeValue } from '../util/value.util.js';
3
5
  import { META } from '../util/metadata-keys.js';
4
6
 
5
7
  function Cooldown(options) {
@@ -10,10 +12,11 @@ function Cooldown(options) {
10
12
  per,
11
13
  windowMs: Math.round(seconds * 1000)
12
14
  };
13
- return function(target, propertyKey) {
15
+ return declaring(function(target, propertyKey) {
14
16
  // Checked where it applies, so the refusal names the handler or controller
15
17
  const problem = cooldownProblem(seconds, uses, per, by);
16
18
  if (problem) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${problem}`));
19
+ for (const warning of cooldownWarnings(options))logger.warn(`${decoratedName(target, propertyKey)}: ${warning}`);
17
20
  // Decorators apply bottom-up; prepending keeps them in the order they read.
18
21
  if (propertyKey === undefined) {
19
22
  const existing = Reflect.getOwnMetadata(META.classCooldowns, target) ?? [];
@@ -28,12 +31,26 @@ function Cooldown(options) {
28
31
  ...existing
29
32
  ], target, propertyKey);
30
33
  }
31
- };
34
+ });
32
35
  }
33
36
  /**
34
37
  * The longest window, in seconds: half the furthest time a `Date` holds, 8.64e15 ms, so the moment a wait ends, now
35
38
  * plus the window, is a real date for a refusal's `retryAt` and the time it shows.
36
39
  */ const MAX_SECONDS = 4320000000000;
40
+ const logger = new Logger('Cooldown');
41
+ /**
42
+ * What a cooldown's options get wrong that the types refuse, yet still counts: a `bypass` that is not a function, which
43
+ * fails every call, and a `seconds` that is not a number, read as one. A falsy `bypass` means none.
44
+ */ function cooldownWarnings({ seconds, bypass }) {
45
+ const warnings = [];
46
+ if (bypass && typeof bypass !== 'function') {
47
+ warnings.push(`@Cooldown takes bypass as a function of the call, returning whether the call skips the cooldown, not ${describeValue(bypass)}; ` + 'every call it counts fails with it. In the next major version (5.0) it is refused.');
48
+ }
49
+ if (typeof seconds !== 'number') {
50
+ warnings.push(`@Cooldown takes seconds as a number, not ${describeValue(seconds)}, which it counts as a window of ` + `${Math.round(seconds * 1000)} ms. In the next major version (5.0) it is refused.`);
51
+ }
52
+ return warnings;
53
+ }
37
54
  /** Why a cooldown's options cannot be counted, or undefined when they can. */ function cooldownProblem(seconds, uses, per, by) {
38
55
  if (!(seconds >= 0.001 && seconds <= MAX_SECONDS)) return `@Cooldown needs a number of seconds from 0.001 to ${MAX_SECONDS}, not ${seconds}.`;
39
56
  if (!Number.isInteger(uses) || uses < 1) return `@Cooldown needs a whole number of uses of at least 1, not ${uses}.`;
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { nonInteractionHandler, deferMisuseError } from '../core/defer.js';
3
- import { refuseOnClass, refuse } from '../util/refusal.util.js';
3
+ import { declaring, refuseOnClass, refuse } from '../util/refusal.util.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
5
 
6
6
  /**
@@ -37,9 +37,9 @@ import { META } from '../util/metadata-keys.js';
37
37
  * @category Pipeline stages
38
38
  * @see {@link DeferOptions}
39
39
  * @see {@link respond}
40
- * @see {@link https://meocord.dev/docs/4.1/defer | Deferring}
40
+ * @see {@link https://meocord.dev/docs/4.2/defer | Deferring}
41
41
  */ function Defer(options = {}) {
42
- return (target, propertyKey)=>{
42
+ return declaring((target, propertyKey)=>{
43
43
  refuseOnClass('@Defer', target, propertyKey);
44
44
  const methodName = String(propertyKey);
45
45
  const kind = nonInteractionHandler(target, methodName);
@@ -47,7 +47,7 @@ import { META } from '../util/metadata-keys.js';
47
47
  Reflect.defineMetadata(META.deferOptions, {
48
48
  ...options
49
49
  }, target, methodName);
50
- };
50
+ });
51
51
  }
52
52
 
53
53
  export { Defer };
@@ -1,11 +1,11 @@
1
1
  import 'reflect-metadata';
2
2
  import { ownHandlerList } from './controller.decorator.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
- import { refuseOnClass } from '../util/refusal.util.js';
4
+ import { declaring, refuseOnClass } from '../util/refusal.util.js';
5
5
 
6
6
  function eventDecorator(event, once) {
7
7
  // Generic over the method, so it is checked against the event's arguments rather than required to match exactly
8
- return function(target, propertyKey, _descriptor) {
8
+ return declaring(function(target, propertyKey, _descriptor) {
9
9
  refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
10
10
  const handlers = ownHandlerList(META.eventHandlers, target);
11
11
  handlers.push({
@@ -14,7 +14,7 @@ function eventDecorator(event, once) {
14
14
  once
15
15
  });
16
16
  Reflect.defineMetadata(META.eventHandlers, handlers, target);
17
- };
17
+ });
18
18
  }
19
19
  /**
20
20
  * Handles a discord.js client event every time it is emitted, on a controller or a service.
@@ -42,7 +42,7 @@ function eventDecorator(event, once) {
42
42
  * @group Decorators
43
43
  * @category Handlers
44
44
  * @see {@link Once}
45
- * @see {@link https://meocord.dev/docs/4.1/gateway-events | Gateway events}
45
+ * @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
46
46
  */ function On(event) {
47
47
  return eventDecorator(event, false);
48
48
  }
@@ -69,7 +69,7 @@ function eventDecorator(event, once) {
69
69
  * @group Decorators
70
70
  * @category Handlers
71
71
  * @see {@link On}
72
- * @see {@link https://meocord.dev/docs/4.1/gateway-events | Gateway events}
72
+ * @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
73
73
  */ function Once(event) {
74
74
  return eventDecorator(event, true);
75
75
  }
@@ -5,7 +5,7 @@ import { Logger } from '../common/logger.js';
5
5
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
6
6
  import { isConstructor, describeValue } from '../util/value.util.js';
7
7
  import { META } from '../util/metadata-keys.js';
8
- import { refuseOnMethod } from '../util/refusal.util.js';
8
+ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
9
9
 
10
10
  /**
11
11
  * Marks a class as an exception filter for the given error types.
@@ -34,9 +34,9 @@ import { refuseOnMethod } from '../util/refusal.util.js';
34
34
  * @group Decorators
35
35
  * @category Pipeline stages
36
36
  * @see {@link UseFilter}
37
- * @see {@link https://meocord.dev/docs/4.1/exception-filters | Exception filters}
37
+ * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
38
38
  */ function Catch(...errorTypes) {
39
- return function(target, propertyKey) {
39
+ return declaring(function(target, propertyKey) {
40
40
  refuseOnMethod('@Catch', target, propertyKey);
41
41
  makeInjectable(target);
42
42
  errorTypes.forEach((type, index)=>{
@@ -45,7 +45,7 @@ import { refuseOnMethod } from '../util/refusal.util.js';
45
45
  warnDeprecatedBehaviour(logger, `${target.name}: @Catch's ${entry}, ${describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
46
46
  });
47
47
  Reflect.defineMetadata(META.catchTypes, errorTypes, target);
48
- };
48
+ });
49
49
  }
50
50
  const logger = new Logger('Catch');
51
51
  const ORDINALS = [
@@ -85,9 +85,9 @@ const ORDINALS = [
85
85
  * @group Decorators
86
86
  * @category Pipeline stages
87
87
  * @see {@link Catch}
88
- * @see {@link https://meocord.dev/docs/4.1/exception-filters | Exception filters}
88
+ * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
89
89
  */ function UseFilter(...filters) {
90
- return function(target, propertyKey) {
90
+ return declaring(function(target, propertyKey) {
91
91
  const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
92
92
  assertStageEntries('@UseFilter', 'filter', where, filters);
93
93
  // Decorators apply bottom-up, so a higher decorator's filters are tried first.
@@ -104,7 +104,7 @@ const ORDINALS = [
104
104
  ...existing
105
105
  ], target, propertyKey);
106
106
  }
107
- };
107
+ });
108
108
  }
109
109
 
110
110
  export { Catch, UseFilter };