meocord 4.2.0 → 4.2.2
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.
- package/CHANGELOG.md +322 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +188 -169
- package/dist/cjs/decorator/index.cjs +52 -7
- package/dist/cjs/testing/index.cjs +1854 -311
- package/dist/cli.json +1 -1
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +13 -3
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/errors.js +25 -0
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +89 -15
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +52 -11
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +59 -34
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +19 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/shard-context.js +5 -6
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +7 -2
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +20 -13
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +225 -30
- package/dist/esm/testing/meocord-testing-module.js +158 -44
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1250 -182
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +40 -17
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +2 -2
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +3 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +30 -2
- package/dist/types/common/index.d.cts +10 -7
- package/dist/types/common/index.d.ts +10 -7
- package/dist/types/core/index.d.cts +3 -2
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/decorator/index.d.cts +1 -0
- package/dist/types/decorator/index.d.ts +1 -0
- package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
- package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
- package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
- package/dist/types/testing/index.d.cts +198 -62
- package/dist/types/testing/index.d.ts +198 -62
- package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +2 -2
|
@@ -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/
|
|
8
|
-
import '../common/
|
|
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,
|
|
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 (!
|
|
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));
|
|
@@ -46,7 +46,8 @@ import { META } from '../util/metadata-keys.js';
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
/** The class of a stage entry. */ function stageClass(entry) {
|
|
49
|
-
|
|
49
|
+
// Entries were checked as they were declared, so a function is the class itself
|
|
50
|
+
return typeof entry === 'function' ? entry : entry.provide;
|
|
50
51
|
}
|
|
51
52
|
/** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
|
|
52
53
|
return Reflect.getMetadata(META.stageTypes, stageClass(entry));
|
|
@@ -3,6 +3,8 @@ 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
9
|
import { startupError } from '../util/refusal.util.js';
|
|
8
10
|
|
|
@@ -57,6 +59,24 @@ import { startupError } 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([
|
|
@@ -78,4 +98,4 @@ import { startupError } from '../util/refusal.util.js';
|
|
|
78
98
|
for (const needsOne of classes.filter((cls)=>injectedTokens(cls).includes(Translator)))startupError(missingTranslatorError(needsOne));
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
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
|
-
|
|
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,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
|
-
|
|
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);
|
|
@@ -170,6 +172,9 @@ let themeOutsideCalls;
|
|
|
170
172
|
* Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
|
|
171
173
|
* Timers and promises the handler starts keep its theme without it.
|
|
172
174
|
*
|
|
175
|
+
* The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
|
|
176
|
+
* it is for, is answered in the starting member's theme.
|
|
177
|
+
*
|
|
173
178
|
* @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
|
|
174
179
|
* @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
|
|
175
180
|
*
|
|
@@ -179,7 +184,7 @@ let themeOutsideCalls;
|
|
|
179
184
|
* async poll(message: Message) {
|
|
180
185
|
* const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
|
|
181
186
|
* collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
|
|
182
|
-
* const { emojis } = useTheme() // the
|
|
187
|
+
* const { emojis } = useTheme() // the starting member's theme, @UseTheme included
|
|
183
188
|
* await click.reply(`${emojis.success} Picked`)
|
|
184
189
|
* }))
|
|
185
190
|
* }
|
|
@@ -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
|
|
29
|
-
* selector or skin tone, joined to others by
|
|
30
|
-
|
|
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
|
|
46
|
-
const
|
|
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
|
-
|
|
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;
|
|
@@ -9,6 +9,7 @@ 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
11
|
import { timeoutProblem } from '../util/timer-limit.util.js';
|
|
12
|
+
import { isConstructor, describeValue } from '../util/value.util.js';
|
|
12
13
|
import { META } from '../util/metadata-keys.js';
|
|
13
14
|
|
|
14
15
|
/** Refuses a `messages` option of the wrong type where the app is declared, rather than at the first message. */ function assertMessageOptions(appName, messages, i18n) {
|
|
@@ -52,6 +53,35 @@ import { META } from '../util/metadata-keys.js';
|
|
|
52
53
|
if (type.labelKey !== undefined) assertLabelKey(appName, name, type.labelKey, i18n);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Refuses an option that takes a class given anything else, such as an object or an arrow function, where the app is
|
|
58
|
+
* declared: the startup checks read each class's prototype, and would otherwise fail on it without naming the option.
|
|
59
|
+
*/ function assertClassOptions(appName, { presenter, cooldownStore, controllers, services }) {
|
|
60
|
+
for (const [name, value, implementing] of [
|
|
61
|
+
[
|
|
62
|
+
'presenter',
|
|
63
|
+
presenter,
|
|
64
|
+
'ResponsePresenter'
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
'cooldownStore',
|
|
68
|
+
cooldownStore,
|
|
69
|
+
'CooldownStore'
|
|
70
|
+
]
|
|
71
|
+
]){
|
|
72
|
+
if (value !== undefined && !isConstructor(value)) {
|
|
73
|
+
throw refuse(new TypeError(`${appName}: @MeoCord({ ${name} }) takes a class implementing ${implementing}, not ${describeValue(value)}.`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const controller of controllers ?? []){
|
|
77
|
+
if (!isConstructor(controller)) throw refuse(new TypeError(`${appName}: @MeoCord({ controllers }) takes classes, not ${describeValue(controller)}.`));
|
|
78
|
+
}
|
|
79
|
+
// A string or symbol is a token, which a provider has to provide; that is checked as the app is created
|
|
80
|
+
for (const service of services ?? []){
|
|
81
|
+
if (isConstructor(service) || typeof service === 'string' || typeof service === 'symbol') continue;
|
|
82
|
+
throw refuse(new TypeError(`${appName}: @MeoCord({ services }) takes classes, or tokens that providers provide, not ${describeValue(service)}.`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
55
85
|
/** Refuses a type's `labelKey` that no message of the app's default catalog has, as it would be shown as the key. */ function assertLabelKey(appName, name, labelKey, i18n) {
|
|
56
86
|
const where = `${appName}: @MeoCord({ messages: { types } }): "${name}" has labelKey`;
|
|
57
87
|
if (typeof labelKey !== 'string') throw refuse(new TypeError(`${where} ${String(labelKey)}; it takes a message key, such as 'types.${name}'.`));
|
|
@@ -96,6 +126,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
96
126
|
*/ function MeoCord(options) {
|
|
97
127
|
return declaring((target, propertyKey)=>{
|
|
98
128
|
if (deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
|
|
129
|
+
assertClassOptions(target.name, options);
|
|
99
130
|
assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
|
|
100
131
|
assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
|
|
101
132
|
assertStageEntries('@MeoCord({ filters })', 'filter', target.name, options.filters ?? []);
|
|
@@ -2,7 +2,8 @@ import 'reflect-metadata';
|
|
|
2
2
|
import { UserContextMenuCommandInteraction, ApplicationCommandType, MessageContextMenuCommandInteraction } from 'discord.js';
|
|
3
3
|
import { CommandType } from '../enum/controller.enum.js';
|
|
4
4
|
import { MetadataKey } from '../enum/metadata-key.enum.js';
|
|
5
|
-
import { matchesCommandType, interactionClassName
|
|
5
|
+
import { isCustomIdRouted, matchesCommandType, interactionClassName } from '../util/interaction.util.js';
|
|
6
|
+
import { wrapsMethod } from '../util/method-wrapper.util.js';
|
|
6
7
|
import { warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
7
8
|
import { Logger } from '../common/logger.js';
|
|
8
9
|
import { routeSpecificity } from '../core/route-specificity.js';
|
|
@@ -79,9 +80,9 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
79
80
|
// An entry the class takes from its base, for a method it re-declares
|
|
80
81
|
const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
|
|
81
82
|
const commands = ownCommandMap(prototype);
|
|
82
|
-
const baseCommands = Reflect.getMetadata(META.commands, base) ??
|
|
83
|
+
const baseCommands = Reflect.getMetadata(META.commands, base) ?? Object.create(null);
|
|
83
84
|
for (const [name, metas] of Object.entries(commands)){
|
|
84
|
-
const kept = metas.filter((meta)=>!inheritedFor(baseCommands
|
|
85
|
+
const kept = metas.filter((meta)=>!inheritedFor(commandsNamed(baseCommands, name), (entry)=>entry.methodName)(meta));
|
|
85
86
|
if (kept.length > 0) commands[name] = kept;
|
|
86
87
|
else delete commands[name];
|
|
87
88
|
}
|
|
@@ -105,16 +106,21 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
105
106
|
Reflect.defineMetadata(key, ownHandlerList(key, prototype).filter((entry)=>!inheritedFor(inherited, method)(entry)), prototype);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* The class's own command map, started from a copy of the inherited one, for the same reason. Keyed by command name
|
|
111
|
+
* without a prototype, so a command named like an inherited key, such as `constructor`, is a key like any other.
|
|
112
|
+
*/ function ownCommandMap(target) {
|
|
109
113
|
const own = Reflect.getOwnMetadata(META.commands, target);
|
|
110
114
|
if (own) return own;
|
|
111
115
|
const inherited = Reflect.getMetadata(META.commands, target) ?? {};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const copy = Object.create(null);
|
|
117
|
+
for (const [name, metas] of Object.entries(inherited))copy[name] = [
|
|
118
|
+
...metas
|
|
119
|
+
];
|
|
120
|
+
return copy;
|
|
121
|
+
}
|
|
122
|
+
/** The handlers a command map holds for `name`: its own entry only, never one `Object.prototype` holds. */ function commandsNamed(map, name) {
|
|
123
|
+
return map && Object.hasOwn(map, name) ? map[name] : [];
|
|
118
124
|
}
|
|
119
125
|
function MessageHandler(pattern, options = {}) {
|
|
120
126
|
return declaring(function(target, propertyKey) {
|
|
@@ -179,12 +185,24 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
|
|
|
179
185
|
}
|
|
180
186
|
// `{name}`, or `{name:type}` with the type read as far as the brace, so a type no segment can hold is named
|
|
181
187
|
const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
188
|
+
// The whole of a brace pair that is a param, as PLACEHOLDER_PATTERN reads one
|
|
189
|
+
const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
190
|
+
/**
|
|
191
|
+
* Why a pattern's brace pairs that aren't params are matched as literal text, or undefined when it has none: a
|
|
192
|
+
* param's name is ASCII letters, digits and _, so `{café}` is text.
|
|
193
|
+
*/ function literalBraces(pattern) {
|
|
194
|
+
const braces = (pattern.match(/\{[^{}]*}/g) ?? []).filter((brace)=>!WHOLE_PLACEHOLDER.test(brace));
|
|
195
|
+
if (braces.length === 0) return undefined;
|
|
196
|
+
const one = braces.length === 1;
|
|
197
|
+
return `${braces.join(', ')} ${one ? "isn't a param" : "aren't params"}, since a param's name is ASCII letters, digits and _; ` + `${one ? 'it is' : 'they are'} matched as literal text.`;
|
|
198
|
+
}
|
|
182
199
|
/** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
|
|
200
|
+
/** The longest customId Discord accepts; the shortest is one character. */ const MAX_CUSTOM_ID_LENGTH = 100;
|
|
183
201
|
/** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
184
202
|
/**
|
|
185
|
-
* Compiles a pattern into a regex, its parameter names, their types
|
|
186
|
-
* whole `/`-separated segment, so a uuid is captured whole and
|
|
187
|
-
* `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
|
|
203
|
+
* Compiles a pattern into a regex, its parameter names, their types, its 4.1 specificity and the length of the
|
|
204
|
+
* shortest customId it matches. A `{name}` takes a whole `/`-separated segment, so a uuid is captured whole and
|
|
205
|
+
* `profile/{uuid}` never overlaps `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
|
|
188
206
|
*/ function createRegexFromPattern(pattern) {
|
|
189
207
|
const params = [];
|
|
190
208
|
// By the param's own name, so one named like an inherited key, such as `__proto__`, keeps its type
|
|
@@ -205,6 +223,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
205
223
|
if (literal !== '' && !literal.endsWith(PARAM_SEPARATOR) || after !== undefined && after !== PARAM_SEPARATOR) {
|
|
206
224
|
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} must occupy a whole segment, so it has to be ` + `preceded and followed by "${PARAM_SEPARATOR}" or by the ends of the pattern. ` + `Write "a${PARAM_SEPARATOR}{${param}}" rather than "a-{${param}}".`));
|
|
207
225
|
}
|
|
226
|
+
// A regex group's name, which the engine would refuse with the compiled regex in place of the pattern
|
|
227
|
+
if (params.includes(param)) {
|
|
228
|
+
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} appears twice; give each param its own name.`));
|
|
229
|
+
}
|
|
230
|
+
if (/^\d/.test(param)) {
|
|
231
|
+
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} starts with a digit; start a param's name with a letter or _.`));
|
|
232
|
+
}
|
|
208
233
|
if (type !== undefined) {
|
|
209
234
|
if (!isSegmentType(type)) throw refuse(new Error(`Invalid pattern "${pattern}": ${segmentTypeProblem(param, type)}`));
|
|
210
235
|
if (choicesOf(type)?.includes('')) {
|
|
@@ -228,11 +253,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
228
253
|
params: params.length,
|
|
229
254
|
typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
|
|
230
255
|
});
|
|
256
|
+
// Each param takes at least one character
|
|
231
257
|
return {
|
|
232
258
|
regex,
|
|
233
259
|
params,
|
|
234
260
|
types: lookupTable(types),
|
|
235
|
-
specificity
|
|
261
|
+
specificity,
|
|
262
|
+
shortest: literalLength + params.length
|
|
236
263
|
};
|
|
237
264
|
}
|
|
238
265
|
/** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
|
|
@@ -302,7 +329,6 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
302
329
|
*/ function Command(name, builderOrType) {
|
|
303
330
|
const commandName = typeof name === 'string' ? name : name.pattern;
|
|
304
331
|
return declaring(function(target, propertyKey, _descriptor) {
|
|
305
|
-
var _commands, _commandName;
|
|
306
332
|
refuseOnClass('@Command', target, propertyKey);
|
|
307
333
|
const originalMethod = _descriptor.value;
|
|
308
334
|
if (!originalMethod) {
|
|
@@ -310,7 +336,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
310
336
|
}
|
|
311
337
|
// Wrap original method for interaction type validation
|
|
312
338
|
const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
|
|
313
|
-
|
|
339
|
+
const checked = function(interaction, params) {
|
|
314
340
|
if (!matchesCommandType(commandType, interaction)) {
|
|
315
341
|
throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
|
|
316
342
|
}
|
|
@@ -319,6 +345,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
319
345
|
params
|
|
320
346
|
]);
|
|
321
347
|
};
|
|
348
|
+
_descriptor.value = wrapsMethod(checked, originalMethod);
|
|
322
349
|
// This class's own map, inherited routes included
|
|
323
350
|
const commands = ownCommandMap(target);
|
|
324
351
|
let builderInstance;
|
|
@@ -354,7 +381,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
354
381
|
}));
|
|
355
382
|
}
|
|
356
383
|
// discord.js builders validate as they are set, and their errors name neither the command nor the field.
|
|
357
|
-
throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` +
|
|
384
|
+
throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` + 'localizations, which Discord limits to 32 characters for a name and 100 for a description. A name is letters, ' + 'numbers, `-` and `_`, lowercase where the script has case, and without spaces, in every locale.', {
|
|
358
385
|
cause: error
|
|
359
386
|
}));
|
|
360
387
|
}
|
|
@@ -374,6 +401,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
374
401
|
commandType = builderOrType;
|
|
375
402
|
}
|
|
376
403
|
if (isCustomIdRouted(commandType)) {
|
|
404
|
+
// A route warned as it was made
|
|
405
|
+
const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
|
|
406
|
+
if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
|
|
377
407
|
let pattern;
|
|
378
408
|
try {
|
|
379
409
|
pattern = createRegexFromPattern(commandName);
|
|
@@ -385,6 +415,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
385
415
|
regex = pattern.regex;
|
|
386
416
|
dynamicParams = pattern.params;
|
|
387
417
|
specificity = pattern.specificity;
|
|
418
|
+
// Warned rather than refused, so a bot that carries such a handler still starts
|
|
419
|
+
if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
|
|
420
|
+
const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
|
|
421
|
+
logger.warn(`${target.constructor.name}.${propertyKey}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
|
|
422
|
+
}
|
|
388
423
|
}
|
|
389
424
|
const declared = {
|
|
390
425
|
methodName: propertyKey,
|
|
@@ -401,8 +436,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
401
436
|
}
|
|
402
437
|
};
|
|
403
438
|
// One the class inherits for this method and route is replaced where it stands, so this one's options apply
|
|
404
|
-
|
|
405
|
-
const
|
|
439
|
+
if (!Object.hasOwn(commands, commandName)) commands[commandName] = [];
|
|
440
|
+
const metas = commands[commandName];
|
|
441
|
+
const inherited = commandsNamed(getCommandMap(Object.getPrototypeOf(target)), commandName);
|
|
406
442
|
const replaced = metas.findIndex((meta)=>inherited.includes(meta) && meta.methodName === propertyKey && meta.type === commandType);
|
|
407
443
|
if (replaced === -1) metas.push(declared);
|
|
408
444
|
else metas[replaced] = declared;
|
|
@@ -572,4 +608,4 @@ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
|
|
|
572
608
|
return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
|
|
573
609
|
}
|
|
574
610
|
|
|
575
|
-
export { Autocomplete, Command, MessageHandler, PARAM_SEPARATOR, ReactionHandler, createRegexFromPattern, dropInheritedRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, matchesEmoji, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
|
|
611
|
+
export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
+
import { Logger } from '../common/logger.js';
|
|
2
3
|
import { declaring, refuse, decoratedName } from '../util/refusal.util.js';
|
|
4
|
+
import { describeValue } from '../util/value.util.js';
|
|
3
5
|
import { META } from '../util/metadata-keys.js';
|
|
4
6
|
|
|
5
7
|
function Cooldown(options) {
|
|
@@ -14,6 +16,7 @@ function Cooldown(options) {
|
|
|
14
16
|
// Checked where it applies, so the refusal names the handler or controller
|
|
15
17
|
const problem = cooldownProblem(seconds, uses, per, by);
|
|
16
18
|
if (problem) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${problem}`));
|
|
19
|
+
for (const warning of cooldownWarnings(options))logger.warn(`${decoratedName(target, propertyKey)}: ${warning}`);
|
|
17
20
|
// Decorators apply bottom-up; prepending keeps them in the order they read.
|
|
18
21
|
if (propertyKey === undefined) {
|
|
19
22
|
const existing = Reflect.getOwnMetadata(META.classCooldowns, target) ?? [];
|
|
@@ -34,6 +37,20 @@ function Cooldown(options) {
|
|
|
34
37
|
* The longest window, in seconds: half the furthest time a `Date` holds, 8.64e15 ms, so the moment a wait ends, now
|
|
35
38
|
* plus the window, is a real date for a refusal's `retryAt` and the time it shows.
|
|
36
39
|
*/ const MAX_SECONDS = 4320000000000;
|
|
40
|
+
const logger = new Logger('Cooldown');
|
|
41
|
+
/**
|
|
42
|
+
* What a cooldown's options get wrong that the types refuse, yet still counts: a `bypass` that is not a function, which
|
|
43
|
+
* fails every call, and a `seconds` that is not a number, read as one. A falsy `bypass` means none.
|
|
44
|
+
*/ function cooldownWarnings({ seconds, bypass }) {
|
|
45
|
+
const warnings = [];
|
|
46
|
+
if (bypass && typeof bypass !== 'function') {
|
|
47
|
+
warnings.push(`@Cooldown takes bypass as a function of the call, returning whether the call skips the cooldown, not ${describeValue(bypass)}; ` + 'every call it counts fails with it. In the next major version (5.0) it is refused.');
|
|
48
|
+
}
|
|
49
|
+
if (typeof seconds !== 'number') {
|
|
50
|
+
warnings.push(`@Cooldown takes seconds as a number, not ${describeValue(seconds)}, which it counts as a window of ` + `${Math.round(seconds * 1000)} ms. In the next major version (5.0) it is refused.`);
|
|
51
|
+
}
|
|
52
|
+
return warnings;
|
|
53
|
+
}
|
|
37
54
|
/** Why a cooldown's options cannot be counted, or undefined when they can. */ function cooldownProblem(seconds, uses, per, by) {
|
|
38
55
|
if (!(seconds >= 0.001 && seconds <= MAX_SECONDS)) return `@Cooldown needs a number of seconds from 0.001 to ${MAX_SECONDS}, not ${seconds}.`;
|
|
39
56
|
if (!Number.isInteger(uses) || uses < 1) return `@Cooldown needs a whole number of uses of at least 1, not ${uses}.`;
|
|
@@ -3,7 +3,7 @@ import { makeInjectable } from '../util/injectable.util.js';
|
|
|
3
3
|
import { assertStageEntries } from '../core/stage-scope.js';
|
|
4
4
|
import { Logger } from '../common/logger.js';
|
|
5
5
|
import { warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isConstructible, describeValue } from '../util/value.util.js';
|
|
7
7
|
import { META } from '../util/metadata-keys.js';
|
|
8
8
|
import { declaring, refuseOnMethod } from '../util/refusal.util.js';
|
|
9
9
|
|
|
@@ -40,7 +40,7 @@ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
|
|
|
40
40
|
refuseOnMethod('@Catch', target, propertyKey);
|
|
41
41
|
makeInjectable(target);
|
|
42
42
|
errorTypes.forEach((type, index)=>{
|
|
43
|
-
if (
|
|
43
|
+
if (isConstructible(type)) return;
|
|
44
44
|
const entry = ORDINALS[index] ? `${ORDINALS[index]} entry` : `entry ${index + 1}`;
|
|
45
45
|
warnDeprecatedBehaviour(logger, `${target.name}: @Catch's ${entry}, ${describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
|
|
46
46
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { handlerMethods, classLevelGuards, consumeDispatchMark, runDirectCall, declaringPrototype, classChain } from '../core/guard-runner.js';
|
|
3
3
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
4
|
+
import { wrapsMethod } from '../util/method-wrapper.util.js';
|
|
4
5
|
import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
|
|
5
6
|
import { META } from '../util/metadata-keys.js';
|
|
6
7
|
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
@@ -34,10 +35,10 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
34
35
|
* which has already run the guards, passes through every wrapper.
|
|
35
36
|
*/ function applyGuards(descriptor, prototype, propertyKey) {
|
|
36
37
|
const originalMethod = descriptor.value;
|
|
37
|
-
descriptor.value = async function(...args) {
|
|
38
|
+
descriptor.value = wrapsMethod(async function(...args) {
|
|
38
39
|
if (consumeDispatchMark(args[0], this, propertyKey)) return originalMethod.apply(this, args);
|
|
39
40
|
return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args));
|
|
40
|
-
};
|
|
41
|
+
}, originalMethod);
|
|
41
42
|
const wrappers = Reflect.getOwnMetadata(META.guardWrappers, prototype, propertyKey) ?? 0;
|
|
42
43
|
Reflect.defineMetadata(META.guardWrappers, wrappers + 1, prototype, propertyKey);
|
|
43
44
|
}
|
package/dist/esm/package.json.js
CHANGED