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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('./controller.decorator-
|
|
3
|
+
var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
|
|
4
4
|
require('reflect-metadata');
|
|
5
5
|
var node_async_hooks = require('node:async_hooks');
|
|
6
6
|
var inversify = require('inversify');
|
|
@@ -101,7 +101,7 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
|
|
|
101
101
|
|
|
102
102
|
function eventDecorator(event, once) {
|
|
103
103
|
// Generic over the method, so it is checked against the event's arguments rather than required to match exactly
|
|
104
|
-
return function(target, propertyKey, _descriptor) {
|
|
104
|
+
return controller_decorator.declaring(function(target, propertyKey, _descriptor) {
|
|
105
105
|
controller_decorator.refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
|
|
106
106
|
const handlers = controller_decorator.ownHandlerList(controller_decorator.META.eventHandlers, target);
|
|
107
107
|
handlers.push({
|
|
@@ -110,7 +110,7 @@ function eventDecorator(event, once) {
|
|
|
110
110
|
once
|
|
111
111
|
});
|
|
112
112
|
Reflect.defineMetadata(controller_decorator.META.eventHandlers, handlers, target);
|
|
113
|
-
};
|
|
113
|
+
});
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* Handles a discord.js client event every time it is emitted, on a controller or a service.
|
|
@@ -138,7 +138,7 @@ function eventDecorator(event, once) {
|
|
|
138
138
|
* @group Decorators
|
|
139
139
|
* @category Handlers
|
|
140
140
|
* @see {@link Once}
|
|
141
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
141
|
+
* @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
|
|
142
142
|
*/ function On(event) {
|
|
143
143
|
return eventDecorator(event, false);
|
|
144
144
|
}
|
|
@@ -165,7 +165,7 @@ function eventDecorator(event, once) {
|
|
|
165
165
|
* @group Decorators
|
|
166
166
|
* @category Handlers
|
|
167
167
|
* @see {@link On}
|
|
168
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
168
|
+
* @see {@link https://meocord.dev/docs/4.2/gateway-events | Gateway events}
|
|
169
169
|
*/ function Once(event) {
|
|
170
170
|
return eventDecorator(event, true);
|
|
171
171
|
}
|
|
@@ -505,14 +505,27 @@ const wrapperCount = perHandler((prototype, methodName)=>{
|
|
|
505
505
|
else marks.delete(methodName);
|
|
506
506
|
return true;
|
|
507
507
|
}
|
|
508
|
+
/** The container that made each instance, so a direct call runs in its own app or testing module. */ const instanceContainers = new WeakMap();
|
|
509
|
+
/**
|
|
510
|
+
* Records each instance `container` makes of `classes` as its own. Two testing modules made from one class each run a
|
|
511
|
+
* direct call on their own instance in themselves, where the class's metadata names only the last.
|
|
512
|
+
*/ function recordInstanceContainers(container, classes) {
|
|
513
|
+
for (const cls of classes){
|
|
514
|
+
container.onActivation(cls, (_context, instance)=>{
|
|
515
|
+
if (typeof instance === 'object' && instance !== null) instanceContainers.set(instance, container);
|
|
516
|
+
return instance;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
508
520
|
/**
|
|
509
521
|
* Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves them, then the
|
|
510
522
|
* call with a pass for each wrapper inside the one entered, so no guard runs twice.
|
|
511
523
|
*/ async function runDirectCall(instance, methodName, args, call) {
|
|
512
524
|
const prototype = Object.getPrototypeOf(instance);
|
|
513
525
|
const controller = instance.constructor;
|
|
514
|
-
// The app that made the class
|
|
515
|
-
|
|
526
|
+
// The container that made the instance, or else the app that made the class or a class it extends: a subclass the
|
|
527
|
+
// app's code makes runs as its base does
|
|
528
|
+
const container = instanceContainers.get(instance) ?? Reflect.getMetadata(controller_decorator.META.container, controller);
|
|
516
529
|
if (!container) {
|
|
517
530
|
throw new Error(`${controller.name}.${methodName}: @UseGuard runs its guards through the app that made ${controller.name}, and this ` + `instance was made outside one. Inject ${controller.name}, or call it on an instance the app made.`);
|
|
518
531
|
}
|
|
@@ -767,19 +780,37 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
767
780
|
const value = by ? await by(context, params) : undefined;
|
|
768
781
|
// Encoded, so a value holding a colon cannot count under another value's key. Without a value, `:by` alone keeps
|
|
769
782
|
// the call apart from a cooldown with no `by` over the same window.
|
|
770
|
-
const suffix = !by ? '' : value === undefined ? ':by' : `:by:${
|
|
783
|
+
const suffix = !by ? '' : value === undefined ? ':by' : `:by:${encodeKeyPart(String(value))}`;
|
|
784
|
+
const scope = scopeId(per, first);
|
|
785
|
+
// The caller, for a notice of a refusal under a scope that counts more than one caller
|
|
786
|
+
const user = scope.startsWith('user:') ? undefined : caller(first).user;
|
|
771
787
|
counted.push({
|
|
772
|
-
key: `${controller.name}.${methodName}#${id}:${per}:${
|
|
788
|
+
key: `${controller.name}.${methodName}#${id}:${per}:${scope}${suffix}`,
|
|
773
789
|
windowMs,
|
|
774
790
|
uses,
|
|
775
|
-
per
|
|
791
|
+
per,
|
|
792
|
+
user
|
|
776
793
|
});
|
|
777
794
|
}
|
|
778
795
|
return counted;
|
|
779
796
|
}
|
|
797
|
+
// A surrogate with no partner beside it, which encodeURIComponent refuses
|
|
798
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
799
|
+
/**
|
|
800
|
+
* A value as part of a store key: each well-formed run as `encodeURIComponent` writes it, and each lone surrogate as `%u`
|
|
801
|
+
* and its four hex digits, which no other value encodes to, since `encodeURIComponent` writes `%` only before two.
|
|
802
|
+
*/ function encodeKeyPart(value) {
|
|
803
|
+
let encoded = '';
|
|
804
|
+
let from = 0;
|
|
805
|
+
for (const { 0: surrogate, index } of value.matchAll(LONE_SURROGATE)){
|
|
806
|
+
encoded += `${encodeURIComponent(value.slice(from, index))}%u${surrogate.charCodeAt(0).toString(16).toUpperCase()}`;
|
|
807
|
+
from = index + 1;
|
|
808
|
+
}
|
|
809
|
+
return encoded + encodeURIComponent(value.slice(from));
|
|
810
|
+
}
|
|
780
811
|
/**
|
|
781
|
-
* The store key and window of the cooldown each refusal came from,
|
|
782
|
-
*
|
|
812
|
+
* The store key and window of the cooldown each refusal came from, when its wait ends by the store's clock if the
|
|
813
|
+
* store said, and the refused caller when the key counts more than one, for {@link claimCooldownNotice}.
|
|
783
814
|
*/ const refusalKeys = new WeakMap();
|
|
784
815
|
/** Asks the store about the call under the app's policy, and throws what the answer means for it. */ async function ask(container, counted, peek, call) {
|
|
785
816
|
const store = cooldownStoreOf(container);
|
|
@@ -812,7 +843,8 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
812
843
|
refusalKeys.set(refusal, {
|
|
813
844
|
key: blocking.key,
|
|
814
845
|
windowMs: blocking.windowMs,
|
|
815
|
-
endsAt: verdict.retryTimestamp
|
|
846
|
+
endsAt: verdict.retryTimestamp,
|
|
847
|
+
user: blocking.user
|
|
816
848
|
});
|
|
817
849
|
throw refusal;
|
|
818
850
|
}
|
|
@@ -835,16 +867,18 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
835
867
|
// retry landing a moment late is refused on the earlier, which its wait holds, before taking the later, which a wait
|
|
836
868
|
// ending then needs for its own notice.
|
|
837
869
|
const bucket = Math.floor(refusal.retryAt.getTime() / NOTICE_BUCKET_MS);
|
|
870
|
+
// A key counting a server, a channel or everyone is shared by every caller it refuses, each told once
|
|
871
|
+
const key = refused.user === undefined ? refused.key : `${refused.key}:user:${refused.user}`;
|
|
838
872
|
const notices = refused.endsAt !== undefined ? [
|
|
839
873
|
{
|
|
840
|
-
key: `${
|
|
874
|
+
key: `${key}:notice:at:${refused.endsAt}`,
|
|
841
875
|
limit
|
|
842
876
|
}
|
|
843
877
|
] : [
|
|
844
878
|
bucket - 1,
|
|
845
879
|
bucket
|
|
846
880
|
].map((at)=>({
|
|
847
|
-
key: `${
|
|
881
|
+
key: `${key}:notice:${at}`,
|
|
848
882
|
limit
|
|
849
883
|
}));
|
|
850
884
|
try {
|
|
@@ -1066,19 +1100,35 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1066
1100
|
const guilds = options.config?.guilds ?? [];
|
|
1067
1101
|
return !everythingTarget(options) && guilds.length > 0 && ids(guilds).length === 0;
|
|
1068
1102
|
}
|
|
1103
|
+
/** Whether a command is an activity's primary entry point, which Discord takes only globally. */ const isEntryPoint = (command)=>command.body.type === discord_js.ApplicationCommandType.PrimaryEntryPoint;
|
|
1104
|
+
/** What a blank guild to register every command to says, as an unset variable gives one. */ const BLANK_GUILD = 'No commands were registered: the guild to register to is blank. Give a guild id, or leave it out to register where commands.* says.';
|
|
1069
1105
|
/**
|
|
1070
1106
|
* Where each command is sent: one bulk update per scope. A scope's update replaces everything the application has
|
|
1071
1107
|
* there, so the default scope is included even when it ends up empty, unless `commands.guilds` lists no guild id.
|
|
1108
|
+
*
|
|
1109
|
+
* A primary entry point command goes in the global update, as Discord refuses a guild update that holds one. A run
|
|
1110
|
+
* that sends everything to one guild sends nothing global, so it leaves the entry point out.
|
|
1072
1111
|
*/ function planTargets(commands, options, logger) {
|
|
1112
|
+
// Registering to the default scopes instead could publish everywhere
|
|
1113
|
+
if (options.onlyGuild !== undefined && !options.onlyGuild.trim()) {
|
|
1114
|
+
logger.error(BLANK_GUILD);
|
|
1115
|
+
return [];
|
|
1116
|
+
}
|
|
1073
1117
|
const everythingTo = everythingTarget(options);
|
|
1074
|
-
if (everythingTo)
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
},
|
|
1079
|
-
commands
|
|
1118
|
+
if (everythingTo) {
|
|
1119
|
+
// A global update from here would hold only what this run sends, deleting the application's other global commands
|
|
1120
|
+
for (const command of commands.filter(isEntryPoint)){
|
|
1121
|
+
logger.warn(`Entry point command "${command.name}" is registered globally only; a production run or \`meocord register\` registers it.`);
|
|
1080
1122
|
}
|
|
1081
|
-
|
|
1123
|
+
return [
|
|
1124
|
+
{
|
|
1125
|
+
scope: {
|
|
1126
|
+
guild: everythingTo
|
|
1127
|
+
},
|
|
1128
|
+
commands: commands.filter((command)=>!isEntryPoint(command))
|
|
1129
|
+
}
|
|
1130
|
+
];
|
|
1131
|
+
}
|
|
1082
1132
|
const defaultGuilds = ids(options.config?.guilds);
|
|
1083
1133
|
const nowhere = defaultGuildsEmpty(options);
|
|
1084
1134
|
const targets = new Map();
|
|
@@ -1096,7 +1146,12 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1096
1146
|
});
|
|
1097
1147
|
const unregistered = [];
|
|
1098
1148
|
for (const command of commands){
|
|
1099
|
-
if (command
|
|
1149
|
+
if (isEntryPoint(command)) {
|
|
1150
|
+
if (command.guilds) {
|
|
1151
|
+
logger.warn(`Entry point command "${command.name}" can only be registered globally, so the guilds its builder lists are ignored.`);
|
|
1152
|
+
}
|
|
1153
|
+
add('global', command);
|
|
1154
|
+
} else if (command.guilds) {
|
|
1100
1155
|
const own = ids(command.guilds);
|
|
1101
1156
|
// Falling back to the default scope could publish a staff-only command everywhere.
|
|
1102
1157
|
if (own.length === 0) logger.warn(`Command "${command.name}" lists no guild ids in its builder, so it is not registered.`);
|
|
@@ -1114,7 +1169,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1114
1169
|
}
|
|
1115
1170
|
}
|
|
1116
1171
|
if (nowhere) {
|
|
1117
|
-
const outcome = unregistered.length === 0 ? 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
|
|
1172
|
+
const outcome = unregistered.length === 0 ? targets.has('global') ? 'only the entry point command is registered globally' : 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
|
|
1118
1173
|
logger.warn(`commands.guilds lists no guild id, as an unset environment variable leaves it, so ${outcome}. ` + `Set the guild ids, or remove commands.guilds to register globally.`);
|
|
1119
1174
|
}
|
|
1120
1175
|
return [
|
|
@@ -1143,28 +1198,57 @@ const describeScope = (scope)=>scope === 'global' ? 'globally' : `to guild ${sco
|
|
|
1143
1198
|
return path__default.default.join(nodeModules, '.cache', 'meocord', `commands-${applicationId}-${scopeKey(scope)}.json`);
|
|
1144
1199
|
}
|
|
1145
1200
|
const hashOf = (commands)=>crypto.createHash('sha256').update(JSON.stringify(commands.map(({ body })=>body))).digest('hex');
|
|
1146
|
-
function
|
|
1201
|
+
function readRecord(file, logger) {
|
|
1147
1202
|
if (!file || !fs.existsSync(file)) return undefined;
|
|
1148
1203
|
try {
|
|
1149
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
1204
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1150
1205
|
} catch (error) {
|
|
1151
1206
|
logger.debug(`Could not read the registered commands, so they are sent again: ${String(error)}`);
|
|
1152
1207
|
return undefined;
|
|
1153
1208
|
}
|
|
1154
1209
|
}
|
|
1155
|
-
function
|
|
1210
|
+
function writeRecord(file, record, logger) {
|
|
1156
1211
|
if (!file) return;
|
|
1157
1212
|
try {
|
|
1158
1213
|
fs.mkdirSync(path__default.default.dirname(file), {
|
|
1159
1214
|
recursive: true
|
|
1160
1215
|
});
|
|
1161
|
-
fs.writeFileSync(file, `${JSON.stringify(
|
|
1162
|
-
hash
|
|
1163
|
-
})}\n`);
|
|
1216
|
+
fs.writeFileSync(file, `${JSON.stringify(record)}\n`);
|
|
1164
1217
|
} catch (error) {
|
|
1165
1218
|
logger.debug(`Could not record the registered commands: ${String(error)}`);
|
|
1166
1219
|
}
|
|
1167
1220
|
}
|
|
1221
|
+
/** Forgets what development last sent to a scope, once that scope is written some other way. */ function forgetRecord(file, logger) {
|
|
1222
|
+
if (!file || !fs.existsSync(file)) return;
|
|
1223
|
+
try {
|
|
1224
|
+
fs.rmSync(file);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
logger.debug(`Could not forget the registered commands: ${String(error)}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Each command's `id:version`, sorted, from what Discord lists or answers a bulk update with; `undefined` for anything
|
|
1231
|
+
* else. Discord gives a command a new version whenever it is updated, so the set changes with any edit, removal or
|
|
1232
|
+
* re-registration, from this checkout or any other.
|
|
1233
|
+
*/ function versionsOf(commands) {
|
|
1234
|
+
if (!Array.isArray(commands)) return undefined;
|
|
1235
|
+
const versions = commands.map((command)=>{
|
|
1236
|
+
const { id, version } = command ?? {};
|
|
1237
|
+
return typeof id === 'string' && typeof version === 'string' ? `${id}:${version}` : undefined;
|
|
1238
|
+
});
|
|
1239
|
+
return versions.every((version)=>version !== undefined) ? versions.sort() : undefined;
|
|
1240
|
+
}
|
|
1241
|
+
/** Whether Discord still holds a scope as development last sent it, by each command's version; a failed listing doesn't. */ async function stillAsSent(rest, route, record, scope, logger) {
|
|
1242
|
+
if (!record.versions) return false;
|
|
1243
|
+
let registered;
|
|
1244
|
+
try {
|
|
1245
|
+
registered = await rest.get(route);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
logger.debug(`Could not list the commands registered ${describeScope(scope)}, so they are sent again: ${String(error)}`);
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
return versionsOf(registered)?.join('\n') === record.versions.join('\n');
|
|
1251
|
+
}
|
|
1168
1252
|
function logRegistered(target, logger) {
|
|
1169
1253
|
const table = new Table__default.default({
|
|
1170
1254
|
head: [
|
|
@@ -1187,22 +1271,29 @@ function logRegistered(target, logger) {
|
|
|
1187
1271
|
};
|
|
1188
1272
|
for (const { name, body } of target.commands){
|
|
1189
1273
|
const options = Array.isArray(body.options) ? body.options : [];
|
|
1274
|
+
// A subcommand or a subcommand group, not a plain option
|
|
1275
|
+
const subcommands = options.filter((option)=>option.type === 1 || option.type === 2);
|
|
1190
1276
|
table.push([
|
|
1191
1277
|
name,
|
|
1192
1278
|
typeNames[typeof body.type === 'number' ? body.type : 1] ?? 'Command',
|
|
1193
|
-
|
|
1279
|
+
subcommands.map((option)=>option.name).join(', ')
|
|
1194
1280
|
]);
|
|
1195
1281
|
}
|
|
1196
1282
|
logger.log(`Registered ${target.commands.length} bot commands ${describeScope(target.scope)}:\n${table.toString()}`);
|
|
1197
1283
|
}
|
|
1198
1284
|
/**
|
|
1199
|
-
* Registers the commands, one bulk update per scope; development skips a scope unchanged since its last send
|
|
1200
|
-
* `force`. Once one is sent, and not to `onlyGuild`, the other scopes the configuration names are checked for
|
|
1285
|
+
* Registers the commands, one bulk update per scope; development skips a scope unchanged since its last send and still
|
|
1286
|
+
* registered as sent, which one listing per skipped scope checks by each command's version, unless `force`. A blank `onlyGuild` registers nothing. Once one is sent, and not to `onlyGuild`, the other scopes the configuration names are checked for
|
|
1201
1287
|
* leftovers, which `clearOther` removes; a development-guild run or a `commands.guilds` with no id only warns. Never
|
|
1202
1288
|
* throws.
|
|
1203
1289
|
* @returns Whether every scope was sent or unchanged, and `commands.guilds`, if set, lists a guild id.
|
|
1204
1290
|
*/ async function registerCommands(options) {
|
|
1205
1291
|
const { rest, applicationId, controllerClasses, logger, config, development, onlyGuild, force = false } = options;
|
|
1292
|
+
// An unset variable passed as the guild; registering to the default scopes instead could publish everywhere
|
|
1293
|
+
if (onlyGuild !== undefined && !onlyGuild.trim()) {
|
|
1294
|
+
logger.error(BLANK_GUILD);
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1206
1297
|
const commands = collectCommands(controllerClasses, logger);
|
|
1207
1298
|
if (!commands) return false;
|
|
1208
1299
|
const targets = planTargets(commands, {
|
|
@@ -1219,18 +1310,26 @@ function logRegistered(target, logger) {
|
|
|
1219
1310
|
let succeeded = !guildsEmpty;
|
|
1220
1311
|
let sent = false;
|
|
1221
1312
|
for (const target of targets){
|
|
1222
|
-
const file =
|
|
1313
|
+
const file = cacheFile(applicationId, target.scope);
|
|
1223
1314
|
const hash = hashOf(target.commands);
|
|
1224
|
-
|
|
1315
|
+
const route = routeFor(applicationId, target.scope);
|
|
1316
|
+
// Unchanged since development last sent it, and still what Discord holds there
|
|
1317
|
+
const record = development && !force ? readRecord(file, logger) : undefined;
|
|
1318
|
+
if (record?.hash === hash && await stillAsSent(rest, route, record, target.scope, logger)) {
|
|
1225
1319
|
logger.log(`Commands ${describeScope(target.scope)} are unchanged; not registering them again.`);
|
|
1226
1320
|
continue;
|
|
1227
1321
|
}
|
|
1228
1322
|
try {
|
|
1229
|
-
await rest.put(
|
|
1323
|
+
const answered = await rest.put(route, {
|
|
1230
1324
|
body: target.commands.map(({ body })=>body)
|
|
1231
1325
|
});
|
|
1232
1326
|
sent = true;
|
|
1233
|
-
|
|
1327
|
+
// Outside development the record would be stale, so the next development start sends again
|
|
1328
|
+
if (development) writeRecord(file, {
|
|
1329
|
+
hash,
|
|
1330
|
+
versions: versionsOf(answered)
|
|
1331
|
+
}, logger);
|
|
1332
|
+
else forgetRecord(file, logger);
|
|
1234
1333
|
logRegistered(target, logger);
|
|
1235
1334
|
} catch (error) {
|
|
1236
1335
|
succeeded = false;
|
|
@@ -1259,13 +1358,17 @@ function logRegistered(target, logger) {
|
|
|
1259
1358
|
if (!Array.isArray(existing) || existing.length === 0) continue;
|
|
1260
1359
|
const names = existing.map(({ name })=>name).join(', ');
|
|
1261
1360
|
if (!config?.clearOther || keptBecause) {
|
|
1262
|
-
|
|
1361
|
+
// Advising clearOther when it would not apply sends the reader to a setting that changes nothing
|
|
1362
|
+
const advice = !keptBecause ? 'Set commands.clearOther to remove them.' : config?.clearOther ? `clearOther is on, but they are not removed ${keptBecause}.` : `They are not removed ${keptBecause}, even with commands.clearOther.`;
|
|
1363
|
+
logger.warn(`${existing.length} command(s) are still registered ${describeScope(scope)} (${names}), which this ` + `configuration does not register to, so Discord keeps showing them there. ${advice}`);
|
|
1263
1364
|
continue;
|
|
1264
1365
|
}
|
|
1265
1366
|
try {
|
|
1266
1367
|
await rest.put(routeFor(applicationId, scope), {
|
|
1267
1368
|
body: []
|
|
1268
1369
|
});
|
|
1370
|
+
// What development last sent there is gone, so its next start sends again
|
|
1371
|
+
forgetRecord(cacheFile(applicationId, scope), logger);
|
|
1269
1372
|
logger.log(`Removed ${existing.length} command(s) left registered ${describeScope(scope)}: ${names}`);
|
|
1270
1373
|
} catch (error) {
|
|
1271
1374
|
logger.error(`Error removing the commands left registered ${describeScope(scope)}:`, error);
|
|
@@ -1376,7 +1479,7 @@ function isInterceptorWithParams(entry) {
|
|
|
1376
1479
|
});
|
|
1377
1480
|
/** Binds `cls` and its unbound dependencies as singletons, refusing any that injects `ExecutionContext`. */ function bindShared(container, cls) {
|
|
1378
1481
|
if (container.isBound(cls)) return;
|
|
1379
|
-
if (injectedTokens(cls).includes(controller_decorator.ExecutionContext))
|
|
1482
|
+
if (injectedTokens(cls).includes(controller_decorator.ExecutionContext)) return controller_decorator.startupError(singletonContextError(cls));
|
|
1380
1483
|
assertStartupChecked(container, cls);
|
|
1381
1484
|
makeInjectable(cls);
|
|
1382
1485
|
container.bind(cls).toSelf().inSingletonScope();
|
|
@@ -1571,7 +1674,7 @@ function isInterceptorWithParams(entry) {
|
|
|
1571
1674
|
/** Binds a filter as a singleton, after checking it is one. */ function prepareFilter(container, entry) {
|
|
1572
1675
|
const cls = filterClass(entry);
|
|
1573
1676
|
if (!Reflect.hasOwnMetadata(controller_decorator.META.catchTypes, cls)) {
|
|
1574
|
-
|
|
1677
|
+
return controller_decorator.startupError(new Error(`${cls.name || 'A filter'}: used as an exception filter, but not decorated with @Catch().`));
|
|
1575
1678
|
}
|
|
1576
1679
|
bindShared(container, cls);
|
|
1577
1680
|
}
|
|
@@ -1683,11 +1786,12 @@ const asList = (entries)=>Array.isArray(entries) ? entries : [
|
|
|
1683
1786
|
|
|
1684
1787
|
/** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
|
|
1685
1788
|
const logger$1 = new controller_decorator.Logger('Observer');
|
|
1686
|
-
/**
|
|
1789
|
+
/** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
|
|
1687
1790
|
for (const observer of observers){
|
|
1688
1791
|
if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
|
|
1689
1792
|
const name = typeof observer === 'function' ? observer.name : String(observer);
|
|
1690
|
-
|
|
1793
|
+
controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
|
|
1794
|
+
continue;
|
|
1691
1795
|
}
|
|
1692
1796
|
}
|
|
1693
1797
|
}
|
|
@@ -1942,12 +2046,16 @@ const NO_GLOBAL_STAGES = {
|
|
|
1942
2046
|
*/ function bindAppPresenter(container, app, client) {
|
|
1943
2047
|
const options = Reflect.getMetadata(controller_decorator.META.appOptions, app);
|
|
1944
2048
|
if (!options?.presenter) return undefined;
|
|
1945
|
-
|
|
2049
|
+
prepareAppPresenter(container, app);
|
|
1946
2050
|
const presenter = container.get(options.presenter);
|
|
1947
2051
|
container.bind(APP_PRESENTER).toConstantValue(presenter);
|
|
1948
2052
|
if (client) controller_decorator.setPresenter(client, presenter);
|
|
1949
2053
|
return presenter;
|
|
1950
2054
|
}
|
|
2055
|
+
/** Binds the app's presenter class as a shared stage, checking it as one, without making it. */ function prepareAppPresenter(container, app) {
|
|
2056
|
+
const options = Reflect.getMetadata(controller_decorator.META.appOptions, app);
|
|
2057
|
+
if (options?.presenter) bindShared(container, options.presenter);
|
|
2058
|
+
}
|
|
1951
2059
|
/** Where a container keeps the application's presenter. */ const APP_PRESENTER = Symbol('app_presenter');
|
|
1952
2060
|
/** The presenter `bindAppPresenter` resolved into `container`, if any. */ function appPresenterOf(container) {
|
|
1953
2061
|
return container.isBound(APP_PRESENTER) ? container.get(APP_PRESENTER) : undefined;
|
|
@@ -2037,18 +2145,21 @@ const stagesByGlobals = new WeakMap();
|
|
|
2037
2145
|
/**
|
|
2038
2146
|
* Binds the interceptors, filters and pipes every handler of `controllers` uses, and the global ones, as singletons, so
|
|
2039
2147
|
* one that cannot be shared, or a filter without `@Catch`, fails at startup; refuses there `@Defer`, `@Validate`,
|
|
2040
|
-
* `@UsePipe` and `@Cooldown` where they cannot apply, and two classes that keep state under one name.
|
|
2148
|
+
* `@UsePipe` and `@Cooldown` where they cannot apply, and two classes that keep state under one name. Binds only; it
|
|
2149
|
+
* makes nothing, so it runs with the other startup checks.
|
|
2041
2150
|
*/ function prepareHandlerStages(container, controllers) {
|
|
2042
2151
|
assertDistinctNamesWhereKeyed(controllers);
|
|
2043
2152
|
const globals = globalStagesOf(container);
|
|
2044
|
-
configureThemes(container, globals.theme, controllers, globals.themeFor);
|
|
2045
2153
|
for (const entry of globals.interceptors)prepareInterceptor(container, entry);
|
|
2046
2154
|
for (const entry of globals.filters)prepareFilter(container, entry);
|
|
2047
2155
|
for (const controller of controllers){
|
|
2048
2156
|
const prototype = controller.prototype;
|
|
2049
2157
|
for (const method of handlerMethods(prototype)){
|
|
2050
2158
|
const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
|
|
2051
|
-
if (kind)
|
|
2159
|
+
if (kind) {
|
|
2160
|
+
controller_decorator.startupError(deferMisuseError(controller.name, method, kind));
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2052
2163
|
for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
|
|
2053
2164
|
for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
|
|
2054
2165
|
for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
|
|
@@ -2056,6 +2167,10 @@ const stagesByGlobals = new WeakMap();
|
|
|
2056
2167
|
assertInputStagesOnInteractions(controller, prototype);
|
|
2057
2168
|
}
|
|
2058
2169
|
}
|
|
2170
|
+
/** Records the app's theme for the handlers of `controllers`, once the startup checks have passed: it makes the theme cache. */ function prepareThemes(container, controllers) {
|
|
2171
|
+
const globals = globalStagesOf(container);
|
|
2172
|
+
configureThemes(container, globals.theme, controllers, globals.themeFor);
|
|
2173
|
+
}
|
|
2059
2174
|
/** Whether a class has state keyed by its name: a cooldown on a handler, or a `@Once` handler. */ function keyedByName(cls) {
|
|
2060
2175
|
const prototype = cls.prototype;
|
|
2061
2176
|
const handlers = [
|
|
@@ -2072,7 +2187,8 @@ const stagesByGlobals = new WeakMap();
|
|
|
2072
2187
|
for (const cls of classes){
|
|
2073
2188
|
const other = byName.get(cls.name);
|
|
2074
2189
|
if (other && other !== cls && (keyedByName(cls) || keyedByName(other))) {
|
|
2075
|
-
|
|
2190
|
+
controller_decorator.startupError(new Error(`${cls.name}: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share ` + `their counts. Rename one of them.`));
|
|
2191
|
+
continue;
|
|
2076
2192
|
}
|
|
2077
2193
|
byName.set(cls.name, cls);
|
|
2078
2194
|
}
|
|
@@ -2100,11 +2216,12 @@ const stagesByGlobals = new WeakMap();
|
|
|
2100
2216
|
const { schema, pipes } = handlerInputStages(prototype, method);
|
|
2101
2217
|
const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
|
|
2102
2218
|
if (schema || pipes.length > 0) {
|
|
2103
|
-
|
|
2219
|
+
controller_decorator.startupError(new Error(`${controller.name}.${method}: @Validate and @UsePipe are for interaction and patterned message handlers, ` + `whose options, customId params, modal fields and pattern params they check, and this is ${handler}.`));
|
|
2220
|
+
continue;
|
|
2104
2221
|
}
|
|
2105
2222
|
// A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
|
|
2106
2223
|
if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
|
|
2107
|
-
|
|
2224
|
+
controller_decorator.startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
|
|
2108
2225
|
}
|
|
2109
2226
|
}
|
|
2110
2227
|
}
|
|
@@ -3261,7 +3378,8 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3261
3378
|
if (own.hidden !== undefined && typeof own.hidden !== 'boolean') throw new Error(`hidden is true or false, not ${JSON.stringify(own.hidden)}.`);
|
|
3262
3379
|
aliases = aliasPatterns(pattern, parsed.tokens, own.aliases);
|
|
3263
3380
|
} catch (error) {
|
|
3264
|
-
|
|
3381
|
+
controller_decorator.startupError(new Error(`${controllerClass.name}.${method}: @MessageHandler('${pattern}'): ${error.message}`));
|
|
3382
|
+
continue;
|
|
3265
3383
|
}
|
|
3266
3384
|
const shared = {
|
|
3267
3385
|
controllerClass,
|
|
@@ -3300,7 +3418,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3300
3418
|
routes.splice(j--, 1);
|
|
3301
3419
|
continue;
|
|
3302
3420
|
}
|
|
3303
|
-
|
|
3421
|
+
controller_decorator.startupError(new Error(`${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} ` + `match the same messages, so only one of them could ever answer those. Change one pattern, or give one its own prefix.`));
|
|
3304
3422
|
}
|
|
3305
3423
|
}
|
|
3306
3424
|
return routes;
|
|
@@ -4179,7 +4297,7 @@ const COMMAND_TYPES = new Set([
|
|
|
4179
4297
|
*
|
|
4180
4298
|
* @group Controllers
|
|
4181
4299
|
* @see {@link HandlerEntry}
|
|
4182
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
4300
|
+
* @see {@link https://meocord.dev/docs/4.2/handler-discovery | Handler discovery}
|
|
4183
4301
|
*/ class HandlerRegistry {
|
|
4184
4302
|
/**
|
|
4185
4303
|
* Lists the registered handlers.
|
|
@@ -4424,7 +4542,7 @@ async function withTimeout(promise, ms, what) {
|
|
|
4424
4542
|
*
|
|
4425
4543
|
* @group Controllers
|
|
4426
4544
|
* @see {@link ShardCallResult}
|
|
4427
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
4545
|
+
* @see {@link https://meocord.dev/docs/4.2/sharding | Sharding}
|
|
4428
4546
|
*/ class ShardContext {
|
|
4429
4547
|
/** The ids of the shards this process runs. With process sharding, one id; otherwise every shard, once ready. */ get ids() {
|
|
4430
4548
|
if (this.client?.shard) return [
|
|
@@ -4601,9 +4719,9 @@ const isFactoryProvider = (provider)=>'useFactory' in provider;
|
|
|
4601
4719
|
}
|
|
4602
4720
|
const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
|
|
4603
4721
|
/**
|
|
4604
|
-
* Checks each provider's shape and indexes them by token,
|
|
4605
|
-
*
|
|
4606
|
-
*
|
|
4722
|
+
* Checks each provider's shape and indexes them by token, reporting each that cannot be bound, such as one with no
|
|
4723
|
+
* token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a token
|
|
4724
|
+
* provided twice, as a startup error; see `startupError`.
|
|
4607
4725
|
*/ function providerMap(providers, where) {
|
|
4608
4726
|
const map = new Map();
|
|
4609
4727
|
for (const provider of providers){
|
|
@@ -4612,10 +4730,12 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4612
4730
|
if (typeof entry === 'function') {
|
|
4613
4731
|
const name = tokenName(entry);
|
|
4614
4732
|
const listing = where.includes('@MeoCord') ? 'list a service in @MeoCord({ services })' : 'a class a controller or service injects is bound for you, so it needs no listing';
|
|
4615
|
-
|
|
4733
|
+
controller_decorator.startupError(new Error(`${where}: ${name} is a class, not a provider: ${listing}. To put something in its place, write ` + `{ provide: ${name}, useValue } or { provide: ${name}, useClass }.`));
|
|
4734
|
+
continue;
|
|
4616
4735
|
}
|
|
4617
4736
|
if (!entry || typeof entry !== 'object' || !isToken(entry.provide)) {
|
|
4618
|
-
|
|
4737
|
+
controller_decorator.startupError(new Error(`${where}: a provider has no token: set provide to a class, a string or a symbol.`));
|
|
4738
|
+
continue;
|
|
4619
4739
|
}
|
|
4620
4740
|
const name = tokenName(entry.provide);
|
|
4621
4741
|
const kinds = [
|
|
@@ -4624,26 +4744,35 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4624
4744
|
'useFactory'
|
|
4625
4745
|
].filter((kind)=>kind in entry);
|
|
4626
4746
|
if (kinds.length !== 1) {
|
|
4627
|
-
|
|
4747
|
+
controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
4748
|
+
continue;
|
|
4628
4749
|
}
|
|
4629
4750
|
if ('useClass' in entry && typeof entry.useClass !== 'function') {
|
|
4630
|
-
|
|
4751
|
+
controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
4752
|
+
continue;
|
|
4631
4753
|
}
|
|
4632
4754
|
if ('useClass' in entry && injectedTokens(entry.useClass).includes(controller_decorator.ExecutionContext)) {
|
|
4633
|
-
|
|
4755
|
+
controller_decorator.startupError(new Error(`${where}: the provider for ${name} uses ${tokenName(entry.useClass)}, which injects ExecutionContext, but it is ` + "made once and shared, so it would keep the first call's context for every later call. Inject " + 'ExecutionContext only into guards.'));
|
|
4756
|
+
continue;
|
|
4634
4757
|
}
|
|
4635
4758
|
if ('useFactory' in entry) {
|
|
4636
4759
|
if (typeof entry.useFactory !== 'function') {
|
|
4637
|
-
|
|
4760
|
+
controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useFactory that is not a function.`));
|
|
4761
|
+
continue;
|
|
4638
4762
|
}
|
|
4639
4763
|
if (entry.inject !== undefined && !(Array.isArray(entry.inject) && entry.inject.every(isToken))) {
|
|
4640
|
-
|
|
4764
|
+
controller_decorator.startupError(new Error(`${where}: the provider for ${name} has an inject that is not a list of tokens.`));
|
|
4765
|
+
continue;
|
|
4641
4766
|
}
|
|
4642
4767
|
if (entry.inject?.includes(controller_decorator.ExecutionContext)) {
|
|
4643
|
-
|
|
4768
|
+
controller_decorator.startupError(new Error(`${where}: the provider for ${name} injects ExecutionContext, but its factory runs once and its value is ` + "shared, so it would keep the first call's context for every later call. Inject ExecutionContext only into guards."));
|
|
4769
|
+
continue;
|
|
4644
4770
|
}
|
|
4645
4771
|
}
|
|
4646
|
-
if (map.has(entry.provide))
|
|
4772
|
+
if (map.has(entry.provide)) {
|
|
4773
|
+
controller_decorator.startupError(new Error(`${where}: ${name} is provided twice.`));
|
|
4774
|
+
continue;
|
|
4775
|
+
}
|
|
4647
4776
|
map.set(entry.provide, provider);
|
|
4648
4777
|
}
|
|
4649
4778
|
return map;
|
|
@@ -4696,22 +4825,23 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4696
4825
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
4697
4826
|
}
|
|
4698
4827
|
/**
|
|
4699
|
-
*
|
|
4700
|
-
*
|
|
4701
|
-
* never missing.
|
|
4828
|
+
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
4829
|
+
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
4702
4830
|
*/ function assertProvided(container, providers, classes, where) {
|
|
4703
4831
|
const missing = (token)=>(typeof token === 'string' || typeof token === 'symbol') && !container.isBound(token);
|
|
4704
4832
|
for (const cls of classes){
|
|
4705
4833
|
const token = injectedTokens(cls).find(missing);
|
|
4706
4834
|
if (token !== undefined) {
|
|
4707
|
-
|
|
4835
|
+
controller_decorator.startupError(new Error(`${cls.name}: it injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
|
|
4836
|
+
continue;
|
|
4708
4837
|
}
|
|
4709
4838
|
}
|
|
4710
4839
|
for (const [provided, provider] of providers){
|
|
4711
4840
|
const dependencies = isFactoryProvider(provider) ? provider.inject ?? [] : isClassProvider(provider) ? injectedTokens(provider.useClass) : [];
|
|
4712
4841
|
const token = dependencies.find(missing);
|
|
4713
4842
|
if (token !== undefined) {
|
|
4714
|
-
|
|
4843
|
+
controller_decorator.startupError(new Error(`${tokenName(provided)}: its provider injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
|
|
4844
|
+
continue;
|
|
4715
4845
|
}
|
|
4716
4846
|
}
|
|
4717
4847
|
}
|
|
@@ -4744,16 +4874,21 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4744
4874
|
*/ function assertTypedParameters(classes, decorators) {
|
|
4745
4875
|
for (const cls of classes){
|
|
4746
4876
|
const instead = meocordClassAdvice(cls);
|
|
4747
|
-
if (instead && undecoratedConstructor(cls))
|
|
4877
|
+
if (instead && undecoratedConstructor(cls)) {
|
|
4878
|
+
controller_decorator.startupError(new Error(`${cls.name}: MeoCord does not inject it; ${instead}.`));
|
|
4879
|
+
continue;
|
|
4880
|
+
}
|
|
4748
4881
|
if (undecoratedConstructor(cls)) {
|
|
4749
4882
|
const decorator = decorators.get(cls);
|
|
4750
|
-
|
|
4883
|
+
controller_decorator.startupError(new Error(`${cls.name}: its constructor takes parameters, but ${cls.name} has no decorator, so TypeScript recorded none of ` + `their types and it cannot be created. ${decorator ? `Decorate it with ${decorator}.` : 'Decorate it with @Service(), or give a class from a package a provider in @MeoCord({ providers }).'}`));
|
|
4884
|
+
continue;
|
|
4751
4885
|
}
|
|
4752
4886
|
const index = untypedParameter(cls);
|
|
4753
4887
|
if (index === -1) continue;
|
|
4754
4888
|
const injectors = classes.filter((other)=>other !== cls && injectedTokens(other).includes(cls)).map((other)=>other.name);
|
|
4755
4889
|
const injectedBy = injectors.length === 0 ? '' : ` (${injectors.length === 1 ? injectors[0] : `${injectors.slice(0, -1).join(', ')} and ${injectors.at(-1)}`} ` + `inject${injectors.length === 1 ? 's' : ''} ${cls.name})`;
|
|
4756
|
-
|
|
4890
|
+
controller_decorator.startupError(new Error(`${cls.name}: parameter ${index + 1} of its constructor has no runtime type, so it cannot be created. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).'));
|
|
4891
|
+
continue;
|
|
4757
4892
|
}
|
|
4758
4893
|
}
|
|
4759
4894
|
/** For each container, the tokens MeoCord binds itself, each to the app's class it stands for, if any. */ const ownTokens = new WeakMap();
|
|
@@ -4822,15 +4957,20 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4822
4957
|
}
|
|
4823
4958
|
/**
|
|
4824
4959
|
* Runs every factory in `order`, awaiting those that return a promise, so each value is made before
|
|
4825
|
-
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails.
|
|
4826
|
-
|
|
4960
|
+
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
|
|
4961
|
+
* it makes no further value once the app is stopping.
|
|
4962
|
+
*/ async function resolveProviders(container, providers, order, control) {
|
|
4827
4963
|
for (const token of order){
|
|
4828
4964
|
const provider = providers.get(token);
|
|
4829
4965
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
4966
|
+
if (control?.stopped()) return;
|
|
4967
|
+
control?.pending(tokenName(token));
|
|
4830
4968
|
try {
|
|
4831
4969
|
await container.getAsync(token);
|
|
4832
4970
|
} catch (error) {
|
|
4833
4971
|
throw providerFailure(token, error);
|
|
4972
|
+
} finally{
|
|
4973
|
+
control?.pending(undefined);
|
|
4834
4974
|
}
|
|
4835
4975
|
}
|
|
4836
4976
|
}
|
|
@@ -4904,9 +5044,12 @@ exports.messageStartsFor = messageStartsFor;
|
|
|
4904
5044
|
exports.needsGuild = needsGuild;
|
|
4905
5045
|
exports.nonInteractionHandler = nonInteractionHandler;
|
|
4906
5046
|
exports.observeUnclaimed = observeUnclaimed;
|
|
5047
|
+
exports.prepareAppPresenter = prepareAppPresenter;
|
|
4907
5048
|
exports.prepareHandlerStages = prepareHandlerStages;
|
|
5049
|
+
exports.prepareThemes = prepareThemes;
|
|
4908
5050
|
exports.providerMap = providerMap;
|
|
4909
5051
|
exports.reachableClasses = reachableClasses;
|
|
5052
|
+
exports.recordInstanceContainers = recordInstanceContainers;
|
|
4910
5053
|
exports.registerClientTheme = registerClientTheme;
|
|
4911
5054
|
exports.registerCommands = registerCommands;
|
|
4912
5055
|
exports.registrationKey = registrationKey;
|