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
@@ -21,34 +21,70 @@ const UNIT_MS = lookupTable({
21
21
  w: 604800000
22
22
  });
23
23
  /** A number written in full, `whole` for an integer: `undefined` for anything else. */ function number(word, whole) {
24
- if (!(whole ? /^[+-]?\d+$/ : /^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i).test(word)) return undefined;
24
+ // Each run of digits splits only one way, so refusing a word takes time linear in its length
25
+ if (!(whole ? /^[+-]?\d+$/ : /^[+-]?(\d+(?:\.\d*)?|\.\d+)(e[+-]?\d+)?$/i).test(word)) return undefined;
25
26
  const value = Number(word);
26
27
  return Number.isFinite(value) && (!whole || Number.isSafeInteger(value)) ? value : undefined;
27
28
  }
28
29
  /** `yes`, `no`, `true`, `false`, `on` or `off`, in any case, as a boolean. */ const bool = (word)=>BOOLEANS[word.toLowerCase()];
29
- /** A length of time written as amounts and units, such as `90s`, `2h30m` or `1.5d`, in milliseconds. */ function duration(word) {
30
- const parts = word.toLowerCase().match(/(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g);
30
+ /**
31
+ * A length of time written as amounts and units, such as `90s`, `2h30m` or `1.5d`, in milliseconds: `undefined` for one
32
+ * too long to add up.
33
+ */ function duration(word) {
34
+ // Sticky: each part starts where the last ended, so the scan stops at the first that doesn't and reads each part once
35
+ const parts = word.toLowerCase().match(/(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/gy);
31
36
  if (!parts || parts.join('') !== word.toLowerCase()) return undefined;
32
- return parts.reduce((total, part)=>{
37
+ const total = parts.reduce((sum, part)=>{
33
38
  const [, amount, unit] = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(part);
34
- return total + Number(amount) * UNIT_MS[unit];
39
+ return sum + Number(amount) * UNIT_MS[unit];
35
40
  }, 0);
41
+ // Too long to add up, as number() refuses a value past the largest number
42
+ return Number.isFinite(total) ? total : undefined;
36
43
  }
37
44
  /** The largest snowflake: Discord's IDs are unsigned 64-bit integers. */ const MAX_SNOWFLAKE = 2n ** 64n - 1n;
45
+ /** The fewest and most digits a snowflake has, and the words of that many digits, with no leading zero. */ const SNOWFLAKE_DIGITS = {
46
+ min: 17,
47
+ max: 20
48
+ };
49
+ const SNOWFLAKE = new RegExp(`^[1-9]\\d{${SNOWFLAKE_DIGITS.min - 1},${SNOWFLAKE_DIGITS.max - 1}}$`);
38
50
  /**
39
51
  * A Discord ID, kept as its text: 17 to 20 digits, no leading zero, up to the largest 64-bit value. An ID's top 42 bits
40
52
  * are milliseconds since 2015-01-01, so every ID made from 2015-01-28 on has 17 digits or more, and none is a safe
41
53
  * integer.
42
- */ const snowflake = (word)=>/^[1-9]\d{16,19}$/.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
43
- /** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(word) ? word : undefined;
54
+ */ const snowflake = (word)=>SNOWFLAKE.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
55
+ /** A UUID's canonical 8-4-4-4-12 form, 36 characters with its dashes. */ const UUID = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
56
+ const UUID_LENGTH = 36;
57
+ /** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>UUID.test(word) ? word : undefined;
44
58
  /** The words a type such as `on|off` chooses from, or `undefined` for any other type. */ const choicesOf = (type)=>type.includes('|') ? type.split('|') : undefined;
45
- /** The types a component's customId segment can take: each parsed from the segment alone, with no request. */ const SEGMENT_TYPES = lookupTable({
46
- string: (segment)=>segment,
47
- int: (segment)=>number(segment, true),
48
- number: (segment)=>number(segment, false),
49
- bool,
50
- snowflake,
51
- uuid
59
+ /** The shortest segment of any type: one character. */ const SHORTEST_SEGMENT = 1;
60
+ /**
61
+ * The types a component's customId segment can take: each parsed from the segment alone, with no request, and the
62
+ * fewest characters a segment of it has, which the check for a pattern no customId can match counts.
63
+ */ const SEGMENT_TYPES = lookupTable({
64
+ string: {
65
+ parse: (segment)=>segment,
66
+ shortest: SHORTEST_SEGMENT
67
+ },
68
+ int: {
69
+ parse: (segment)=>number(segment, true),
70
+ shortest: SHORTEST_SEGMENT
71
+ },
72
+ number: {
73
+ parse: (segment)=>number(segment, false),
74
+ shortest: SHORTEST_SEGMENT
75
+ },
76
+ bool: {
77
+ parse: bool,
78
+ shortest: Math.min(...Object.keys(BOOLEANS).map((word)=>word.length))
79
+ },
80
+ snowflake: {
81
+ parse: snowflake,
82
+ shortest: SNOWFLAKE_DIGITS.min
83
+ },
84
+ uuid: {
85
+ parse: uuid,
86
+ shortest: UUID_LENGTH
87
+ }
52
88
  });
53
89
  /**
54
90
  * Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
@@ -57,7 +93,12 @@ const UNIT_MS = lookupTable({
57
93
  /** A customId segment as its type's value, words to choose from compared as written: `undefined` when it is not one. */ function parseSegment(type, segment) {
58
94
  const choices = choicesOf(type);
59
95
  if (choices) return choices.includes(segment) ? segment : undefined;
60
- return SEGMENT_TYPES[type](segment);
96
+ return SEGMENT_TYPES[type].parse(segment);
97
+ }
98
+ /** The fewest characters a customId segment of `type` has: words to choose from count their shortest word. */ function shortestSegment(type) {
99
+ const choices = choicesOf(type);
100
+ if (choices) return Math.min(...choices.map((word)=>word.length));
101
+ return SEGMENT_TYPES[type]?.shortest ?? SHORTEST_SEGMENT;
61
102
  }
62
103
 
63
- export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment, snowflake, uuid };
104
+ export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment, shortestSegment, snowflake, uuid };
@@ -1,19 +1,20 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { Logger } from '../common/logger.js';
3
- import './theme-scope.js';
4
- import './theme-validation.js';
3
+ import '../common/theme.js';
5
4
  import '../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../common/deprecation.js';
8
8
  import '../common/response/response-state.js';
9
+ import './theme-scope.js';
9
10
  import '../common/errors.js';
11
+ import '../common/cooldown-store.js';
10
12
  import '../decorator/controller.decorator.js';
11
13
  import './scalar-types.js';
12
- import 'path';
13
- import 'node:fs';
14
- import 'node:url';
14
+ import '../util/refusal.util.js';
15
15
  import '../common/translator.js';
16
16
  import '../common/meocord-text.js';
17
+ import './theme-resolvers.js';
17
18
 
18
19
  /** Where each shard's client keeps the function that runs a `ShardContext.call` in that shard. */ const SHARD_CALL_KEY = Symbol.for('meocord.shardCall');
19
20
  const packArgs = (args)=>Object.assign({
@@ -122,11 +123,11 @@ async function withTimeout(promise, ms, what) {
122
123
  *
123
124
  * With process sharding that is one result per shard; with one process, a single result listing every
124
125
  * shard. The arguments and the result pass as JSON in every mode, one process and tests included, so a
125
- * `Date` arrives as a string and a `Map` as `{}` wherever it runs, and a value JSON cannot carry, such as a
126
- * `BigInt`, fails the call. A method whose params JSON would change, such as one taking a `Date`, cannot be called:
127
- * the argument is refused, naming what to declare instead. An `undefined` argument arrives as `undefined`. A process
128
- * that throws, lacks the service, or does not answer within 10 seconds gives an error result instead of failing the
129
- * others.
126
+ * `Date` arrives as a string, a `Map` as `{}` and a class instance as its own fields, without its getters, wherever
127
+ * it runs, and a value JSON cannot carry, such as a `BigInt`, fails the call. A method whose params JSON would
128
+ * change, such as one taking a `Date`, cannot be called: the argument is refused, naming what to declare instead. An
129
+ * `undefined` argument arrives as `undefined`. A process that throws, lacks the service, or does not answer within
130
+ * 10 seconds gives an error result instead of failing the others.
130
131
  *
131
132
  * @param service - The controller, the service, or a class a provider stands in for; each process resolves its own
132
133
  * instance.
@@ -1,4 +1,5 @@
1
1
  import { isExplainedError, markExplained } from '../common/explained-error.js';
2
+ import { processState } from './process-state.js';
2
3
  import { REFUSED_CODE } from './shard-messages.js';
3
4
  import { sendToParent } from '../util/parent-send.util.js';
4
5
  import { isRefusal, describeRefusal, handOffRefusal } from '../util/refusal.util.js';
@@ -9,14 +10,13 @@ import { isRefusal, describeRefusal, handOffRefusal } from '../util/refusal.util
9
10
  */ async function tellManager(message) {
10
11
  await sendToParent(message);
11
12
  }
12
- /** Whether this shard is already ending, so a failure reported twice, as one going on uncaught is, ends it once. */ let ending = false;
13
13
  /**
14
14
  * Ends a shard whose startup failed, which its manager restarts unless it was told the failure is one every shard
15
15
  * meets: a refusal, told here first, which the manager logs before stopping them all, or a login Discord refused, told
16
16
  * as it failed. The exit waits until the caller has handled the error and the manager has the reason.
17
17
  */ function endFailedShard(error) {
18
- if (ending) return;
19
- ending = true;
18
+ if (processState.shardEnding) return;
19
+ processState.shardEnding = true;
20
20
  const refused = isRefusal(error);
21
21
  const reason = refused && describeRefusal(error, process.cwd());
22
22
  if (refused) {
@@ -1,10 +1,11 @@
1
1
  import { fetchRecommendedShardCount, ShardingManager, REST, Routes } from 'discord.js';
2
2
  import { Logger, hideInLogs } from '../common/logger.js';
3
- import './theme-scope.js';
4
- import './theme-validation.js';
3
+ import '../common/theme.js';
5
4
  import '../common/decorator.js';
6
5
  import 'reflect-metadata';
6
+ import '../common/deprecation.js';
7
7
  import '../common/response/response-state.js';
8
+ import './theme-scope.js';
8
9
  import '../common/errors.js';
9
10
  import { MemoryCooldownStore } from '../common/cooldown-store.js';
10
11
  import 'node:crypto';
@@ -13,10 +14,11 @@ import './scalar-types.js';
13
14
  import { answerCooldown } from '../common/sharded-cooldown-store.js';
14
15
  import '../common/translator.js';
15
16
  import '../common/meocord-text.js';
17
+ import './theme-resolvers.js';
16
18
  import { registerCommands } from './command-registration.js';
17
19
  import { SHUTDOWN_MARGIN_MS, shutdownTimeoutOf } from '../util/shutdown-timeout.util.js';
18
20
  import { bundleEntry } from '../util/bundle-entry.util.js';
19
- import { FORCE_REGISTER_ENV } from '../util/registration-mode.util.js';
21
+ import { FORCE_REGISTER_ENV, registersAsDevelopment } from '../util/registration-mode.util.js';
20
22
  import { isShardMessage, REFUSED_CODE } from './shard-messages.js';
21
23
  import { tokenMessage, isRefusedToken } from './login-failure.js';
22
24
  import { onDevRunnerStop, tellDevRunner, underDevRunner } from '../util/dev-runner.util.js';
@@ -54,7 +56,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
54
56
  return this.exit(1);
55
57
  }
56
58
  this.logger.log('Starting shards in separate processes...');
57
- if (!await this.register()) return this.exitForLogin();
59
+ const registered = await this.register();
60
+ // A stop that came while it waited ends the start here: nothing after it asks Discord, spawns, listens or exits
61
+ if (this.stopping) return;
62
+ if (!registered) return this.exitForLogin();
58
63
  process.on('SIGINT', ()=>void this.stopAndExit());
59
64
  process.on('SIGTERM', ()=>void this.stopAndExit());
60
65
  // How `meocord start --dev` stops the bot to restart it; no stop request of the user's, whose first Ctrl+C joins it
@@ -64,6 +69,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
64
69
  const shards = config.sharding?.shards ?? 'auto';
65
70
  total = shards === 'auto' ? await (this.options.recommendedShardCount ?? fetchRecommendedShardCount)(token) : shards;
66
71
  } catch (error) {
72
+ if (this.stopping) return;
67
73
  if (isRefusedToken(error)) {
68
74
  this.logger.error(tokenMessage(token));
69
75
  this.logger.debug('Asking for the shard count failed:', error);
@@ -72,6 +78,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
72
78
  }
73
79
  return this.exitForLogin();
74
80
  }
81
+ if (this.stopping) return;
75
82
  const manager = (this.options.createManager ?? ((path, opts)=>new ShardingManager(path, opts)))(file, {
76
83
  token,
77
84
  totalShards: total,
@@ -105,13 +112,15 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
105
112
  try {
106
113
  const rest = this.options.createRest?.(token) ?? new REST().setToken(token);
107
114
  const applicationId = (await rest.get(Routes.currentApplication())).id;
115
+ // Stopped while it asked: the commands are left as they are
116
+ if (this.stopping) return true;
108
117
  await registerCommands({
109
118
  rest,
110
119
  applicationId,
111
120
  controllerClasses,
112
121
  logger: this.logger,
113
122
  config: config.commands,
114
- development: process.env.NODE_ENV === 'development',
123
+ development: registersAsDevelopment(),
115
124
  force: process.env[FORCE_REGISTER_ENV] === '1'
116
125
  });
117
126
  } catch (error) {
@@ -153,7 +162,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
153
162
  if (this.now() - state.spawnedAt >= RESPAWN_RESET_MS) state.attempts = 0;
154
163
  const delay = Math.min(RESPAWN_BASE_MS * 2 ** state.attempts, RESPAWN_CAP_MS);
155
164
  state.attempts++;
156
- this.logger.warn(`Shard ${shard.id} exited with code ${child?.exitCode ?? 'unknown'}; restarting it in ${delay} ms.`);
165
+ const how = child?.signalCode ? `on ${child.signalCode}` : `with code ${child?.exitCode ?? 'unknown'}`;
166
+ this.logger.warn(`Shard ${shard.id} exited ${how}; restarting it in ${delay} ms.`);
157
167
  state.timer = setTimeout(()=>{
158
168
  state.timer = undefined;
159
169
  if (!this.stopping && !this.fatal) void this.spawn(shard);
@@ -5,6 +5,7 @@ import { reachableClasses, assertTypedParameters } from './providers.js';
5
5
  import { themeResolverClass } from './theme-resolvers.js';
6
6
  import { isAppClassToken } from './lifecycle-order.js';
7
7
  import { ExecutionContext } from '../common/execution-context.js';
8
+ import { getEventHandlers } from '../decorator/event.decorator.js';
8
9
  import { META } from '../util/metadata-keys.js';
9
10
  import { startupError } from '../util/refusal.util.js';
10
11
 
@@ -63,19 +64,38 @@ import { startupError } from '../util/refusal.util.js';
63
64
  seen.add(cls);
64
65
  return injectedTokens(cls).some((token)=>token === ExecutionContext || isAppClassToken(token) && !providers.has(token) && !seen.has(token) && reachesContext(token, providers, seen));
65
66
  }
66
- /**
67
- * The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
68
- * app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
69
- * made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
70
- * is not made.
71
- */ function stageDependencies({ decorators }, providers, presenter) {
67
+ /** The app's classes the stages and presenter inject. */ function stageInjected({ decorators }, presenter) {
72
68
  const stages = [
73
69
  ...decorators
74
70
  ].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
75
71
  if (typeof presenter === 'function') stages.push(presenter);
76
72
  return [
77
73
  ...new Set(stages.flatMap((stage)=>injectedTokens(stage)))
78
- ].filter(isAppClassToken).filter((cls)=>!reachesContext(cls, providers));
74
+ ].filter(isAppClassToken);
75
+ }
76
+ /**
77
+ * The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
78
+ * app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
79
+ * made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
80
+ * is not made.
81
+ */ function stageDependencies(startup, providers, presenter) {
82
+ return stageInjected(startup, presenter).filter((cls)=>!reachesContext(cls, providers));
83
+ }
84
+ /**
85
+ * A warning for each `@On` and `@Once` handler on a class the stages and presenter reach that is made for each call,
86
+ * as it reaches `ExecutionContext`: no one instance of it receives events.
87
+ */ function perCallEventWarnings(startup, providers, presenter) {
88
+ const perCall = new Set();
89
+ // What a class made for each call injects is made for each call too only if it also reaches the context
90
+ const visit = (cls)=>{
91
+ if (perCall.has(cls) || providers.has(cls) || !reachesContext(cls, providers)) return;
92
+ perCall.add(cls);
93
+ injectedTokens(cls).filter(isAppClassToken).forEach(visit);
94
+ };
95
+ stageInjected(startup, presenter).forEach(visit);
96
+ return [
97
+ ...perCall
98
+ ].flatMap((cls)=>getEventHandlers(cls.prototype).map(({ event, method, once })=>`${cls.name}.${method}: @${once ? 'Once' : 'On'}('${event}') is on a class made for each call, as it injects ` + 'ExecutionContext or a class that does, so it receives no events. The next major version (5.0) refuses it; ' + 'move the handler to a service that does not.'));
79
99
  }
80
100
  /** How a container checks a guard first met at a call: the guard and what it injects, as the startup checks would. */ function lateGuardCheck(providers, translator) {
81
101
  return (guard)=>assertStartupClasses({
@@ -98,4 +118,4 @@ import { startupError } from '../util/refusal.util.js';
98
118
  for (const needsOne of classes.filter((cls)=>injectedTokens(cls).includes(Translator)))startupError(missingTranslatorError(needsOne));
99
119
  }
100
120
 
101
- export { assertStartupClasses, lateGuardCheck, stageDependencies, startupClasses };
121
+ export { assertStartupClasses, lateGuardCheck, perCallEventWarnings, stageDependencies, startupClasses };
@@ -1,6 +1,9 @@
1
+ import { User } from 'discord.js';
1
2
  import { Logger } from '../common/logger.js';
2
3
  import { themeProblems } from './theme-validation.js';
3
4
  import { outsideThemeScope, copyLayer } from './theme-scope.js';
5
+ import { quoteForLog } from '../util/user-text.util.js';
6
+ import { shared } from '../util/shared-state.util.js';
4
7
 
5
8
  /** How long a resolver's result is kept, in seconds, unless `themeCache.ttlSeconds` says otherwise. */ const DEFAULT_THEME_TTL_SECONDS = 300;
6
9
  /** How many servers' results are kept, unless `themeCache.maxGuilds` says otherwise. */ const DEFAULT_THEME_MAX_GUILDS = 10000;
@@ -113,15 +116,25 @@ const logger = new Logger('Theme');
113
116
  ]).finally(()=>clearTimeout(timer));
114
117
  }
115
118
  /**
116
- * A result as a layer: copied, then checked; one with problems is left out, with a warning once per id when `report`.
117
- * `null`, as a database gives for a missing row, is no theme, as `undefined` is.
119
+ * A result as a layer: copied, then checked; one with problems, or that throws as it is read, is left out, with a
120
+ * warning once per id when `report`. `null`, as a database gives for a missing row, is no theme, as `undefined` is.
118
121
  */ accept(id, result, report) {
119
122
  if (result === undefined || result === null) return undefined;
120
- const layer = copyLayer(result);
121
- // A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
122
- const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`, {
123
- forLog: true
124
- });
123
+ const where = `themeFor.${this.kind} for ${this.kind} ${id}`;
124
+ let layer;
125
+ let problems;
126
+ try {
127
+ layer = copyLayer(result);
128
+ // A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
129
+ problems = themeProblems(layer, where, {
130
+ forLog: true
131
+ });
132
+ } catch (error) {
133
+ // A cycle overflows the copy, and a getter may throw: the result is left out as one with problems is
134
+ problems = [
135
+ `${where}: theme cannot be read: ${quoteForLog(String(error?.message ?? error))}`
136
+ ];
137
+ }
125
138
  if (problems.length === 0) {
126
139
  if (report) this.warned.delete(id);
127
140
  return layer;
@@ -250,13 +263,14 @@ const logger = new Logger('Theme');
250
263
  const idOf = (value)=>typeof value === 'string' ? value : undefined;
251
264
  /**
252
265
  * The server and user a call comes from: an interaction's or a message's server and its user or author; a
253
- * reaction's message's server and the user who reacted, which a reaction handler gets second; an event argument's
254
- * server and user, when it has them.
266
+ * reaction's message's server and the user who reacted, which a reaction handler gets second as `{ user }` and a
267
+ * reaction event as the `User`; an event argument's server and user, when it has them.
255
268
  */ function themeTargets(args) {
256
269
  const [first, second] = args;
257
270
  if (!first || typeof first !== 'object') return {};
258
271
  const guildId = idOf(first.guildId) ?? idOf(first.guild?.id) ?? idOf(first.message?.guildId);
259
- const userId = idOf(first.user?.id) ?? idOf(first.author?.id) ?? idOf(second?.user?.id);
272
+ // Only a User's own id: an event of an old and a new channel, role or message would give the new one's otherwise
273
+ const userId = idOf(first.user?.id) ?? idOf(first.author?.id) ?? idOf(second?.user?.id) ?? (second instanceof User ? idOf(second.id) : undefined);
260
274
  return {
261
275
  guildId,
262
276
  userId
@@ -283,7 +297,8 @@ const idOf = (value)=>typeof value === 'string' ? value : undefined;
283
297
  user
284
298
  ];
285
299
  }
286
- const stores = new WeakMap();
300
+ // One per process for this meocord version, as the rest of an app's theme state is
301
+ const stores = shared('themeCaches', ()=>new WeakMap());
287
302
  /**
288
303
  * An app's cache of the themes `@MeoCord({ themeFor })` looked up, by server and by user.
289
304
  *
@@ -1,12 +1,14 @@
1
1
  import 'reflect-metadata';
2
- import { stageClasses, sourcePrototype } from './guard-runner.js';
2
+ import { stageClasses, stageOwners } from './guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
4
  import { lookupLayers, resolverCaches, ThemeCache, attachThemeCache } from './theme-resolvers.js';
5
- import { releaseAmbientTheme, claimAmbientTheme, mergeTheme, defaultTheme, themeLayersVersion, ambientThemeVersion, ownsAmbientTheme, hasAmbientTheme, setThemeOutsideCalls, useTheme } from './theme-scope.js';
5
+ import { setThemeOutsideCalls, releaseAmbientTheme, claimAmbientTheme, defaultTheme, mergeTheme, themeLayersVersion, ambientThemeVersion, ownsAmbientTheme, useTheme } from './theme-scope.js';
6
6
  import { META } from '../util/metadata-keys.js';
7
+ import { shared } from '../util/shared-state.util.js';
7
8
 
8
- /** Classes with a `@UseTheme`, on the class or one of its methods. */ const THEMED_CLASSES = new WeakSet();
9
- const apps = new WeakMap();
9
+ /** Classes with a `@UseTheme`, on the class or one of its methods, from either build of this version. */ const THEMED_CLASSES = shared('themedClasses', ()=>new WeakSet());
10
+ // One per process for this meocord version, so an app started through one build finds its theme from the other
11
+ const apps = shared('appThemes', ()=>new WeakMap());
10
12
  /** The classes on `cls`'s prototype chain, itself first. */ function chainOf(cls) {
11
13
  const chain = [];
12
14
  for(let prototype = cls.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
@@ -37,6 +39,13 @@ const apps = new WeakMap();
37
39
  }
38
40
  attachThemeCache(container.get(ThemeCache), resolvers);
39
41
  }
42
+ /** Counts `classes`, whose handlers are prepared once the app's providers are made, among those a theme can vary by. */ function prepareLateThemes(container, classes) {
43
+ const themes = apps.get(container);
44
+ if (!themes || themes.varies || !classes.some((cls)=>chainOf(cls).some((link)=>THEMED_CLASSES.has(link)))) return;
45
+ themes.varies = true;
46
+ // Built again on its next read, with each handler's own theme
47
+ themes.version = -1;
48
+ }
40
49
  /** The app's themes, built again when the layers beneath the app's have changed. */ function current(container) {
41
50
  const themes = apps.get(container);
42
51
  if (themes && themes.version !== themeLayersVersion()) {
@@ -51,13 +60,12 @@ const apps = new WeakMap();
51
60
  }
52
61
  /**
53
62
  * Decides whether the app's calls need a scope, again whenever the layers beneath it or the app read outside a call
54
- * change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it
55
- * needs one unless that is its own theme: when the app owns it, or when no app does and the app's theme is MeoCord's
56
- * defaults.
63
+ * change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it needs
64
+ * one unless its app owns that theme: another app may claim it while the call runs, and the call keeps the theme it
65
+ * started with.
57
66
  */ function rescope(container, themes) {
58
67
  themes.ambientVersion = ambientThemeVersion();
59
- const readsOwnTheme = ownsAmbientTheme(container) || !hasAmbientTheme() && themes.app === defaultTheme();
60
- themes.scoped = themes.varies || !readsOwnTheme ? themes.app : undefined;
68
+ themes.scoped = themes.varies || !ownsAmbientTheme(container) ? themes.app : undefined;
61
69
  }
62
70
  /** The app's theme: MeoCord's defaults and the app's `@MeoCord({ theme })`, or the defaults for a container with none. */ function appTheme(container) {
63
71
  return current(container)?.app ?? defaultTheme();
@@ -75,8 +83,10 @@ const apps = new WeakMap();
75
83
  for (const cls of stageClasses(prototype, methodName)){
76
84
  theme = mergeTheme(theme, Reflect.getOwnMetadata(META.classTheme, cls));
77
85
  }
78
- const source = sourcePrototype(prototype, methodName);
79
- if (source) theme = mergeTheme(theme, Reflect.getOwnMetadata(META.methodTheme, source, methodName));
86
+ // The method's layer over its class's, and an override's over the method it overrides
87
+ for (const owner of stageOwners(prototype, methodName)){
88
+ theme = mergeTheme(theme, Reflect.getOwnMetadata(META.methodTheme, owner, methodName));
89
+ }
80
90
  byMethod.set(methodName, theme);
81
91
  return theme;
82
92
  }
@@ -115,7 +125,7 @@ const apps = new WeakMap();
115
125
  ready
116
126
  };
117
127
  }
118
- const clientApps = new WeakMap();
128
+ const clientApps = shared('clientThemes', ()=>new WeakMap());
119
129
  /** Records which app a client's interactions come to, so an answer outside any call can find its theme. */ function registerClientTheme(client, container) {
120
130
  clientApps.set(client, container);
121
131
  }
@@ -139,4 +149,4 @@ setThemeOutsideCalls((interaction)=>{
139
149
  releaseAmbientTheme(container);
140
150
  }
141
151
 
142
- export { THEMED_CLASSES, appTheme, beginCallTheme, claimAmbientAppTheme, configureThemes, registerClientTheme, releaseAmbientAppTheme };
152
+ export { THEMED_CLASSES, appTheme, beginCallTheme, claimAmbientAppTheme, configureThemes, prepareLateThemes, registerClientTheme, releaseAmbientAppTheme };
@@ -64,50 +64,50 @@ const merges = new WeakMap();
64
64
  if (!merged) byLayer.set(layer, merged = deepFreeze(mergeInto(theme, layer)));
65
65
  return merged;
66
66
  }
67
- // Written by the deprecated `Theme` statics: beneath every theme an app sets
68
- let legacyLayer;
69
- let legacyVersion = 0;
70
- let defaults = DEFAULT_THEME;
67
+ /**
68
+ * The theme state outside a call, one for both builds of this version so either reads what the other set: the defaults
69
+ * with what the deprecated `Theme` statics set beneath every theme, the app whose theme is read outside a call, and how
70
+ * an answer outside a call finds its theme.
71
+ */ const state = shared('themeState', ()=>({
72
+ defaults: DEFAULT_THEME,
73
+ legacyVersion: 0,
74
+ ambient: undefined,
75
+ ambientVersion: 0,
76
+ themeOutsideCalls: undefined
77
+ }));
71
78
  /** MeoCord's defaults, with what the deprecated `Theme` statics set over them. */ function defaultTheme() {
72
- return defaults;
79
+ return state.defaults;
73
80
  }
74
81
  /** The number of times the legacy layer has changed, so a theme built on an older one is built again. */ function themeLayersVersion() {
75
- return legacyVersion;
82
+ return state.legacyVersion;
76
83
  }
77
84
  /** Sets the layer the deprecated `Theme` statics write, merged over MeoCord's defaults. */ function setLegacyThemeLayer(layer) {
78
- legacyLayer = layer === undefined ? undefined : copyLayer(layer);
79
- defaults = mergeTheme(DEFAULT_THEME, legacyLayer);
80
- legacyVersion++;
85
+ state.defaults = mergeTheme(DEFAULT_THEME, layer === undefined ? undefined : copyLayer(layer));
86
+ state.legacyVersion++;
81
87
  }
82
- // The theme outside any call: the one of the app or testing module that claimed it, until it gives it up
83
- let ambient;
84
- let ambientVersion = 0;
85
88
  /** The number of times the app read outside a call has changed, so each app decides again whether its calls need a scope. */ function ambientThemeVersion() {
86
- return ambientVersion;
87
- }
88
- /** Whether some app's theme is the one read outside a call. */ function hasAmbientTheme() {
89
- return ambient !== undefined;
89
+ return state.ambientVersion;
90
90
  }
91
91
  /**
92
92
  * Makes an app's theme the one read outside a call, when no other app has: a bot runs one app, and code it runs
93
93
  * outside a handler, such as a scheduled job, then reads that app's theme. A second app in the same process keeps
94
94
  * its theme to its own calls.
95
95
  */ function claimAmbientTheme(owner, theme) {
96
- if (ambient && ambient.owner !== owner) return false;
97
- if (!ambient) ambientVersion++;
98
- ambient = {
96
+ if (state.ambient && state.ambient.owner !== owner) return false;
97
+ if (!state.ambient) state.ambientVersion++;
98
+ state.ambient = {
99
99
  owner,
100
100
  theme
101
101
  };
102
102
  return true;
103
103
  }
104
104
  /** Gives up the theme read outside a call, when `owner` has it: its start failed, or it has shut down. */ function releaseAmbientTheme(owner) {
105
- if (ambient?.owner !== owner) return;
106
- ambient = undefined;
107
- ambientVersion++;
105
+ if (state.ambient?.owner !== owner) return;
106
+ state.ambient = undefined;
107
+ state.ambientVersion++;
108
108
  }
109
109
  /** Whether `owner` is the app whose theme is read outside a call. */ function ownsAmbientTheme(owner) {
110
- return ambient?.owner === owner;
110
+ return state.ambient?.owner === owner;
111
111
  }
112
112
  /** Runs `fn` with `theme` as the theme of the call, for everything it runs, awaits or starts. */ function runWithTheme(theme, fn) {
113
113
  return scope.run({
@@ -123,10 +123,8 @@ let ambientVersion = 0;
123
123
  /** Runs `fn` in `themeScope`, whose theme may be replaced while it runs, as a per-server theme is found. */ function runInThemeScope(themeScope, fn) {
124
124
  return scope.run(themeScope, fn);
125
125
  }
126
- // Set by the runtime: the theme an app gives an interaction answered outside any call, as in a collector
127
- let themeOutsideCalls;
128
126
  /** Sets how an answer outside any call finds its theme: from the interaction's app, server and user. */ function setThemeOutsideCalls(resolve) {
129
- themeOutsideCalls = resolve;
127
+ state.themeOutsideCalls = resolve;
130
128
  }
131
129
  /**
132
130
  * The theme an answer to `interaction` uses: the running call's, or, outside any call, as in a collector's callback,
@@ -134,7 +132,7 @@ let themeOutsideCalls;
134
132
  */ function themeForInteraction(interaction) {
135
133
  const call = scope.getStore();
136
134
  if (call) return call.theme;
137
- return themeOutsideCalls?.(interaction) ?? useTheme();
135
+ return state.themeOutsideCalls?.(interaction) ?? useTheme();
138
136
  }
139
137
  /**
140
138
  * The theme of the running call, with every role present.
@@ -163,7 +161,7 @@ let themeOutsideCalls;
163
161
  *
164
162
  * @group Responses
165
163
  */ function useTheme() {
166
- return scope.getStore()?.theme ?? ambient?.theme() ?? defaults;
164
+ return scope.getStore()?.theme ?? state.ambient?.theme() ?? state.defaults;
167
165
  }
168
166
  /**
169
167
  * Makes a function run in the theme of the call that binds it, wherever it is called from later.
@@ -198,4 +196,4 @@ let themeOutsideCalls;
198
196
  };
199
197
  }
200
198
 
201
- export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme, hasAmbientTheme, mergeTheme, outsideThemeScope, ownsAmbientTheme, releaseAmbientTheme, runInThemeScope, runWithTheme, setLegacyThemeLayer, setThemeOutsideCalls, themeForInteraction, themeLayersVersion, useTheme };
199
+ export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme, mergeTheme, outsideThemeScope, ownsAmbientTheme, releaseAmbientTheme, runInThemeScope, runWithTheme, setLegacyThemeLayer, setThemeOutsideCalls, themeForInteraction, themeLayersVersion, useTheme };
@@ -51,7 +51,7 @@ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an arr
51
51
  const prototype = Object.getPrototypeOf(value);
52
52
  return prototype === Object.prototype || prototype === null;
53
53
  };
54
- const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(), since anything else would replace the whole";
54
+ const PLAIN = "give a plain object: a database row's plain form, such as Mongoose's toObject() or Sequelize's get({ plain: true }), or an object literal of the values you set, since anything else would replace the whole";
55
55
  /** What a non-plain object is: by its class, or as one without a plain prototype when it has none named. */ const describeInstance = (value)=>{
56
56
  const name = value.constructor?.name;
57
57
  return typeof name === 'string' && name ? `a ${name}` : 'an object without a plain prototype';
@@ -8,10 +8,12 @@ import { assertObservers } from '../core/observer-runner.js';
8
8
  import { assertValidTheme, themeForProblem } from '../core/theme-validation.js';
9
9
  import { copyLayer } from '../core/theme-scope.js';
10
10
  import { declaring, deprecatedOnMethod, refuse } from '../util/refusal.util.js';
11
- import { timeoutProblem } from '../util/timer-limit.util.js';
11
+ import { timeoutProblem, timerKeepsSeconds, MAX_TIMER_SECONDS } from '../util/timer-limit.util.js';
12
+ import { Logger } from '../common/logger.js';
12
13
  import { isConstructor, describeValue } from '../util/value.util.js';
13
14
  import { META } from '../util/metadata-keys.js';
14
15
 
16
+ const logger = new Logger('MeoCord');
15
17
  /** Refuses a `messages` option of the wrong type where the app is declared, rather than at the first message. */ function assertMessageOptions(appName, messages, i18n) {
16
18
  if (!messages) return;
17
19
  const { prefix, mention, caseSensitive } = messages;
@@ -39,6 +41,9 @@ import { META } from '../util/metadata-keys.js';
39
41
  if (deleteUsageRepliesAfter !== undefined && !(typeof deleteUsageRepliesAfter === 'number' && deleteUsageRepliesAfter >= 0 && Number.isFinite(deleteUsageRepliesAfter))) {
40
42
  throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) takes a number of seconds, or 0 to keep usage replies.`));
41
43
  }
44
+ if (deleteUsageRepliesAfter !== undefined && !timerKeepsSeconds(deleteUsageRepliesAfter)) {
45
+ logger.warn(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) is ${deleteUsageRepliesAfter} seconds, longer than a timer ` + `can wait (${MAX_TIMER_SECONDS}), so usage replies are kept. The next major version (5.0) refuses it.`);
46
+ }
42
47
  const { help } = messages;
43
48
  // One word each, since help is asked for by its first word
44
49
  const isWord = (value)=>typeof value === 'string' && /^\S+$/.test(value);
@@ -2,7 +2,15 @@ import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
4
  import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
5
+ import { Logger } from '../common/logger.js';
5
6
 
7
+ const logger = new Logger('CommandBuilder');
8
+ /** The nearest base class of `target` that gives `guilds`, which `target` inherits when it gives none. */ function guildsBase(target) {
9
+ for(let base = Object.getPrototypeOf(target); base && base !== Function.prototype; base = Object.getPrototypeOf(base)){
10
+ if (Reflect.hasOwnMetadata(META.builderGuilds, base)) return base;
11
+ }
12
+ return undefined;
13
+ }
6
14
  /**
7
15
  * Marks a class as a command's builder, which describes the command MeoCord registers with Discord.
8
16
  *
@@ -40,6 +48,12 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
40
48
  if (options.guilds) Reflect.defineMetadata(META.builderGuilds, [
41
49
  ...options.guilds
42
50
  ], target);
51
+ else {
52
+ const base = guildsBase(target);
53
+ if (base) {
54
+ logger.warn(`${target.name} gives no guilds and inherits ${base.name}'s; declare guilds on ${target.name} to keep them. ` + 'The next major version (5.0) registers it in the configured scope.');
55
+ }
56
+ }
43
57
  });
44
58
  }
45
59