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.
- package/CHANGELOG.md +362 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +365 -310
- package/dist/cjs/decorator/index.cjs +85 -61
- package/dist/cjs/testing/index.cjs +2236 -496
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- 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 +17 -4
- 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/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +27 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/metadata.js +1 -1
- 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 +84 -10
- package/dist/esm/common/route.js +21 -5
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +51 -7
- 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 +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +241 -179
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +45 -23
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +6 -7
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/startup-roots.js +23 -4
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +162 -83
- package/dist/esm/decorator/cooldown.decorator.js +20 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +11 -10
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +27 -11
- package/dist/esm/testing/cooldown-store-suite.js +85 -15
- package/dist/esm/testing/discord-defaults.js +249 -21
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +352 -205
- package/dist/esm/testing/mock-fn.js +173 -22
- package/dist/esm/testing/mock-interaction.js +1214 -209
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +65 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- 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 +3 -3
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +4 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/refusal.util.js +148 -8
- 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 +12 -1
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +10 -7
- package/dist/types/core/index.d.ts +10 -7
- package/dist/types/decorator/index.d.cts +31 -29
- package/dist/types/decorator/index.d.ts +31 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
- package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
- package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
- package/dist/types/testing/index.d.cts +406 -70
- package/dist/types/testing/index.d.ts +406 -70
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
- 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
|
-
/**
|
|
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/
|
|
9
|
-
import '../common/
|
|
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.
|
|
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/
|
|
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));
|
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
-
import { releaseAmbientTheme, claimAmbientTheme,
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 };
|