meocord 4.2.1 → 4.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +32 -10
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +27 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +92 -66
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +6 -6
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +54 -32
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. package/package.json +5 -2
@@ -1,12 +1,63 @@
1
1
  import { getMessageHandlers } from '../decorator/controller.decorator.js';
2
2
  import { routeSpecificity } from './route-specificity.js';
3
- import { isKnownParamType, BUILT_IN_TYPES, isGuildType, fitsParamType } from './message-params.js';
4
- import { isSpace, splitFlagWords, splitWords, restFrom } from './message-words.js';
3
+ import { isKnownParamType, BUILT_IN_TYPES, isGuildType, effectiveScope, fitsParamType } from './message-params.js';
4
+ import { isSpace, splitFlagWords, literalFlags, splitWords, restFrom, isLiteralFlag } from './message-words.js';
5
+ import { setOwn, shadowInherited } from '../util/value.util.js';
5
6
  import { startupError } from '../util/refusal.util.js';
7
+ import { warnOnce } from '../common/deprecation.js';
8
+ import { Logger } from '../common/logger.js';
6
9
 
10
+ const logger = new Logger('MeoCord');
7
11
  /** Whether a route's scope lets it run where the message was sent. */ const fitsScope = (scope, inGuild)=>inGuild === undefined || scope === 'any' || scope === 'guild' === inGuild;
8
12
  const PARAM = /^\{(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\.\.\.)?(\?)?\}$/;
9
13
  const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
14
+ /**
15
+ * A whole brace pair that PARAM or FLAG refused, read loosely to tell its name from its type: anything up to `:` or `}`,
16
+ * then the marks a param ends with, `...` and `?`, wherever they were written.
17
+ */ const LOOSE_FLAG = /^\{--([^:}]*?)(?::([^}]*?))?(\.\.\.)?(\?)?\}$/;
18
+ const LOOSE_PARAM = /^\{([^:}]*?)(?::([^}]*?))?(\.\.\.)?(\?)?\}$/;
19
+ /** A name or type with marks after it, as one written out of order carries them. */ const MARKED = /^(.*?)((?:\.\.\.|\?)+)$/;
20
+ /** A param's name, and a flag's, which also starts with a letter, as a message could not give it otherwise. */ const PARAM_NAME = /^\w+$/;
21
+ const FLAG_NAME = /^[A-Za-z]\w*$/;
22
+ /** A param as its pattern writes it, which the grammar spells only one way. */ const writtenParam = (token)=>`{${token.param}${token.type ? `:${token.type}` : ''}${token.rest ? '...' : ''}${token.optional ? '?' : ''}}`;
23
+ /** A flag as its pattern writes it. */ const writtenFlag = (flag)=>`{--${flag.flag}${flag.type ? `:${flag.type}` : ''}${flag.optional ? '?' : ''}}`;
24
+ /**
25
+ * Why a word with a brace in it is no param or flag: text around its braces, or a whole pair whose name, type or marks
26
+ * the syntax doesn't take. Each names the rule broken, with the word mended as an example.
27
+ */ function notAParam(word) {
28
+ const flag = LOOSE_FLAG.exec(word);
29
+ if (flag) {
30
+ const [, name, type, rest] = flag;
31
+ if (rest || word.includes('...')) {
32
+ return `"${word}" is not a flag: a flag doesn't take the rest of the message; a param does, such as {${mended(unmarked(name), 'param')}...}.`;
33
+ }
34
+ if (unmarked(name) !== name || type !== undefined && unmarked(type) !== type) {
35
+ const typed = type === undefined ? '' : `:${unmarked(type)}`;
36
+ return `"${word}" is not a flag: its ? goes last, as in {--${unmarked(name)}${typed}?}.`;
37
+ }
38
+ if (!FLAG_NAME.test(name)) {
39
+ return `"${word}" is not a flag: a flag's name starts with a letter, then letters, digits or _, such as {--${mended(name, 'flag')}}.`;
40
+ }
41
+ return `"${word}" is not a flag: a type is a name, or words to choose from joined by |, such as {--${name}:on|off}.`;
42
+ }
43
+ const param = LOOSE_PARAM.exec(word);
44
+ if (param) {
45
+ const [, name, type] = param;
46
+ if (unmarked(name) !== name || type !== undefined && unmarked(type) !== type) {
47
+ const typed = type === undefined ? '' : `:${unmarked(type)}`;
48
+ const ordered = `{${unmarked(name)}${typed}${word.includes('...') ? '...' : ''}${word.includes('?') ? '?' : ''}}`;
49
+ return `"${word}" is not a param: its marks go last, ... then ?, as in ${ordered}.`;
50
+ }
51
+ if (!PARAM_NAME.test(name)) return `"${word}" is not a param: a param's name is ASCII letters, digits and _, such as {${mended(name, 'param')}}.`;
52
+ return `"${word}" is not a param: a type is a name, or words to choose from joined by |, such as {${name}:on|off}.`;
53
+ }
54
+ return `"${word}" is not a param: a param is a whole word, such as {name}, {name:type}, {name...}, {name?} or a flag, {--name}.`;
55
+ }
56
+ /** A name or type without the marks written after it, `...` and `?`. */ const unmarked = (text)=>MARKED.exec(text)?.[1] ?? text;
57
+ /** A name with each character the syntax doesn't take as `_`, and a flag's led by a letter, for an example. */ function mended(name, kind) {
58
+ const word = name.replace(/\W/g, '_') || kind;
59
+ return kind === 'flag' && !/^[A-Za-z]/.test(word) ? `flag${word}` : word;
60
+ }
10
61
  /**
11
62
  * Reads a pattern into its words and flags. Throws for a pattern of flags alone, a param that is not a whole word, a
12
63
  * name given twice, a rest param that is not last, a word after an optional param that is not optional too, an untyped
@@ -42,9 +93,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
42
93
  words.forEach((word, index)=>{
43
94
  const match = PARAM.exec(word);
44
95
  if (!match) {
45
- if (/[{}]/.test(word)) {
46
- throw new Error(`"${word}" is not a param: a param is a whole word, such as {name}, {name:type}, {name...}, {name?} or a flag, {--name}.`);
47
- }
96
+ if (/[{}]/.test(word)) throw new Error(notAParam(word));
48
97
  tokens.push({
49
98
  literal: word
50
99
  });
@@ -67,7 +116,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
67
116
  const next = tokens[index + 1];
68
117
  if (!('param' in token) || !token.optional || !next) return;
69
118
  if (!('param' in next) || !next.optional) {
70
- throw new Error(`{${token.param}?} is optional, so only optional params may follow it; "${'param' in next ? `{${next.param}}` : next.literal}" is not.`);
119
+ throw new Error(`${writtenParam(token)} is optional, so only optional params may follow it; "${'param' in next ? writtenParam(next) : next.literal}" is not.`);
71
120
  }
72
121
  if (token.type === undefined || token.type === 'string') {
73
122
  throw new Error(`{${token.param}${token.type ? ':string' : ''}?} comes before another optional param, so it needs a type that tells its words ` + `from the next param's, such as {${token.param}:int?}: as text, it would take every word.`);
@@ -80,10 +129,15 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
80
129
  rest: params.some((token)=>token.rest),
81
130
  optional: params.some((token)=>token.optional)
82
131
  });
132
+ const inheritedNames = [
133
+ ...params.map((token)=>token.param),
134
+ ...flags.map((flag)=>flag.flag)
135
+ ].filter((name)=>name in Object.prototype);
83
136
  return {
84
137
  tokens,
85
138
  flags,
86
- specificity
139
+ specificity,
140
+ inheritedNames
87
141
  };
88
142
  }
89
143
  /** A prefix setting as a list, where `''` stands for none. */ function prefixList(prefix) {
@@ -134,7 +188,20 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
134
188
  ...mention
135
189
  ]);
136
190
  }
137
- /** Whether two routes accept some of the same messages: a start they share, and words that match alike. */ function sameMessages(a, b, options) {
191
+ /** Where two routes of equal rank and shape part: the one with a required param where the other has an optional one comes first. */ function compareOptional(a, b) {
192
+ for(let i = 0; i < Math.min(a.length, b.length); i++){
193
+ const [aToken, bToken] = [
194
+ a[i],
195
+ b[i]
196
+ ];
197
+ if ('param' in aToken && 'param' in bToken && aToken.optional !== bToken.optional) return aToken.optional ? 1 : -1;
198
+ }
199
+ return 0;
200
+ }
201
+ /**
202
+ * Whether two routes accept some of the same messages: a start they share, and words that match alike. `'optional'`
203
+ * when they do only because a required param and an optional one, in one position, both take a word there.
204
+ */ function sameMessages(a, b, options) {
138
205
  // A case-insensitive word matches every message the case-sensitive one does
139
206
  const exact = a.caseSensitive && b.caseSensitive;
140
207
  const key = (start)=>typeof start === 'string' && !exact ? start.toLowerCase() : start;
@@ -152,13 +219,55 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
152
219
  ].some((start)=>theirs.has(key(start)));
153
220
  });
154
221
  if (!startAlike || a.tokens.length !== b.tokens.length) return false;
155
- return a.tokens.every((token, i)=>{
222
+ let byOptional = false;
223
+ const alike = a.tokens.every((token, i)=>{
156
224
  const other = b.tokens[i];
157
225
  if ('literal' in token) {
158
226
  return 'literal' in other && (exact ? token.literal === other.literal : token.literal.toLowerCase() === other.literal.toLowerCase());
159
227
  }
160
- return 'param' in other && token.rest === other.rest && token.optional === other.optional;
228
+ if (!('param' in other) || token.rest !== other.rest) return false;
229
+ if (token.optional !== other.optional) byOptional = true;
230
+ return true;
161
231
  });
232
+ return alike && (byOptional ? 'optional' : true);
233
+ }
234
+ /** What a static empty prefix list does now, which a prefix function's `[]` doesn't: take messages as they are. */ const EMPTY_PREFIX_LIST = "`[]` takes messages as they are, with no prefix; write `''` for that";
235
+ /**
236
+ * The warnings for message handlers a bot starts with today though a message can't reach them as written, each once,
237
+ * for the app or the handler: a static empty prefix list, which takes messages as they are, where a prefix function's
238
+ * `[]` starts no command; and two flags of a command that reads flags in any case whose names differ only in case, the
239
+ * second of which is never set. The next major version (5.0) changes the first and refuses the second.
240
+ */ function messageRouteWarnings(routes, options) {
241
+ const warnings = [];
242
+ if (Array.isArray(options.prefix) && options.prefix.length === 0) {
243
+ warnings.push(`@MeoCord({ messages: { prefix: [] } }): ${EMPTY_PREFIX_LIST}. In the next major version (5.0), \`[]\` starts no command, ` + "as a prefix function's `[]` does.");
244
+ }
245
+ const seen = new Map();
246
+ for (const route of routes){
247
+ // A handler's aliases share its options and flags, so each is read once; one method can carry several handlers
248
+ const pattern = route.aliasOf ?? route.pattern;
249
+ const methods = seen.get(route.controllerClass) ?? seen.set(route.controllerClass, new Map()).get(route.controllerClass);
250
+ const patterns = methods.get(route.method) ?? methods.set(route.method, new Set()).get(route.method);
251
+ if (patterns.has(pattern)) continue;
252
+ patterns.add(pattern);
253
+ const handler = `${route.controllerClass.name}.${route.method}: @MessageHandler('${pattern}'`;
254
+ const own = getMessageHandlers(route.controllerClass.prototype).find((candidate)=>candidate.method === route.method && candidate.pattern === pattern)?.options;
255
+ if (Array.isArray(own?.prefix) && own.prefix.length === 0) {
256
+ warnings.push(`${handler}, { prefix: [] }): ${EMPTY_PREFIX_LIST}, or \`false\` for no prefix and no mention. In the next major ` + 'version (5.0), `[]` starts no command.');
257
+ }
258
+ if (route.caseSensitive) continue;
259
+ const first = new Map();
260
+ for (const flag of route.flags){
261
+ const same = first.get(flag.flag.toLowerCase());
262
+ if (!same) {
263
+ first.set(flag.flag.toLowerCase(), flag);
264
+ continue;
265
+ }
266
+ warnings.push(`${handler}): ${writtenFlag(same)} and ${writtenFlag(flag)} differ only in case, and this command reads flags in any ` + `case, so --${flag.flag} always sets ${writtenFlag(same)} and ${writtenFlag(flag)} is never set. Rename one, or set ` + 'caseSensitive: true. The next major version (5.0) refuses this.');
267
+ break;
268
+ }
269
+ }
270
+ return warnings;
162
271
  }
163
272
  /**
164
273
  * Every patterned message handler of the given controllers, most specific first, read from metadata
@@ -227,14 +336,21 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
227
336
  });
228
337
  }
229
338
  }
230
- routes.sort((a, b)=>b.specificity - a.specificity || compareShapes(a.tokens, b.tokens) || Number(takesAppStart(a)) - Number(takesAppStart(b)));
339
+ routes.sort((a, b)=>b.specificity - a.specificity || compareShapes(a.tokens, b.tokens) || compareOptional(a.tokens, b.tokens) || Number(takesAppStart(a)) - Number(takesAppStart(b)));
231
340
  for(let i = 0; i < routes.length; i++){
232
341
  for(let j = i + 1; j < routes.length; j++){
233
342
  const [a, b] = [
234
343
  routes[i],
235
344
  routes[j]
236
345
  ];
237
- if (!sameMessages(a, b, options)) continue;
346
+ const same = sameMessages(a, b, options);
347
+ if (!same) continue;
348
+ // Ranked by their params, so the same one answers whatever the order; 5.0 refuses these as it does the rest
349
+ if (same === 'optional') {
350
+ if (a.controllerClass === b.controllerClass && a.method === b.method) continue;
351
+ warnOnce(logger, `${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} can ` + `take the same messages, as one takes a word where the other has an optional param; ${a.controllerClass.name}.${a.method} ` + 'answers those. Change one pattern, or give one its own prefix. The next major version (5.0) stops the bot at startup for these.');
352
+ continue;
353
+ }
238
354
  // One handler under two spellings, such as 'hello' and 'Hello', is one route, its pattern's over an alias's
239
355
  if (a.controllerClass === b.controllerClass && a.method === b.method) {
240
356
  if (a.aliasOf && !b.aliasOf) routes[i] = b;
@@ -279,7 +395,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
279
395
  if (scope !== 'dm') return;
280
396
  const param = tokens.find((token)=>'param' in token && token.type !== undefined && isGuildType(token.type));
281
397
  const flag = flags.find((candidate)=>candidate.type !== undefined && isGuildType(candidate.type));
282
- const shown = param ? `{${param.param}:${param.type}}` : flag && `{--${flag.flag}:${flag.type}}`;
398
+ const shown = param ? writtenParam(param) : flag && writtenFlag(flag);
283
399
  if (shown) throw new Error(`scope is 'dm', but ${shown} is found only in a server.`);
284
400
  }
285
401
  /** Refuses a handler's `mention` other than `'only'`. */ function assertMention({ mention }) {
@@ -391,11 +507,11 @@ const trieNode = ()=>({
391
507
  for(let t = 0; t < tail.length && w < words.length; t++){
392
508
  const token = tail[t];
393
509
  if (token.rest) {
394
- params[token.param] = restFrom(text, words[w].start, cuts);
510
+ setOwn(params, token.param, restFrom(text, words[w].start, cuts));
395
511
  return params;
396
512
  }
397
513
  if (t < tail.length - 1 && !fitsParamType(token.type, words[w].value, caseSensitive)) continue;
398
- params[token.param] = words[w++].value;
514
+ setOwn(params, token.param, words[w++].value);
399
515
  }
400
516
  return w === words.length ? params : undefined;
401
517
  }
@@ -440,9 +556,12 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
440
556
  return;
441
557
  }
442
558
  if ('literal' in token) {
559
+ var _node1;
443
560
  const key = wordKey(token.literal, route.caseSensitive);
444
561
  let next = node.words.get(key);
445
562
  if (!next) node.words.set(key, next = trieNode());
563
+ // Such a word typed bare is taken out as a flag before the words are walked, so it is matched as one too
564
+ if (route.flags.length > 0 && token.literal.startsWith('--')) ((_node1 = node).flagWords ?? (_node1.flagWords = new Map())).set(key, next);
446
565
  node = next;
447
566
  continue;
448
567
  }
@@ -461,6 +580,8 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
461
580
  ];
462
581
  for (const group of all){
463
582
  const flagged = group.flagged;
583
+ // A pattern's own leading `--` word stays reachable there: given bare, it is taken out as a flag, so the message
584
+ // seems to start with one
464
585
  if (flagged) group.flaggedAfterFlag = {
465
586
  ...flagged,
466
587
  words: new Map()
@@ -501,7 +622,16 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
501
622
  if (first === undefined || node.param || node.rests.length > 0 || node.tails.length > 0 || node.ends.length > 0) return true;
502
623
  return node.words.has(first);
503
624
  }
504
- /** The ranks of every route the words reach from `node`. Each node sits at one word depth, so each is visited once. */ function reach(node, words, i, text, cuts, caseSensitive, found) {
625
+ /**
626
+ * The ranks of every route the words reach from `node`. A pattern's own `--` word is reached by that word, or by a flag
627
+ * of its name given bare in its place, which takes no word.
628
+ */ function reach(node, words, i, text, cuts, caseSensitive, flags, found) {
629
+ if (node.flagWords) {
630
+ for (const flag of flags){
631
+ const next = flag.at === i && flag.value === undefined ? node.flagWords.get(wordKey(`--${flag.name}`, caseSensitive)) : undefined;
632
+ if (next) reach(next, words, i, text, cuts, caseSensitive, flags, found);
633
+ }
634
+ }
505
635
  for (const tail of node.tails)if (assignTail(tail.tokens, words, i, text, cuts, caseSensitive)) found.push(tail.rank);
506
636
  if (i === words.length) {
507
637
  for (const rank of node.ends)found.push(rank);
@@ -509,22 +639,34 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
509
639
  }
510
640
  for (const rank of node.rests)found.push(rank);
511
641
  const next = node.words.get(wordKey(words[i].value, caseSensitive));
512
- if (next) reach(next, words, i + 1, text, cuts, caseSensitive, found);
513
- if (node.param) reach(node.param, words, i + 1, text, cuts, caseSensitive, found);
642
+ if (next) reach(next, words, i + 1, text, cuts, caseSensitive, flags, found);
643
+ if (node.param) reach(node.param, words, i + 1, text, cuts, caseSensitive, flags, found);
514
644
  }
515
- /** The params a route's words capture, given words its pattern is known to match. */ function paramsOf(route, words, text, cuts) {
645
+ /**
646
+ * The params a route's words capture, given words its pattern is known to match. A pattern's own `--` word that the
647
+ * message gave as a flag, one of `taken`, takes no word.
648
+ */ function paramsOf(route, words, text, cuts, taken) {
516
649
  const tail = tailStart(route.tokens);
517
- const params = tail === -1 ? {} : assignTail(route.tokens.slice(tail), words, tail, text, cuts, route.caseSensitive);
518
- for(let i = 0; i < (tail === -1 ? route.tokens.length : tail) && i < words.length; i++){
650
+ const params = {};
651
+ let left = taken;
652
+ let w = 0;
653
+ for(let i = 0; i < (tail === -1 ? route.tokens.length : tail) && w < words.length; i++){
519
654
  const token = route.tokens[i];
520
- if ('literal' in token) continue;
655
+ if ('literal' in token) {
656
+ if (left > 0 && token.literal.startsWith('--') && wordKey(words[w].value, route.caseSensitive) !== wordKey(token.literal, route.caseSensitive)) left--;
657
+ else w++;
658
+ continue;
659
+ }
521
660
  if (token.rest) {
522
- params[token.param] = restFrom(text, words[i].start, cuts);
523
- break;
661
+ setOwn(params, token.param, restFrom(text, words[w].start, cuts));
662
+ return params;
524
663
  }
525
- params[token.param] = words[i].value;
664
+ setOwn(params, token.param, words[w++].value);
526
665
  }
527
- return params;
666
+ return tail === -1 ? params : {
667
+ ...params,
668
+ ...assignTail(route.tokens.slice(tail), words, w, text, cuts, route.caseSensitive)
669
+ };
528
670
  }
529
671
  /**
530
672
  * The text after the start a message used for the group's routes, or `undefined` when it used none: a mention
@@ -577,7 +719,7 @@ const indexes = new WeakMap();
577
719
  const found = [];
578
720
  if (mayEnter(group.root, first)) {
579
721
  const words = wordsOf(rest);
580
- reach(group.root, words, 0, rest, NONE, group.caseSensitive, found);
722
+ reach(group.root, words, 0, rest, NONE, group.caseSensitive, NONE, found);
581
723
  for (const rank of found){
582
724
  if (fitsScope(routes[rank].scope, starts.inGuild)) {
583
725
  if (!best || rank < best.rank) best = {
@@ -606,7 +748,7 @@ const indexes = new WeakMap();
606
748
  };
607
749
  found.length = 0;
608
750
  const from = scanned.words[0]?.start === 0 ? flagged : group.flaggedAfterFlag;
609
- reach(from, scanned.words, 0, rest, scanned.cuts, group.caseSensitive, found);
751
+ reach(from, scanned.words, 0, rest, scanned.cuts, group.caseSensitive, scanned.flags, found);
610
752
  for (const rank of found){
611
753
  const candidate = {
612
754
  rank,
@@ -623,11 +765,16 @@ const indexes = new WeakMap();
623
765
  const chosen = best ?? outside;
624
766
  if (!chosen) return undefined;
625
767
  const route = routes[chosen.rank];
626
- const params = paramsOf(route, chosen.words, chosen.rest, chosen.cuts);
627
- for (const { name, value } of chosen.flags){
768
+ // The flags the pattern's own `--` words took are those words, and set none of its flags
769
+ const taken = literalFlags(route.tokens, route.caseSensitive, chosen.words, chosen.flags);
770
+ const params = paramsOf(route, chosen.words, chosen.rest, chosen.cuts, taken.size);
771
+ chosen.flags.forEach(({ name, value }, index)=>{
772
+ if (taken.has(index)) return;
628
773
  const declared = route.flags.find((flag)=>wordKey(flag.flag, route.caseSensitive) === wordKey(name, route.caseSensitive));
629
- if (declared) params[declared.flag] = value ?? '';
630
- }
774
+ if (declared) setOwn(params, declared.flag, value ?? '');
775
+ });
776
+ // A param or flag left out reads as left out, not as what Object.prototype has under its name
777
+ if (route.inheritedNames.length > 0) shadowInherited(params, route.inheritedNames);
631
778
  return {
632
779
  route,
633
780
  params,
@@ -657,19 +804,28 @@ const indexes = new WeakMap();
657
804
  if (first !== undefined && !group.commands.has(first)) continue;
658
805
  const plain = splitWords(rest);
659
806
  // A route with flags counts the words left once the message's flags are taken out
660
- const positional = mayEnter(group.flagged, first) && rest.includes('--') ? splitFlagWords(rest).words : plain;
807
+ const scanned = mayEnter(group.flagged, first) && rest.includes('--') ? splitFlagWords(rest) : {
808
+ words: plain,
809
+ flags: NONE
810
+ };
661
811
  const candidates = group.commands.get(wordKey(plain[0]?.value ?? '', group.caseSensitive)) ?? [];
662
812
  for (const rank of candidates){
663
813
  if (best && rank >= best.rank) break;
664
- const words = routes[rank].flags.length > 0 ? positional : plain;
665
- const command = commandWordsOf(routes[rank].tokens);
666
- if (command.every((word, i)=>words[i] && wordKey(words[i].value, group.caseSensitive) === wordKey(word, group.caseSensitive))) {
814
+ const flagged = routes[rank].flags.length > 0;
815
+ const words = flagged ? scanned.words : plain;
816
+ const flags = flagged ? scanned.flags : NONE;
817
+ const taken = commandWordsTaken(commandWordsOf(routes[rank].tokens), words, flags, group.caseSensitive);
818
+ if (taken !== -1) {
667
819
  const named = {
668
820
  rank,
669
821
  start: text.slice(0, text.length - rest.length),
670
- given: words.length - command.length
822
+ given: {
823
+ words,
824
+ flags
825
+ }
671
826
  };
672
- if (fitsScope(routes[rank].scope, starts.inGuild)) {
827
+ // Where it works as dispatch answers it, so a DM's usage comes from a route that works there
828
+ if (fitsScope(effectiveScope(routes[rank]), starts.inGuild)) {
673
829
  best = named;
674
830
  break;
675
831
  }
@@ -684,6 +840,17 @@ const indexes = new WeakMap();
684
840
  given: chosen.given
685
841
  };
686
842
  }
843
+ /**
844
+ * How many of a message's words a route's command words take, or -1 when the message names another command. A
845
+ * command's own `--` word may be given bare as a flag in its place, and takes no word.
846
+ */ function commandWordsTaken(command, words, flags, caseSensitive) {
847
+ let w = 0;
848
+ for (const word of command){
849
+ if (words[w] && wordKey(words[w].value, caseSensitive) === wordKey(word, caseSensitive)) w++;
850
+ else if (!flags.some((flag)=>isLiteralFlag(flag, word, w, caseSensitive))) return -1;
851
+ }
852
+ return w;
853
+ }
687
854
  /**
688
855
  * The subcommands a message names the parent of, when no pattern matches it and it names no command: the routes whose
689
856
  * command words begin with the most of the message's words, short of all of them, as `!config` does for `config set …`
@@ -726,11 +893,18 @@ const indexes = new WeakMap();
726
893
  }
727
894
  return found.length > 0 ? found : undefined;
728
895
  }
896
+ /**
897
+ * Whether a message has text a handler could take; dispatch runs no handler for one without. Content that isn't a
898
+ * string, as a mock's placeholder without strict mocks, is no text.
899
+ */ function hasMessageText(content) {
900
+ return typeof content === 'string' && content.trim() !== '';
901
+ }
729
902
  /**
730
903
  * What a test calling a handler with a message alone passes as its params, as dispatch builds them: `undefined` for a
731
- * listener, `{}` for a message without content, else what the pattern captures with the route and start; for one naming
732
- * the command without fitting it, the route, start and `given`, the words after the command words. Either only when
733
- * dispatch would give the message to this handler among `controllers`; otherwise why it does not reach the handler.
904
+ * listener, `{}` for a message whose content isn't a string, as a mock's without strict mocks, else what the pattern
905
+ * captures with the route and start; for one naming the command without fitting it, the route, start and `given`, the
906
+ * message's words and flags. Either only when dispatch would give the message to this handler among
907
+ * `controllers`; otherwise why it does not reach the handler, a message with no text included.
734
908
  */ async function messageParamsFor(controllerClass, methodName, message, options, controllers = [
735
909
  controllerClass
736
910
  ]) {
@@ -742,6 +916,13 @@ const indexes = new WeakMap();
742
916
  if (typeof message.content !== 'string') return {
743
917
  params: {}
744
918
  };
919
+ const handler = `${controllerClass.name}.${methodName}`;
920
+ const pattern = routes.find((route)=>!route.aliasOf)?.pattern ?? routes[0].pattern;
921
+ if (!hasMessageText(message.content)) {
922
+ return {
923
+ mismatch: `message has no text, so dispatch runs no handler for it: give the mock the content ${handler}'s pattern '${pattern}' takes.`
924
+ };
925
+ }
745
926
  const botId = message.client?.user?.id;
746
927
  const all = buildMessageRoutes(controllers, options);
747
928
  const starts = await messageStartsFor(all, options, message, typeof botId === 'string' ? botId : undefined);
@@ -750,7 +931,7 @@ const indexes = new WeakMap();
750
931
  if (target && (target.route.controllerClass !== controllerClass || target.route.method !== methodName)) {
751
932
  const other = `${target.route.controllerClass.name}.${target.route.method}`;
752
933
  return {
753
- mismatch: `message '${message.content}' does not reach ${controllerClass.name}.${methodName}: dispatch runs ${other}.`
934
+ mismatch: `message '${message.content}' does not reach ${handler}: dispatch runs ${other}.`
754
935
  };
755
936
  }
756
937
  if (target && 'params' in target) return {
@@ -764,10 +945,9 @@ const indexes = new WeakMap();
764
945
  start: target.start,
765
946
  given: target.given
766
947
  };
767
- const pattern = routes.find((route)=>!route.aliasOf)?.pattern ?? routes[0].pattern;
768
948
  return {
769
- mismatch: `message '${message.content}' does not match ${controllerClass.name}.${methodName}'s pattern '${pattern}'.`
949
+ mismatch: `message '${message.content}' does not match ${handler}'s pattern '${pattern}'.`
770
950
  };
771
951
  }
772
952
 
773
- export { afterStart, buildMessageRoutes, commandWordsOf, fitsScope, matchMessageCommand, matchMessageRoute, matchMessageSubcommands, messageParamsFor, messageStarts, messageStartsFor, parseMessagePattern, staticMessageStarts, usesAppPrefix };
953
+ export { afterStart, buildMessageRoutes, commandWordsOf, fitsScope, hasMessageText, matchMessageCommand, matchMessageRoute, matchMessageSubcommands, messageParamsFor, messageRouteWarnings, messageStarts, messageStartsFor, parseMessagePattern, staticMessageStarts, usesAppPrefix };
@@ -109,7 +109,8 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
109
109
  }
110
110
  flags.push({
111
111
  name,
112
- value
112
+ value,
113
+ at: words.length
113
114
  });
114
115
  cuts.push(start, j);
115
116
  i = j;
@@ -144,5 +145,32 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
144
145
  }
145
146
  return (rest + text.slice(from)).trimEnd();
146
147
  }
148
+ const caseKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase();
149
+ /** Whether a flag, given bare in place `at` among the other words, is a pattern's own `--` word `literal`. */ const isLiteralFlag = (flag, literal, at, caseSensitive)=>flag.at === at && flag.value === undefined && caseKey(`--${flag.name}`, caseSensitive) === caseKey(literal, caseSensitive);
150
+ /**
151
+ * The flags a pattern's own `--` words take from a message, as indexes into `flags`: where the pattern has such a word,
152
+ * a flag of that name given bare in that place among the message's other words is the word, not a flag. A word typed in
153
+ * quotes is matched as any word is, and is taken first.
154
+ */ function literalFlags(tokens, caseSensitive, words, flags) {
155
+ const taken = new Set();
156
+ if (flags.length === 0) return taken;
157
+ let w = 0;
158
+ for (const token of tokens){
159
+ if (!('literal' in token)) {
160
+ // Literal words come before the optional and rest params, which a pattern ends with
161
+ if (token.rest || token.optional) break;
162
+ w++;
163
+ continue;
164
+ }
165
+ if (w < words.length && caseKey(words[w].value, caseSensitive) === caseKey(token.literal, caseSensitive)) {
166
+ w++;
167
+ continue;
168
+ }
169
+ const index = flags.findIndex((flag, i)=>!taken.has(i) && isLiteralFlag(flag, token.literal, w, caseSensitive));
170
+ if (index === -1) break;
171
+ taken.add(index);
172
+ }
173
+ return taken;
174
+ }
147
175
 
148
- export { isSpace, restFrom, splitFlagWords, splitWords };
176
+ export { caseKey, isLiteralFlag, isSpace, literalFlags, restFrom, splitFlagWords, splitWords };
@@ -5,6 +5,7 @@ import { bindShared } from './interceptor-runner.js';
5
5
  import { appliesTo } from './stage-scope.js';
6
6
  import { respond } from '../common/response/response-state.js';
7
7
  import { startupError } from '../util/refusal.util.js';
8
+ import { isConstructor } from '../util/value.util.js';
8
9
  import { META } from '../util/metadata-keys.js';
9
10
 
10
11
  /** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
@@ -12,7 +13,7 @@ const logger = new Logger('Observer');
12
13
  /** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
13
14
  for (const observer of observers){
14
15
  if (typeof observer !== 'function' || !Reflect.getMetadata(META.observerClass, observer)) {
15
- const name = typeof observer === 'function' ? observer.name : String(observer);
16
+ const name = isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
16
17
  startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
17
18
  continue;
18
19
  }
@@ -68,8 +69,9 @@ function observersOf(container) {
68
69
  }
69
70
  /**
70
71
  * Where an interaction's answer stands: a repliable one's `ResponseState`, and an autocomplete answered
71
- * or not. `undefined` for anything that is not an interaction.
72
+ * or not. `undefined` for anything that is not an interaction's call, an event carrying one included.
72
73
  */ function responsePhaseOf(context) {
74
+ if (context.getType() === 'event') return undefined;
73
75
  const interaction = context.getInteraction();
74
76
  if (!interaction) return undefined;
75
77
  if (interaction.isAutocomplete()) return interaction.responded ? 'replied' : 'unanswered';
@@ -0,0 +1,19 @@
1
+ import { stopRequests } from '../util/stop-request.util.js';
2
+ import { shared } from '../util/shared-state.util.js';
3
+
4
+ /** Where both builds of this version keep the process's state, as {@link shared} names it. */ const PROCESS_STATE = 'processState';
5
+ /**
6
+ * What the process's apps share, kept by both builds of this version, so a process that loads both has one pair of
7
+ * signal listeners and one shutdown, which stops the apps either build started, one exit code a failed start set, and
8
+ * one end of a failed shard.
9
+ */ const processState = shared(PROCESS_STATE, ()=>({
10
+ /** Closes each started app: runs its shutdown hooks and destroys its client, resolving `false` on failure. */ runningApps: new Set(),
11
+ stopRequest: stopRequests(),
12
+ signalHandlersInstalled: false,
13
+ shuttingDown: undefined,
14
+ /** Whether a failed start, of a provider's factory or the login, set the exit code, so a later login knows to clear it. */ startFailureSetExitCode: false,
15
+ /** Whether `meocord start --dev` was told the bot could not log in, so a later login tells it the bot is online. */ toldDevRunnerLoginFailed: false,
16
+ /** Whether this shard is already ending, so a failure reported twice, as one going on uncaught is, ends it once. */ shardEnding: false
17
+ }));
18
+
19
+ export { processState };