meocord 4.1.2 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -0,0 +1,43 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ /**
4
+ * The calls an app is running, which shutdown waits for before the `onShutdown` hooks. A call that asks the app to
5
+ * stop is excused, so a handler awaiting `app.stop()` never waits for itself.
6
+ */ class RunningCalls {
7
+ /** Runs `run` as a call named `label`, tracked until it settles. */ run(label, run) {
8
+ const token = {};
9
+ this.calls.set(token, label);
10
+ const done = this.current.run(token, run);
11
+ const settle = ()=>{
12
+ this.calls.delete(token);
13
+ this.notify();
14
+ };
15
+ done.then(settle, settle);
16
+ return done;
17
+ }
18
+ /** Excuses the call this runs in, and any it runs within, from the wait; outside a call, nothing. */ excuseCurrent() {
19
+ const token = this.current.getStore();
20
+ if (!token || !this.calls.has(token)) return;
21
+ this.excused.add(token);
22
+ this.notify();
23
+ }
24
+ /** The names of the calls a wait would still wait for. */ running() {
25
+ return [
26
+ ...this.calls
27
+ ].filter(([token])=>!this.excused.has(token)).map(([, label])=>label);
28
+ }
29
+ /** Settles once every call not excused has, those that start or are excused meanwhile included. */ async settled() {
30
+ while(this.running().length > 0)await (this.changed ?? (this.changed = Promise.withResolvers())).promise;
31
+ }
32
+ notify() {
33
+ this.changed?.resolve();
34
+ this.changed = undefined;
35
+ }
36
+ constructor(){
37
+ this.current = new AsyncLocalStorage();
38
+ /** Each call under way, by its token, with the name a log gives it. */ this.calls = new Map();
39
+ this.excused = new WeakSet();
40
+ }
41
+ }
42
+
43
+ export { RunningCalls };
@@ -34,18 +34,30 @@ const UNIT_MS = lookupTable({
34
34
  return total + Number(amount) * UNIT_MS[unit];
35
35
  }, 0);
36
36
  }
37
+ /** The largest snowflake: Discord's IDs are unsigned 64-bit integers. */ const MAX_SNOWFLAKE = 2n ** 64n - 1n;
38
+ /**
39
+ * 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
+ * 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
+ * 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;
37
44
  /** The words a type such as `on|off` chooses from, or `undefined` for any other type. */ const choicesOf = (type)=>type.includes('|') ? type.split('|') : undefined;
38
45
  /** The types a component's customId segment can take: each parsed from the segment alone, with no request. */ const SEGMENT_TYPES = lookupTable({
39
46
  string: (segment)=>segment,
40
47
  int: (segment)=>number(segment, true),
41
48
  number: (segment)=>number(segment, false),
42
- bool
49
+ bool,
50
+ snowflake,
51
+ uuid
43
52
  });
44
- /** Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, or words to choose from. */ const isSegmentType = (type)=>type in SEGMENT_TYPES || choicesOf(type) !== undefined;
53
+ /**
54
+ * Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
55
+ * choose from.
56
+ */ const isSegmentType = (type)=>type in SEGMENT_TYPES || choicesOf(type) !== undefined;
45
57
  /** 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) {
46
58
  const choices = choicesOf(type);
47
59
  if (choices) return choices.includes(segment) ? segment : undefined;
48
60
  return SEGMENT_TYPES[type](segment);
49
61
  }
50
62
 
51
- export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment };
63
+ export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment, snowflake, uuid };
@@ -5,16 +5,15 @@ import './theme-validation.js';
5
5
  import '../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../common/translator.js';
9
- import '../common/response/send-failure.js';
10
- import '../util/interaction.util.js';
11
- import '../common/response/flags.js';
12
- import 'path';
13
- import '../common/response/components.js';
8
+ import '../common/response/response-state.js';
9
+ import '../common/errors.js';
14
10
  import '../decorator/controller.decorator.js';
15
11
  import './scalar-types.js';
12
+ import 'path';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../common/translator.js';
16
+ import '../common/meocord-text.js';
18
17
 
19
18
  /** Where each shard's client keeps the function that runs a `ShardContext.call` in that shard. */ const SHARD_CALL_KEY = Symbol.for('meocord.shardCall');
20
19
  const packArgs = (args)=>Object.assign({
@@ -95,7 +94,7 @@ async function withTimeout(promise, ms, what) {
95
94
  *
96
95
  * @group Controllers
97
96
  * @see {@link ShardCallResult}
98
- * @see {@link https://meocord.dev/docs/4.1/sharding | Sharding}
97
+ * @see {@link https://meocord.dev/docs/4.2/sharding | Sharding}
99
98
  */ class ShardContext {
100
99
  /** The ids of the shards this process runs. With process sharding, one id; otherwise every shard, once ready. */ get ids() {
101
100
  if (this.client?.shard) return [
@@ -4,24 +4,22 @@ import './theme-scope.js';
4
4
  import './theme-validation.js';
5
5
  import '../common/decorator.js';
6
6
  import 'reflect-metadata';
7
- import '../common/translator.js';
8
- import '../common/response/send-failure.js';
9
- import '../util/interaction.util.js';
10
- import '../common/response/flags.js';
11
- import 'path';
12
- import '../common/response/components.js';
7
+ import '../common/response/response-state.js';
8
+ import '../common/errors.js';
13
9
  import { MemoryCooldownStore } from '../common/cooldown-store.js';
14
10
  import 'node:crypto';
15
11
  import '../decorator/controller.decorator.js';
16
12
  import './scalar-types.js';
17
13
  import { answerCooldown } from '../common/sharded-cooldown-store.js';
14
+ import '../common/translator.js';
15
+ import '../common/meocord-text.js';
18
16
  import { registerCommands } from './command-registration.js';
19
17
  import { SHUTDOWN_MARGIN_MS, shutdownTimeoutOf } from '../util/shutdown-timeout.util.js';
20
18
  import { bundleEntry } from '../util/bundle-entry.util.js';
21
19
  import { FORCE_REGISTER_ENV } from '../util/registration-mode.util.js';
22
20
  import { isShardMessage, REFUSED_CODE } from './shard-messages.js';
23
21
  import { tokenMessage, isRefusedToken } from './login-failure.js';
24
- import { onDevRunnerStop, underDevRunner, tellDevRunner } from '../util/dev-runner.util.js';
22
+ import { onDevRunnerStop, tellDevRunner, underDevRunner } from '../util/dev-runner.util.js';
25
23
  import { stopRequests } from '../util/stop-request.util.js';
26
24
 
27
25
  /** How long the manager waits for a shard to become ready before spawning the next. */ const SHARD_READY_TIMEOUT_MS = 30000;
@@ -60,7 +58,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
60
58
  process.on('SIGINT', ()=>void this.stopAndExit());
61
59
  process.on('SIGTERM', ()=>void this.stopAndExit());
62
60
  // How `meocord start --dev` stops the bot to restart it; no stop request of the user's, whose first Ctrl+C joins it
63
- onDevRunnerStop(()=>void this.exitAfterShutdown());
61
+ onDevRunnerStop(()=>void this.exitAfterShutdown(), false);
64
62
  let total;
65
63
  try {
66
64
  const shards = config.sharding?.shards ?? 'auto';
@@ -77,6 +75,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
77
75
  const manager = (this.options.createManager ?? ((path, opts)=>new ShardingManager(path, opts)))(file, {
78
76
  token,
79
77
  totalShards: total,
78
+ // discord.js refuses a call to every shard unless shardList counts the shards; createShard leaves it 'auto'
79
+ shardList: [
80
+ ...Array(total).keys()
81
+ ],
80
82
  mode: 'process',
81
83
  execArgv: process.execArgv,
82
84
  respawn: false
@@ -173,12 +175,12 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
173
175
  void this.exitForLogin();
174
176
  }
175
177
  /** Exits 1 for a bot that could not log in, telling `meocord start --dev` first, which then waits for a change. */ exitForLogin() {
176
- if (!underDevRunner()) return this.exit(1);
178
+ if (!this.devRunner) return this.exit(1);
177
179
  // Set first, so the process exits 1 even if it ends while the message is still on its way
178
180
  process.exitCode = 1;
179
181
  return tellDevRunner({
180
182
  meocord: 'login-failed'
181
- }).then(()=>this.exit(1));
183
+ }, false).then(()=>this.exit(1));
182
184
  }
183
185
  /**
184
186
  * Asks every shard to shut down through its own hooks, waits for them up to the shutdown timeout plus a margin, and
@@ -255,6 +257,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
255
257
  /** Every shard's cooldown calls, for an app whose store is ShardedCooldownStore. */ this.cooldowns = new MemoryCooldownStore();
256
258
  this.stopping = false;
257
259
  this.fatal = false;
260
+ /**
261
+ * Whether `meocord start --dev` runs the manager, decided before discord.js's ShardingManager, which writes the
262
+ * variable that marks a shard into this process too.
263
+ */ this.devRunner = underDevRunner(false);
258
264
  // The manager holds the credential for its shards and logs their failures, without an app of its own to register it
259
265
  hideInLogs(options.token);
260
266
  this.exit = options.exit ?? ((code)=>process.exit(code));
@@ -3,8 +3,10 @@ import { injectedTokens, isGuardWithParams } from './guard-runner.js';
3
3
  import { classDecorators, handlerStageClasses } from './handler-pipeline.js';
4
4
  import { reachableClasses, assertTypedParameters } from './providers.js';
5
5
  import { themeResolverClass } from './theme-resolvers.js';
6
+ import { isAppClassToken } from './lifecycle-order.js';
7
+ import { ExecutionContext } from '../common/execution-context.js';
6
8
  import { META } from '../util/metadata-keys.js';
7
- import { refuse } from '../util/refusal.util.js';
9
+ import { startupError } from '../util/refusal.util.js';
8
10
 
9
11
  /** The guards `@UseGuard` puts on a class's methods, handlers or not, which a direct call runs too. */ function methodGuards(cls) {
10
12
  const guards = [];
@@ -57,6 +59,24 @@ import { refuse } from '../util/refusal.util.js';
57
59
  for (const [stage, decorator] of found)decorators.set(stage, decorator);
58
60
  }
59
61
  }
62
+ /** Whether making `cls` reaches the per-call `ExecutionContext` through the classes it injects, which each call makes. */ function reachesContext(cls, providers, seen = new Set()) {
63
+ seen.add(cls);
64
+ return injectedTokens(cls).some((token)=>token === ExecutionContext || isAppClassToken(token) && !providers.has(token) && !seen.has(token) && reachesContext(token, providers, seen));
65
+ }
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) {
72
+ const stages = [
73
+ ...decorators
74
+ ].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
75
+ if (typeof presenter === 'function') stages.push(presenter);
76
+ return [
77
+ ...new Set(stages.flatMap((stage)=>injectedTokens(stage)))
78
+ ].filter(isAppClassToken).filter((cls)=>!reachesContext(cls, providers));
79
+ }
60
80
  /** 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) {
61
81
  return (guard)=>assertStartupClasses({
62
82
  classes: reachableClasses([
@@ -75,8 +95,7 @@ import { refuse } from '../util/refusal.util.js';
75
95
  /** Runs the startup checks over every class an app runs, so a class that cannot be made is refused by name now. */ function assertStartupClasses({ classes, decorators }, { translator }) {
76
96
  assertTypedParameters(classes, decorators);
77
97
  if (translator) return;
78
- const needsOne = classes.find((cls)=>injectedTokens(cls).includes(Translator));
79
- if (needsOne) throw refuse(missingTranslatorError(needsOne));
98
+ for (const needsOne of classes.filter((cls)=>injectedTokens(cls).includes(Translator)))startupError(missingTranslatorError(needsOne));
80
99
  }
81
100
 
82
- export { assertStartupClasses, lateGuardCheck, startupClasses };
101
+ export { assertStartupClasses, lateGuardCheck, stageDependencies, startupClasses };
@@ -118,7 +118,10 @@ const logger = new Logger('Theme');
118
118
  */ accept(id, result, report) {
119
119
  if (result === undefined || result === null) return undefined;
120
120
  const layer = copyLayer(result);
121
- const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`);
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
+ });
122
125
  if (problems.length === 0) {
123
126
  if (report) this.warned.delete(id);
124
127
  return layer;
@@ -1,8 +1,8 @@
1
1
  import 'reflect-metadata';
2
2
  import { stageClasses, sourcePrototype } from './guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
- import { resolverCaches, ThemeCache, attachThemeCache, lookupLayers } from './theme-resolvers.js';
5
- import { releaseAmbientTheme, claimAmbientTheme, defaultTheme, mergeTheme, themeLayersVersion, ambientThemeVersion, ownsAmbientTheme, hasAmbientTheme, setThemeOutsideCalls, useTheme } from './theme-scope.js';
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';
6
6
  import { META } from '../util/metadata-keys.js';
7
7
 
8
8
  /** Classes with a `@UseTheme`, on the class or one of its methods. */ const THEMED_CLASSES = new WeakSet();
@@ -1,7 +1,9 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { DEFAULT_THEME } from './theme-defaults.js';
3
+ import { shared } from '../util/shared-state.util.js';
3
4
 
4
- const scope = new AsyncLocalStorage();
5
+ // One per process for this meocord version, so a call's theme reads the same from its ES module and CommonJS builds
6
+ const scope = shared('themeScope', ()=>new AsyncLocalStorage());
5
7
  const isPlainObject = (value)=>{
6
8
  if (value === null || typeof value !== 'object') return false;
7
9
  const prototype = Object.getPrototypeOf(value);
@@ -1,5 +1,6 @@
1
1
  import { ButtonStyle, Colors } from 'discord.js';
2
2
  import { refuse } from '../util/refusal.util.js';
3
+ import { escapeForLog, quoteForLog } from '../util/user-text.util.js';
3
4
 
4
5
  /**
5
6
  * The role names MeoCord keeps for roles it may add to `colors`, `emojis` and `buttons`: the runtime copy of
@@ -25,9 +26,10 @@ const EMOJI = 'a unicode emoji, or a custom one written <:name:id> or <a:name:id
25
26
  const BUTTON = 'ButtonStyle.Primary, Secondary, Success or Danger';
26
27
  /** A custom emoji as Discord writes one in text: its name, 2 to 32 word characters, and its id. */ const CUSTOM_EMOJI = /^<a?:\w{2,32}:\d{17,20}>$/;
27
28
  /**
28
- * One unicode emoji: a flag of two regional indicators, a keycap, or a pictograph with an optional presentation
29
- * selector or skin tone, joined to others by zero-width joiners into one sequence.
30
- */ const UNICODE_EMOJI = /^(?:\p{Regional_Indicator}{2}|[#*0-9]️?⃣|\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?(?:‍\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?)*)$/u;
29
+ * One unicode emoji: a flag of two regional indicators, a subdivision flag (a black flag, tag characters and a cancel
30
+ * tag, as England's), a keycap, or a pictograph with an optional presentation selector or skin tone, joined to others by
31
+ * zero-width joiners into one sequence.
32
+ */ const UNICODE_EMOJI = /^(?:\p{Regional_Indicator}{2}|\u{1F3F4}[\u{E0020}-\u{E007E}]+\u{E007F}|[#*0-9]️?⃣|\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?(?:‍\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?)*)$/u;
31
33
  const byte = (value)=>Number.isInteger(value) && value >= 0 && value <= 255;
32
34
  /** Whether discord.js resolves the value to a colour Discord takes. Stricter on tuples, whose channels it only sums. */ function isColour(value) {
33
35
  if (typeof value === 'string') return /^#?[\da-f]{6}$/i.test(value) || value === 'Random' || value === 'Default' || Object.hasOwn(Colors, value);
@@ -42,8 +44,9 @@ const THEMED_BUTTON_STYLES = [
42
44
  ButtonStyle.Danger
43
45
  ];
44
46
  const isButtonStyle = (value)=>THEMED_BUTTON_STYLES.includes(value);
45
- const describe = (value)=>typeof value === 'string' ? `'${value}'` : Array.isArray(value) ? `[${value.map((item)=>typeof item === 'string' ? `'${item}'` : String(item)).join(', ')}]` : value === null ? 'null' : typeof value === 'object' ? 'an object' : String(value);
46
- const describeGroup = (value)=>Array.isArray(value) ? 'an array' : describe(value);
47
+ const asWritten = (text)=>`'${text}'`;
48
+ const describe = (value, quote = asWritten)=>typeof value === 'string' ? quote(value) : Array.isArray(value) ? `[${value.map((item)=>typeof item === 'string' ? quote(item) : String(item)).join(', ')}]` : value === null ? 'null' : typeof value === 'object' ? 'an object' : String(value);
49
+ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an array' : describe(value, quote);
47
50
  /** Whether a value is a plain object, as an object literal or `JSON.parse` makes: one a theme merges key by key. */ const isPlainObject = (value)=>{
48
51
  const prototype = Object.getPrototypeOf(value);
49
52
  return prototype === Object.prototype || prototype === null;
@@ -75,11 +78,15 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
75
78
  * roles an app added, a colour discord.js cannot resolve, an emoji Discord would refuse, a button style other than
76
79
  * Discord's four coloured ones, or a reserved role name. A group of the app's own is its to check.
77
80
  * @param where - Where it was set, such as `themeFor.guild for guild 123`, to begin each line with.
78
- */ function themeProblems(theme, where) {
81
+ * @param options.forLog - Shows values and role names as a log line shows text from outside the code, for a theme a
82
+ * `themeFor` resolver returned: escaped, and a value quoted and cut short.
83
+ */ function themeProblems(theme, where, { forLog = false } = {}) {
79
84
  const at = where ? `${where}: ` : '';
85
+ const quote = forLog ? quoteForLog : asWritten;
86
+ const name = forLog ? escapeForLog : (role)=>role;
80
87
  if (typeof theme !== 'object' || theme === null || Array.isArray(theme)) {
81
88
  return [
82
- `${at}theme must be an object of groups (got ${describeGroup(theme)})`
89
+ `${at}theme must be an object of groups (got ${describeGroup(theme, quote)})`
83
90
  ];
84
91
  }
85
92
  if (!isPlainObject(theme)) return [
@@ -91,7 +98,7 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
91
98
  const roles = theme[group];
92
99
  if (roles === undefined) continue;
93
100
  if (typeof roles !== 'object' || roles === null || Array.isArray(roles)) {
94
- problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles)})`);
101
+ problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles, quote)})`);
95
102
  continue;
96
103
  }
97
104
  if (!isPlainObject(roles)) {
@@ -99,8 +106,8 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
99
106
  continue;
100
107
  }
101
108
  for (const [role, value] of Object.entries(roles)){
102
- if (reserved.has(role)) problems.push(`${at}theme.${group}.${role}: MeoCord reserves the role name ${role} for a role it may add; rename yours`);
103
- else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${role}: ${describe(value)} is not ${what}: give ${instead}`);
109
+ if (reserved.has(role)) problems.push(`${at}theme.${group}.${name(role)}: MeoCord reserves the role name ${name(role)} for a role it may add; rename yours`);
110
+ else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${name(role)}: ${describe(value, quote)} is not ${what}: give ${instead}`);
104
111
  }
105
112
  }
106
113
  return problems;
@@ -7,7 +7,7 @@ import { BUILT_IN_TYPES } from '../core/message-params.js';
7
7
  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
- import { deprecatedOnMethod, refuse } from '../util/refusal.util.js';
10
+ import { declaring, deprecatedOnMethod, refuse } from '../util/refusal.util.js';
11
11
  import { timeoutProblem } from '../util/timer-limit.util.js';
12
12
  import { META } from '../util/metadata-keys.js';
13
13
 
@@ -18,14 +18,19 @@ import { META } from '../util/metadata-keys.js';
18
18
  if (prefix !== undefined && !isText(prefix) && typeof prefix !== 'function' && !(Array.isArray(prefix) && prefix.every(isText))) {
19
19
  throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { prefix } }) takes a string, a list of strings, or a function of the message returning them.`));
20
20
  }
21
- for (const [name, value] of Object.entries({
21
+ const flags = {
22
22
  caseSensitive,
23
23
  replyEmoji: messages.replyEmoji,
24
24
  dmOnError: messages.dmOnError,
25
- dmOnCooldown: messages.dmOnCooldown
26
- })){
25
+ dmOnCooldown: messages.dmOnCooldown,
26
+ slowHandlerWarning: messages.slowHandlerWarning
27
+ };
28
+ for (const [name, value] of Object.entries(flags)){
27
29
  if (value !== undefined && typeof value !== 'boolean') throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { ${name} } }) takes true or false.`));
28
30
  }
31
+ if (messages.handlers !== undefined && messages.handlers !== 'sequential' && messages.handlers !== 'concurrent') {
32
+ throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { handlers } }) takes 'sequential' or 'concurrent'.`));
33
+ }
29
34
  if (mention !== undefined && typeof mention !== 'boolean' && mention !== 'only') {
30
35
  throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { mention } }) takes true, false or 'only'.`));
31
36
  }
@@ -87,9 +92,9 @@ import { META } from '../util/metadata-keys.js';
87
92
  * @see {@link MeoCordOptions}
88
93
  * @see {@link MeoCordFactory}
89
94
  * @see {@link MeoCordConfig}
90
- * @see {@link https://meocord.dev/docs/4.1/configuration | Configuration}
95
+ * @see {@link https://meocord.dev/docs/4.2/configuration | Configuration}
91
96
  */ function MeoCord(options) {
92
- return (target, propertyKey)=>{
97
+ return declaring((target, propertyKey)=>{
93
98
  if (deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
94
99
  assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
95
100
  assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
@@ -111,7 +116,7 @@ import { META } from '../util/metadata-keys.js';
111
116
  ...options,
112
117
  theme
113
118
  }, target);
114
- };
119
+ });
115
120
  }
116
121
  /** Refuses theme resolvers and cache options the runtime cannot follow, where the app is declared. */ function assertThemeFor(appName, { themeFor, themeCache, themeForTimeoutMs }) {
117
122
  const problem = themeFor === undefined ? undefined : themeForProblem(themeFor, `${appName}: @MeoCord({ themeFor })`, 'or a class implementing ThemeResolver.');
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
- import { deprecatedOnMethod } from '../util/refusal.util.js';
4
+ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
5
5
 
6
6
  /**
7
7
  * Marks a class as a command's builder, which describes the command MeoCord registers with Discord.
@@ -30,9 +30,9 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
30
30
  * @group Decorators
31
31
  * @category Handlers
32
32
  * @see {@link Command}
33
- * @see {@link https://meocord.dev/docs/4.1/slash-commands | Slash commands}
33
+ * @see {@link https://meocord.dev/docs/4.2/slash-commands | Slash commands}
34
34
  */ function CommandBuilder(commandType, options = {}) {
35
- return function(target, propertyKey) {
35
+ return declaring(function(target, propertyKey) {
36
36
  if (deprecatedOnMethod('@CommandBuilder', target, propertyKey)) return;
37
37
  makeInjectable(target);
38
38
  // Define the command type metadata for the target class
@@ -40,7 +40,7 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
40
40
  if (options.guilds) Reflect.defineMetadata(META.builderGuilds, [
41
41
  ...options.guilds
42
42
  ], target);
43
- };
43
+ });
44
44
  }
45
45
 
46
46
  export { CommandBuilder };
@@ -1,7 +1,8 @@
1
+ import { dropInheritedRoutes } from './controller.decorator.js';
1
2
  import { guardOwnHandlersWithBaseGuards } from './guard.decorator.js';
2
3
  import { makeInjectable } from '../util/injectable.util.js';
3
4
  import { META } from '../util/metadata-keys.js';
4
- import { deprecatedOnMethod } from '../util/refusal.util.js';
5
+ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
5
6
 
6
7
  /**
7
8
  * Marks a class as a controller, whose methods handle commands, components, messages, reactions or events.
@@ -35,14 +36,18 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
35
36
  * @category Controllers
36
37
  * @see {@link Command}
37
38
  * @see {@link ControllerOptions}
38
- * @see {@link https://meocord.dev/docs/4.1/how-a-call-runs | How a call runs}
39
+ * @see {@link https://meocord.dev/docs/4.2/how-a-call-runs | How a call runs}
39
40
  */ function Controller(options = {}) {
40
- return function(target, propertyKey) {
41
+ return declaring(function(target, propertyKey) {
41
42
  if (deprecatedOnMethod('@Controller', target, propertyKey)) return;
42
43
  makeInjectable(target);
43
44
  if (options.inheritStages === false) Reflect.defineMetadata(META.inheritStages, false, target);
45
+ if (options.inheritedRoutes === 'replace') {
46
+ dropInheritedRoutes(target.prototype);
47
+ Reflect.defineMetadata(META.replacesInheritedRoutes, true, target.prototype);
48
+ }
44
49
  guardOwnHandlersWithBaseGuards(target);
45
- };
50
+ });
46
51
  }
47
52
 
48
53
  export { Controller };