meocord 4.2.2 → 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 (136) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
  3. package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
  4. package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
  5. package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +432 -181
  9. package/dist/cjs/decorator/index.cjs +85 -120
  10. package/dist/cjs/testing/index.cjs +1664 -784
  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 +27 -5
  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 +17 -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 +83 -58
  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 +4 -4
  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 +44 -28
  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 +80 -53
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +5 -9
  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 +2 -1
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +28 -3
  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/startup-roots.js +28 -8
  73. package/dist/esm/core/theme-resolvers.js +26 -11
  74. package/dist/esm/core/theme-runtime.js +23 -13
  75. package/dist/esm/core/theme-scope.js +27 -29
  76. package/dist/esm/core/theme-validation.js +1 -1
  77. package/dist/esm/decorator/app.decorator.js +6 -1
  78. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  79. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  80. package/dist/esm/decorator/controller.decorator.js +48 -19
  81. package/dist/esm/decorator/defer.decorator.js +2 -2
  82. package/dist/esm/decorator/event.decorator.js +4 -5
  83. package/dist/esm/decorator/filter.decorator.js +2 -2
  84. package/dist/esm/decorator/guard.decorator.js +4 -61
  85. package/dist/esm/decorator/inject.decorator.js +4 -1
  86. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  87. package/dist/esm/decorator/theme.decorator.js +4 -3
  88. package/dist/esm/decorator/validation.decorator.js +5 -5
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +29 -13
  91. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  92. package/dist/esm/testing/discord-defaults.js +449 -355
  93. package/dist/esm/testing/inspect-handler.js +91 -10
  94. package/dist/esm/testing/meocord-testing-module.js +195 -77
  95. package/dist/esm/testing/mock-fn.js +8 -2
  96. package/dist/esm/testing/mock-interaction.js +639 -113
  97. package/dist/esm/testing/response-log.js +8 -0
  98. package/dist/esm/testing/response.js +7 -5
  99. package/dist/esm/testing/routing.js +48 -6
  100. package/dist/esm/testing/strict-mocks.js +1 -0
  101. package/dist/esm/testing/theme.js +3 -2
  102. package/dist/esm/util/common.util.js +30 -9
  103. package/dist/esm/util/generator-cli.util.js +6 -4
  104. package/dist/esm/util/inherited-env.util.js +72 -13
  105. package/dist/esm/util/json.util.js +10 -3
  106. package/dist/esm/util/meocord-cli.util.js +3 -2
  107. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  108. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  109. package/dist/esm/util/meocord-source-config.util.js +13 -3
  110. package/dist/esm/util/platform.util.js +3 -2
  111. package/dist/esm/util/refusal.util.js +29 -11
  112. package/dist/esm/util/registration-mode.util.js +14 -1
  113. package/dist/esm/util/timer-limit.util.js +4 -1
  114. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  115. package/dist/esm/util/tsconfig.util.js +16 -58
  116. package/dist/esm/util/user-text.util.js +8 -4
  117. package/dist/esm/util/value.util.js +15 -4
  118. package/dist/types/common/index.d.cts +13 -9
  119. package/dist/types/common/index.d.ts +13 -9
  120. package/dist/types/core/index.d.cts +21 -11
  121. package/dist/types/core/index.d.ts +21 -11
  122. package/dist/types/decorator/index.d.cts +63 -63
  123. package/dist/types/decorator/index.d.ts +63 -63
  124. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  125. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  126. package/dist/types/interface/index.d.cts +1 -1
  127. package/dist/types/interface/index.d.ts +1 -1
  128. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  129. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  130. package/dist/types/testing/index.d.cts +137 -54
  131. package/dist/types/testing/index.d.ts +137 -54
  132. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  133. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  134. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  135. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  136. package/package.json +5 -2
@@ -1,5 +1,5 @@
1
1
  import { dropInheritedRoutes } from './controller.decorator.js';
2
- import { guardOwnHandlersWithBaseGuards } from './guard.decorator.js';
2
+ import { guardOwnHandlersWithBaseGuards } from '../core/guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
5
  import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
@@ -7,8 +7,8 @@ import { wrapsMethod } from '../util/method-wrapper.util.js';
7
7
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
8
8
  import { Logger } from '../common/logger.js';
9
9
  import { routeSpecificity } from '../core/route-specificity.js';
10
- import { lookupTable, isSegmentType, choicesOf, parseSegment } from '../core/scalar-types.js';
11
- import { declaring, refuse, refuseOnClass } from '../util/refusal.util.js';
10
+ import { lookupTable, isSegmentType, choicesOf, shortestSegment, parseSegment } from '../core/scalar-types.js';
11
+ import { declaring, refuse, refuseOnClass, decoratedName } from '../util/refusal.util.js';
12
12
  import { withArticle, describeValue } from '../util/value.util.js';
13
13
  import { META } from '../util/metadata-keys.js';
14
14
 
@@ -18,6 +18,12 @@ import { META } from '../util/metadata-keys.js';
18
18
  return `; it was given ${describeValue(value)}`;
19
19
  }
20
20
  const logger = new Logger('Command');
21
+ /** The interaction types a method's `@Command` routes declare, each once. */ function routeTypesOf(prototype, method) {
22
+ const metas = Object.values(getCommandMap(prototype) ?? {}).flat();
23
+ return [
24
+ ...new Set(metas.filter((meta)=>meta.methodName === method).map((meta)=>meta.type))
25
+ ];
26
+ }
21
27
  /**
22
28
  * The class's own handler list, started from a copy of the inherited one, so a subclass's
23
29
  * handlers never land in its base class's metadata.
@@ -43,6 +49,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
43
49
  method,
44
50
  label: `autocomplete of ${optionName === undefined ? 'every option' : `"${optionName}"`} in "${commandPath}"`
45
51
  });
52
+ /** An `@On` or `@Once` handler as a warning names its route; one event per method, as a subclass replaces it. */ const eventRoute = ({ method, event })=>({
53
+ method,
54
+ label: `event "${event}"`
55
+ });
46
56
  /** Records a route a class's own decorator declares for a method, which the startup check reads. */ function declareRoute(target, declared) {
47
57
  const routes = Reflect.getOwnMetadata(META.declaredRoutes, target) ?? [];
48
58
  routes.push(declared);
@@ -70,13 +80,22 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
70
80
  else handlers[index] = entry;
71
81
  Reflect.defineMetadata(key, handlers, target);
72
82
  }
83
+ /** The `@On` and `@Once` entries a class's own decorators declare, without those it inherits from its base. */ function ownEventHandlers(prototype) {
84
+ const inherited = Reflect.getMetadata(META.eventHandlers, Object.getPrototypeOf(prototype)) ?? [];
85
+ return ownHandlerList(META.eventHandlers, prototype).filter((entry)=>!inherited.includes(entry));
86
+ }
73
87
  /**
74
88
  * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
75
89
  * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
76
90
  */ function dropInheritedRoutes(prototype) {
77
- const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
78
- if (redeclared.size === 0) return;
79
91
  const base = Object.getPrototypeOf(prototype);
92
+ // The class's own @On and @Once re-declare their methods too
93
+ const ownEvents = ownEventHandlers(prototype);
94
+ const redeclared = new Set([
95
+ ...getDeclaredRoutes(prototype).map((route)=>route.method),
96
+ ...ownEvents.map((entry)=>entry.method)
97
+ ]);
98
+ if (redeclared.size === 0) return;
80
99
  // An entry the class takes from its base, for a method it re-declares
81
100
  const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
82
101
  const commands = ownCommandMap(prototype);
@@ -99,6 +118,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
99
118
  [
100
119
  META.autocompleteHandlers,
101
120
  (entry)=>entry.methodName
121
+ ],
122
+ [
123
+ META.eventHandlers,
124
+ (entry)=>entry.method
102
125
  ]
103
126
  ];
104
127
  for (const [key, method] of lists){
@@ -125,13 +148,14 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
125
148
  function MessageHandler(pattern, options = {}) {
126
149
  return declaring(function(target, propertyKey) {
127
150
  refuseOnClass('@MessageHandler', target, propertyKey);
128
- // An empty pattern means every message, as no pattern does
129
- if (pattern === '') {
130
- warnDeprecatedBehaviour(logger, `@MessageHandler('') on ${target.constructor.name}.${propertyKey}`, 'is refused', '@MessageHandler()');
151
+ // An empty pattern, or one of spaces alone, means every message, as no pattern does
152
+ const blank = pattern !== undefined && pattern.trim() === '';
153
+ if (blank) {
154
+ warnDeprecatedBehaviour(logger, `@MessageHandler('${pattern}') on ${decoratedName(target, propertyKey)}`, 'is refused', '@MessageHandler()');
131
155
  }
132
156
  const method = propertyKey.toString();
133
157
  const declared = {
134
- pattern: pattern || undefined,
158
+ pattern: blank ? undefined : pattern,
135
159
  method,
136
160
  options
137
161
  };
@@ -253,13 +277,14 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
253
277
  params: params.length,
254
278
  typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
255
279
  });
256
- // Each param takes at least one character
280
+ // Each param takes at least the shortest value of its type
281
+ const shortest = literalLength + params.reduce((sum, param)=>sum + shortestSegment(types[param] ?? 'string'), 0);
257
282
  return {
258
283
  regex,
259
284
  params,
260
285
  types: lookupTable(types),
261
286
  specificity,
262
- shortest: literalLength + params.length
287
+ shortest
263
288
  };
264
289
  }
265
290
  /** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
@@ -332,13 +357,17 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
332
357
  refuseOnClass('@Command', target, propertyKey);
333
358
  const originalMethod = _descriptor.value;
334
359
  if (!originalMethod) {
335
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: @Command is on something with no implementation.`));
360
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Command is on something with no implementation.`));
336
361
  }
337
362
  // Wrap original method for interaction type validation
338
363
  const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
339
364
  const checked = function(interaction, params) {
340
- if (!matchesCommandType(commandType, interaction)) {
341
- throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
365
+ // Every type the method's routes declare, in the class the call is made on: each @Command wraps it, so one alone
366
+ // would refuse the others' interactions, and an override that adds a route type reaches this one through super
367
+ const called = typeof this === 'object' && this !== null && Object.prototype.isPrototypeOf.call(target, this) ? Object.getPrototypeOf(this) : target;
368
+ const types = routeTypesOf(called, propertyKey);
369
+ if (!types.some((type)=>matchesCommandType(type, interaction))) {
370
+ throw new Error(types.length <= 1 ? `${decoratedName(target, propertyKey)}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.` : `${decoratedName(target, propertyKey)}: its @Command routes take ${types.map(interactionClassName).join(' or ')}` + `${givenInstead(interaction)}.`);
342
371
  }
343
372
  return originalMethod.apply(this, [
344
373
  interaction,
@@ -356,7 +385,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
356
385
  let guilds;
357
386
  // Determine command type and builder
358
387
  if (typeof builderOrType === 'function') {
359
- const where = `${target.constructor.name}.${propertyKey}`;
388
+ const where = decoratedName(target, propertyKey);
360
389
  // A subcommand is part of its command's builder, which belongs on the command's own name
361
390
  const subcommandPath = Reflect.getMetadata(META.commandType, builderOrType) === CommandType.SLASH && commandName.includes(' ');
362
391
  const command = commandName.split(' ')[0];
@@ -403,12 +432,12 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
403
432
  if (isCustomIdRouted(commandType)) {
404
433
  // A route warned as it was made
405
434
  const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
406
- if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
435
+ if (braces) logger.warn(`${decoratedName(target, propertyKey)}: in the pattern "${commandName}", ${braces}`);
407
436
  let pattern;
408
437
  try {
409
438
  pattern = createRegexFromPattern(commandName);
410
439
  } catch (error) {
411
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: ${error.message}`, {
440
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${error.message}`, {
412
441
  cause: error
413
442
  }));
414
443
  }
@@ -418,7 +447,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
418
447
  // Warned rather than refused, so a bot that carries such a handler still starts
419
448
  if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
420
449
  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.`);
450
+ logger.warn(`${decoratedName(target, 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
451
  }
423
452
  }
424
453
  const declared = {
@@ -470,7 +499,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
470
499
  const registered = built?.type;
471
500
  if (!declared || registered === undefined || declared.kind === registered) return;
472
501
  const kind = registered === ApplicationCommandType.User ? 'user' : 'message';
473
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
502
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
474
503
  }
475
504
  /**
476
505
  * Retrieves the command map for a given controller.
@@ -608,4 +637,4 @@ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
608
637
  return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
609
638
  }
610
639
 
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 };
640
+ export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, addOwnHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, eventRoute, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownEventHandlers, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { nonInteractionHandler, deferMisuseError } from '../core/defer.js';
3
- import { declaring, refuseOnClass, refuse } from '../util/refusal.util.js';
3
+ import { declaring, refuseOnClass, refuse, ownerName } from '../util/refusal.util.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
5
 
6
6
  /**
@@ -43,7 +43,7 @@ import { META } from '../util/metadata-keys.js';
43
43
  refuseOnClass('@Defer', target, propertyKey);
44
44
  const methodName = String(propertyKey);
45
45
  const kind = nonInteractionHandler(target, methodName);
46
- if (kind) throw refuse(deferMisuseError(target.constructor.name, methodName, kind));
46
+ if (kind) throw refuse(deferMisuseError(ownerName(target), methodName, kind));
47
47
  Reflect.defineMetadata(META.deferOptions, {
48
48
  ...options
49
49
  }, target, methodName);
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata';
2
- import { ownHandlerList } from './controller.decorator.js';
2
+ import { addOwnHandler } from './controller.decorator.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
4
  import { declaring, refuseOnClass } from '../util/refusal.util.js';
5
5
 
@@ -7,13 +7,12 @@ 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
8
  return declaring(function(target, propertyKey, _descriptor) {
9
9
  refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
10
- const handlers = ownHandlerList(META.eventHandlers, target);
11
- handlers.push({
10
+ // One the class inherits for the same method and event is replaced, as for every other kind of handler
11
+ addOwnHandler(META.eventHandlers, target, {
12
12
  event,
13
13
  method: propertyKey,
14
14
  once
15
- });
16
- Reflect.defineMetadata(META.eventHandlers, handlers, target);
15
+ }, (other)=>other.method === propertyKey && other.event === event);
17
16
  });
18
17
  }
19
18
  /**
@@ -5,7 +5,7 @@ import { Logger } from '../common/logger.js';
5
5
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
6
6
  import { isConstructible, describeValue } from '../util/value.util.js';
7
7
  import { META } from '../util/metadata-keys.js';
8
- import { declaring, refuseOnMethod } from '../util/refusal.util.js';
8
+ import { declaring, refuseOnMethod, decoratedName } from '../util/refusal.util.js';
9
9
 
10
10
  /**
11
11
  * Marks a class as an exception filter for the given error types.
@@ -88,7 +88,7 @@ const ORDINALS = [
88
88
  * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
89
89
  */ function UseFilter(...filters) {
90
90
  return declaring(function(target, propertyKey) {
91
- const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
91
+ const where = decoratedName(target, propertyKey);
92
92
  assertStageEntries('@UseFilter', 'filter', where, filters);
93
93
  // Decorators apply bottom-up, so a higher decorator's filters are tried first.
94
94
  if (propertyKey === undefined) {
@@ -1,47 +1,10 @@
1
1
  import 'reflect-metadata';
2
- import { handlerMethods, classLevelGuards, consumeDispatchMark, runDirectCall, declaringPrototype, classChain } from '../core/guard-runner.js';
2
+ import { applyGuards, recordGuards, handlerMethods, classLevelGuards, declaringPrototype } from '../core/guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
- import { wrapsMethod } from '../util/method-wrapper.util.js';
5
4
  import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
6
5
  import { META } from '../util/metadata-keys.js';
7
- import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
6
+ import { declaring, deprecatedOnMethod, decoratedName } from '../util/refusal.util.js';
8
7
 
9
- /**
10
- * Adds guards to a method's class or method list, then republishes the effective list under
11
- * `META.guards`, in the order they run: the bases' before the class's own, and within one class or method the
12
- * latest decorator first, since it wraps outermost.
13
- */ function recordGuards(key, guards, prototype, methodName) {
14
- const existing = Reflect.getOwnMetadata(key, prototype, methodName) ?? [];
15
- const bases = key === META.classGuards ? Reflect.getOwnMetadata(META.baseClassGuards, prototype, methodName) ?? 0 : 0;
16
- Reflect.defineMetadata(key, [
17
- ...existing.slice(0, bases),
18
- ...guards,
19
- ...existing.slice(bases)
20
- ], prototype, methodName);
21
- const [classGuards, inheritedGuards, methodGuards] = [
22
- META.classGuards,
23
- META.inheritedGuards,
24
- META.methodGuards
25
- ].map((list)=>Reflect.getOwnMetadata(list, prototype, methodName) ?? []);
26
- Reflect.defineMetadata(META.guards, [
27
- ...inheritedGuards,
28
- ...classGuards,
29
- ...methodGuards
30
- ], prototype, methodName);
31
- }
32
- /**
33
- * Wraps a method so a direct call runs its guards first: the wrapper entered first runs the whole
34
- * chain dispatch resolves for the handler, and lets the ones inside it through. A call from dispatch,
35
- * which has already run the guards, passes through every wrapper.
36
- */ function applyGuards(descriptor, prototype, propertyKey) {
37
- const originalMethod = descriptor.value;
38
- descriptor.value = wrapsMethod(async function(...args) {
39
- if (consumeDispatchMark(args[0], this, propertyKey)) return originalMethod.apply(this, args);
40
- return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args));
41
- }, originalMethod);
42
- const wrappers = Reflect.getOwnMetadata(META.guardWrappers, prototype, propertyKey) ?? 0;
43
- Reflect.defineMetadata(META.guardWrappers, wrappers + 1, prototype, propertyKey);
44
- }
45
8
  /**
46
9
  * The class's own descriptor for a handler. An inherited handler gets one that calls the inherited
47
10
  * method, starting from its guard lists and wrapper count, so class guards wrap it like an own method.
@@ -69,26 +32,6 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
69
32
  ...inherited
70
33
  };
71
34
  }
72
- /**
73
- * Gives each handler a controller declares itself the class-level guards of the classes it extends,
74
- * as its inherited handlers have: recorded under `META.guards`, and wrapped so a direct call
75
- * runs them when no other guard wraps the handler. `@Controller` calls this.
76
- */ function guardOwnHandlersWithBaseGuards(target) {
77
- const prototype = target.prototype;
78
- for (const methodName of handlerMethods(prototype)){
79
- const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
80
- // Inherited handlers have the chain of the class that declares them
81
- if (typeof descriptor?.value !== 'function' || Reflect.getOwnMetadata(META.inheritedFrom, prototype, methodName)) continue;
82
- // Base first, as dispatch runs them
83
- const guards = classChain(prototype, prototype).slice(1).reverse().flatMap(classLevelGuards);
84
- if (guards.length === 0) continue;
85
- if (!Reflect.getOwnMetadata(META.guardWrappers, prototype, methodName)) {
86
- applyGuards(descriptor, prototype, methodName);
87
- Object.defineProperty(prototype, methodName, descriptor);
88
- }
89
- recordGuards(META.inheritedGuards, guards, prototype, methodName);
90
- }
91
- }
92
35
  /**
93
36
  * Marks a class as a guard, which decides whether a handler runs.
94
37
  *
@@ -162,7 +105,7 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
162
105
  */ function UseGuard(...entries) {
163
106
  const guards = entries;
164
107
  return declaring(function(target, propertyKey, descriptor) {
165
- const where = propertyKey === undefined ? String(target?.name) : `${target.constructor.name}.${String(propertyKey)}`;
108
+ const where = decoratedName(target, propertyKey);
166
109
  assertStageEntries('@UseGuard', 'guard', where, guards);
167
110
  if (descriptor && propertyKey) {
168
111
  // Method Decorator
@@ -189,4 +132,4 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
189
132
  });
190
133
  }
191
134
 
192
- export { Guard, UseGuard, guardOwnHandlersWithBaseGuards };
135
+ export { Guard, UseGuard };
@@ -1,5 +1,5 @@
1
1
  import { inject } from 'inversify';
2
- import { declaring, refuseOnClass } from '../util/refusal.util.js';
2
+ import { declaring, refuseOnClass, refuse, decoratedName } from '../util/refusal.util.js';
3
3
 
4
4
  /**
5
5
  * Injects what a token provides into a constructor parameter.
@@ -30,6 +30,9 @@ import { declaring, refuseOnClass } from '../util/refusal.util.js';
30
30
  return declaring((target, propertyKey, parameterIndex)=>{
31
31
  // A constructor parameter has no property key either, but has an index
32
32
  if (typeof parameterIndex !== 'number') refuseOnClass('@Inject', target, propertyKey, 'a constructor parameter or a property');
33
+ else if (propertyKey !== undefined) {
34
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Inject goes on a constructor parameter or a property, not a method parameter.`));
35
+ }
33
36
  decorate(target, propertyKey, parameterIndex);
34
37
  });
35
38
  }
@@ -2,7 +2,7 @@ import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
- import { declaring, refuseOnMethod } from '../util/refusal.util.js';
5
+ import { declaring, refuseOnMethod, decoratedName } from '../util/refusal.util.js';
6
6
 
7
7
  /**
8
8
  * Marks a class as an interceptor, which wraps a handler to act before and after it.
@@ -76,7 +76,7 @@ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
76
76
  * @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
77
77
  */ function UseInterceptor(...interceptors) {
78
78
  return declaring(function(target, propertyKey) {
79
- const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
79
+ const where = decoratedName(target, propertyKey);
80
80
  assertStageEntries('@UseInterceptor', 'interceptor', where, interceptors);
81
81
  // Decorators apply bottom-up, so a higher decorator's interceptors go first, as with @UseGuard.
82
82
  if (propertyKey === undefined) {
@@ -2,7 +2,7 @@ import 'reflect-metadata';
2
2
  import { assertValidTheme } from '../core/theme-validation.js';
3
3
  import { THEMED_CLASSES } from '../core/theme-runtime.js';
4
4
  import { copyLayer } from '../core/theme-scope.js';
5
- import { declaring, refuse } from '../util/refusal.util.js';
5
+ import { declaring, decoratedName, refuse } from '../util/refusal.util.js';
6
6
  import { META } from '../util/metadata-keys.js';
7
7
 
8
8
  /**
@@ -37,8 +37,9 @@ import { META } from '../util/metadata-keys.js';
37
37
  */ function UseTheme(theme) {
38
38
  return declaring(function(target, propertyKey) {
39
39
  const onClass = propertyKey === undefined;
40
- const owner = onClass ? target : target.constructor;
41
- const where = `${owner.name}${onClass ? '' : `.${String(propertyKey)}`}: @UseTheme`;
40
+ // The class it themes: the target itself on a class or a static member, else the target's constructor
41
+ const owner = onClass || typeof target === 'function' ? target : target.constructor;
42
+ const where = `${decoratedName(target, propertyKey)}: @UseTheme`;
42
43
  assertValidTheme(theme, where);
43
44
  const key = onClass ? META.classTheme : META.methodTheme;
44
45
  const existing = onClass ? Reflect.getOwnMetadata(key, target) : Reflect.getOwnMetadata(key, target, propertyKey);
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { assertStageEntries } from '../core/stage-scope.js';
4
- import { declaring, refuseOnClass, refuse, refuseOnMethod } from '../util/refusal.util.js';
4
+ import { declaring, refuseOnClass, refuse, decoratedName, refuseOnMethod } from '../util/refusal.util.js';
5
5
  import { META } from '../util/metadata-keys.js';
6
6
 
7
7
  /**
@@ -43,15 +43,15 @@ import { META } from '../util/metadata-keys.js';
43
43
  refuseOnClass('@Validate', target, propertyKey);
44
44
  // Checked where it applies, so the refusal names the handler
45
45
  if (typeof schema?.['~standard']?.validate !== 'function') {
46
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: @Validate takes a Standard Schema, such as a zod, valibot or arktype schema.`));
46
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Validate takes a Standard Schema, such as a zod, valibot or arktype schema.`));
47
47
  }
48
48
  if (Reflect.hasOwnMetadata(META.methodValidation, target, propertyKey)) {
49
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: more than one @Validate; one @Validate per handler: combine the schemas into one.`));
49
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: more than one @Validate; one @Validate per handler: combine the schemas into one.`));
50
50
  }
51
51
  const inlinePipes = Object.values(options.pipes ?? {}).flatMap((entries)=>Array.isArray(entries) ? entries : [
52
52
  entries
53
53
  ]);
54
- assertStageEntries('@Validate', 'pipe', `${target.constructor.name}.${propertyKey}`, inlinePipes);
54
+ assertStageEntries('@Validate', 'pipe', decoratedName(target, propertyKey), inlinePipes);
55
55
  const metadata = {
56
56
  schema,
57
57
  pipes: options.pipes ?? {}
@@ -93,7 +93,7 @@ import { META } from '../util/metadata-keys.js';
93
93
  */ function UsePipe(key, ...pipes) {
94
94
  return declaring(function(target, propertyKey, _descriptor) {
95
95
  refuseOnClass('@UsePipe', target, propertyKey);
96
- assertStageEntries('@UsePipe', 'pipe', `${target.constructor.name}.${propertyKey}`, pipes);
96
+ assertStageEntries('@UsePipe', 'pipe', decoratedName(target, propertyKey), pipes);
97
97
  // Decorators apply bottom-up, so a higher @UsePipe's pipes go first, in the order they read.
98
98
  const existing = Reflect.getOwnMetadata(META.methodPipes, target, propertyKey) ?? [];
99
99
  Reflect.defineMetadata(META.methodPipes, [
@@ -1,4 +1,4 @@
1
- var version = "4.2.2";
1
+ var version = "4.2.3";
2
2
  var engines = {
3
3
  node: ">=22.13"
4
4
  };
@@ -1,25 +1,33 @@
1
1
  import { isPluralShape, CATALOGS, placeholderNames } from '../common/translator.js';
2
2
  import { MEOCORD_MESSAGES } from '../common/meocord-messages.js';
3
3
 
4
- /** Every message key of a catalog, and whether it is a plural. */ function leaves(catalog, prefix = '') {
4
+ /**
5
+ * Every message key of a catalog, and whether it is a plural. `reference` decides whether an object is a plural or a
6
+ * group, as the translator reads a translation by the default catalog's shapes; where it lacks a key, the object's own
7
+ * keys decide.
8
+ */ function leaves(catalog, prefix = '', reference = catalog) {
5
9
  return Object.entries(catalog).flatMap(([name, value])=>{
6
10
  const key = `${prefix}${name}`;
7
- if (typeof value === 'string') return [
11
+ // A null message is one where it stands, which the checks below read as missing
12
+ if (typeof value === 'string' || value === null) return [
8
13
  {
9
14
  key,
10
15
  plural: false
11
16
  }
12
17
  ];
13
- if (isPluralShape(value)) return [
18
+ const shape = typeof reference === 'object' && reference !== null && Object.hasOwn(reference, name) ? reference[name] : undefined;
19
+ const plural = typeof shape === 'object' && shape !== null ? isPluralShape(shape) : isPluralShape(value);
20
+ if (plural) return [
14
21
  {
15
22
  key,
16
23
  plural: true
17
24
  }
18
25
  ];
19
- return leaves(value, `${key}.`);
26
+ return leaves(value, `${key}.`, shape);
20
27
  });
21
28
  }
22
29
  /** A translation's text, which an empty string isn't: the translator reads `''` as missing, as tools export one untranslated. */ const isText = (value)=>typeof value === 'string' && value !== '';
30
+ /** A message the translator reads as missing: none, `null` or `''`, as tools export one untranslated. */ const isMissing = (value)=>value === undefined || value === null || value === '';
23
31
  /** The value at a key's path, through own properties only, so `constructor` or `toString` is not read off a prototype. */ function at(catalog, key) {
24
32
  return key.split('.').reduce((current, part)=>typeof current === 'object' && current !== null && Object.hasOwn(current, part) ? current[part] : undefined, catalog);
25
33
  }
@@ -44,11 +52,13 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
44
52
  *
45
53
  * Use it in a test for a team that wants no fallback in production. It reports the messages a locale lacks, the
46
54
  * messages the default catalog doesn't have, the plural forms a language needs but a plural lacks, such as `few` for
47
- * Russian, a translation of another shape than the default's, a text for a plural or a plural for a text, and each
48
- * `{param}` a translation uses that the message it translates doesn't take. An empty translation, `''`, counts as
49
- * lacking, as the translator reads it as missing. It reads the catalogs' own strings, so it checks the params of a
50
- * catalog from a plain variable or a JSON file too, which the compiler can't. An object counts as a plural only when
51
- * its every key is a plural category, so a group with an `other` key is a group.
55
+ * Russian, a translation of another shape than the default's, a text for a plural or a group, or a plural for a text, and each
56
+ * `{param}` a translation uses that the message it translates doesn't take. An empty translation, `''` or `null`, counts
57
+ * as lacking, as the translator reads it as missing. It reads the catalogs' own strings, so it checks the params of a
58
+ * catalog from a plain variable or a JSON file too, which the compiler can't. An object in the default catalog counts as
59
+ * a plural only when its every key is a plural category, so a group with an `other` key is a group, and a translation
60
+ * is read by the default catalog's shapes, as the translator reads it: one that keeps only a group's `other` message
61
+ * lacks the group's other messages.
52
62
  *
53
63
  * @remarks
54
64
  * MeoCord's own texts, a catalog's `meocord` group, fall back to English by design, so only a key MeoCord lacks, and
@@ -95,7 +105,7 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
95
105
  const categories = new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
96
106
  for (const { key, plural } of reference){
97
107
  const message = at(catalog, key);
98
- if (message === undefined || message === '') {
108
+ if (isMissing(message)) {
99
109
  problems.push(`missing ${key}`);
100
110
  continue;
101
111
  }
@@ -113,9 +123,11 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
113
123
  for (const name of stray)problems.push(`${key} takes no {${name}}; the default is "${shown(original)}"`);
114
124
  }
115
125
  if (options.meocord && !locale.startsWith('en-')) {
116
- for (const key of MEOCORD_KEYS)if (at(catalog, key) === undefined || at(catalog, key) === '') problems.push(`missing ${key}`);
126
+ for (const key of MEOCORD_KEYS)if (isMissing(at(catalog, key))) problems.push(`missing ${key}`);
117
127
  }
118
- for (const { key } of leaves(catalog)){
128
+ for (const { key } of leaves(catalog, '', defaultCatalog)){
129
+ // A message left out, as a null one is, that the default has is reported missing above, and is nothing extra
130
+ if (isMissing(at(catalog, key)) && at(defaultCatalog, key) !== undefined) continue;
119
131
  if (isMeoCordKey(key)) {
120
132
  if (!MEOCORD_KEYS.has(key)) problems.push(`${key} is not one of MeoCord's texts`);
121
133
  else {
@@ -123,7 +135,11 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
123
135
  const stray = strayParams(at(catalog, key), english);
124
136
  for (const name of stray)problems.push(`${key} takes no {${name}}: MeoCord's English is "${shown(english)}"`);
125
137
  }
126
- } else if (!referenceKeys.has(key)) problems.push(`${key} is not in the default catalog`);
138
+ } else if (!referenceKeys.has(key)) {
139
+ // A text where the default has a group: the key is the default's, in another shape
140
+ const original = at(defaultCatalog, key);
141
+ problems.push(typeof original === 'object' && original !== null ? `${key} should be a group, as the default is` : `${key} is not in the default catalog`);
142
+ }
127
143
  }
128
144
  if (problems.length > 0) gaps.push(` ${locale}: ${problems.join('; ')}`);
129
145
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { CooldownStore } from '../common/cooldown-store.js';
2
+ import { isDefaultBatchMethod } from '../common/cooldown-store.js';
3
3
 
4
4
  /** Long enough for a store over the network to answer each case, short enough that the suite takes seconds. */ const CASE_TIMEOUT_MS = 10000;
5
5
  const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
@@ -408,10 +408,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
408
408
  });
409
409
  }
410
410
  /** Whether a store overrides `consumeMany`, as a store that counts a batch at once does. */ function overridesConsumeMany(store) {
411
- return store.consumeMany !== CooldownStore.prototype.consumeMany;
411
+ return !isDefaultBatchMethod(store.consumeMany);
412
412
  }
413
413
  /** Whether a store overrides `peekMany`, as a store that can check a batch without recording it does. */ function overridesPeekMany(store) {
414
- return store.peekMany !== CooldownStore.prototype.peekMany;
414
+ return !isDefaultBatchMethod(store.peekMany);
415
415
  }
416
416
 
417
417
  export { testCooldownStore };