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
|
@@ -6,7 +6,7 @@ import { ReactionHandlerAction } from '../enum/controller.enum.js';
|
|
|
6
6
|
import { CooldownStore, MemoryCooldownStore } from '../common/cooldown-store.js';
|
|
7
7
|
import { ExecutionContext } from '../common/execution-context.js';
|
|
8
8
|
import { Translator, missingTranslatorError } from '../common/translator.js';
|
|
9
|
-
import { injectedTokens, singletonContextError } from '../core/guard-runner.js';
|
|
9
|
+
import { injectedTokens, singletonContextError, recordInstanceContainers } from '../core/guard-runner.js';
|
|
10
10
|
import { appPresenterOf, runHandler, appStages, bindGlobalStages, prepareHandlerStages, prepareAppPresenter, prepareThemes, bindAppPresenter } from '../core/handler-pipeline.js';
|
|
11
11
|
import { setPresenter } from '../common/response/presenter.js';
|
|
12
12
|
import { componentRouteFor, commandMismatch, handlerInput } from '../core/handler-input.js';
|
|
@@ -23,21 +23,25 @@ import { messageCommandHooks } from '../core/message-params.js';
|
|
|
23
23
|
import { appObservers, assertObservers, bindObservers } from '../core/observer-runner.js';
|
|
24
24
|
import { HandlerRegistry } from '../core/handler-registry.js';
|
|
25
25
|
import { adviseInPlaceOfInjecting, meocordClasses } from '../core/meocord-classes.js';
|
|
26
|
-
import { startupClasses, assertStartupClasses, lateGuardCheck } from '../core/startup-roots.js';
|
|
26
|
+
import { startupClasses, assertStartupClasses, lateGuardCheck, stageDependencies } from '../core/startup-roots.js';
|
|
27
27
|
import { markStartupChecked, assertStartupChecked } from '../core/startup-checked.js';
|
|
28
28
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
29
29
|
import { ShardContext, shardCallHandler } from '../core/shard-context.js';
|
|
30
30
|
import { isAppClassToken } from '../core/lifecycle-order.js';
|
|
31
|
-
import {
|
|
32
|
-
import { createMockClient } from './mock-interaction.js';
|
|
31
|
+
import { runShutdownSequence, runReadyHooks, ReadyPass, lifecycleEntry, callsSettled } from '../core/lifecycle-hooks.js';
|
|
32
|
+
import { createMockClient, noteReactionDispatch } from './mock-interaction.js';
|
|
33
33
|
import { Dispatcher } from '../core/dispatcher.js';
|
|
34
34
|
import { createFallback } from '../core/fallback.js';
|
|
35
35
|
import { Logger } from '../common/logger.js';
|
|
36
36
|
import { isExplainedError, markExplained } from '../common/explained-error.js';
|
|
37
|
-
import {
|
|
37
|
+
import { collectStartupErrors, declaredErrorsOf, startupError, stopOnStartupErrors, isRefusal, logStartupErrors } from '../util/refusal.util.js';
|
|
38
38
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, shutdownTimeoutProblem } from '../util/shutdown-timeout.util.js';
|
|
39
|
-
import {
|
|
39
|
+
import { isConstructor, describeValue } from '../util/value.util.js';
|
|
40
|
+
import { resolveProviders, tokenName, bindsOwnToken, providerMap, assertProvidedServices, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies } from '../core/providers.js';
|
|
40
41
|
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
42
|
+
import { getReactionHandlers } from '../decorator/controller.decorator.js';
|
|
43
|
+
import { strictMocks } from './strict-mocks.js';
|
|
44
|
+
import { warnOnce } from '../common/deprecation.js';
|
|
41
45
|
import { META } from '../util/metadata-keys.js';
|
|
42
46
|
import { isUserOutcome } from '../common/user-outcome.js';
|
|
43
47
|
|
|
@@ -110,7 +114,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
110
114
|
await this.resolving;
|
|
111
115
|
if (options.ready) {
|
|
112
116
|
const { client = createMockClient(), primary = true } = options.ready === true ? {} : options.ready;
|
|
113
|
-
this.readying ?? (this.readying = this.runReady(client, primary));
|
|
117
|
+
this.readying ?? (this.readying = this.readyPass.run(()=>this.runReady(client, primary)));
|
|
114
118
|
await this.readying;
|
|
115
119
|
}
|
|
116
120
|
return this;
|
|
@@ -125,12 +129,21 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
125
129
|
* It first gives up the theme read outside calls, if `init({ ready: true })` made it this module's;
|
|
126
130
|
* reads outside calls then return MeoCord's defaults until another module or app is ready.
|
|
127
131
|
*
|
|
132
|
+
* A stopped bot runs nothing more. Once `close()` is called, a module still runs what `dispatch`, `invoke` and `emit`
|
|
133
|
+
* are given, against services shut down, or shutting down, and warns once that it is closed, so a `close()` in the
|
|
134
|
+
* wrong place shows.
|
|
135
|
+
*
|
|
128
136
|
* The cooldown store and what it injects shut down last, in the same sequence as the bot's. When any of them has an
|
|
129
137
|
* `onShutdown`, the calls `invoke`, `dispatch` and `emit` have under way finish first, then the store operations
|
|
130
138
|
* they started, so the store's last writes still reach it. The module stops waiting for the whole sequence after
|
|
131
139
|
* its `shutdownTimeout`, 10 seconds unless set, and logs that it did. Give a test whose fake store never answers, or
|
|
132
140
|
* whose `onShutdown` never settles, a short `shutdownTimeout`.
|
|
133
141
|
*
|
|
142
|
+
* A `close()` while `init({ ready: true })` runs the `onReady` hooks waits for them first, within the same
|
|
143
|
+
* `shutdownTimeout`. An `onReady` still running once only the hooks' share of it is left is named in a warning, and
|
|
144
|
+
* its class is shut down all the same, as everything the module constructed is. One that calls `close()` itself isn't
|
|
145
|
+
* waited for.
|
|
146
|
+
*
|
|
134
147
|
* @returns Once every hook has run. Rejects with the error of a hook that failed, or an
|
|
135
148
|
* `AggregateError` naming each when several did.
|
|
136
149
|
*
|
|
@@ -145,14 +158,18 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
145
158
|
* expect(module.get(POOL).ended).toBe(true)
|
|
146
159
|
* ```
|
|
147
160
|
*/ async close() {
|
|
161
|
+
// An onReady awaiting close() would otherwise hold up the close that waits for it
|
|
162
|
+
this.readyPass.excuseCurrent();
|
|
148
163
|
this.closing ?? (this.closing = (async ()=>{
|
|
149
|
-
// A close during init waits for the
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.
|
|
164
|
+
// A close during init waits for the providers it started, and the sequence for its onReady hooks, so it shuts
|
|
165
|
+
// down whatever they constructed
|
|
166
|
+
await this.resolving?.catch(()=>undefined);
|
|
167
|
+
const entries = ()=>this.lifecycle.filter((unit)=>this.constructed.has(unit.token))// Already made, so this returns the instance; a provided value may be anything, null included
|
|
168
|
+
.map((unit)=>lifecycleEntry(unit, this.container.get(unit.token) ?? {}));
|
|
153
169
|
const failures = [];
|
|
154
170
|
await runShutdownSequence(this.container, entries, {
|
|
155
171
|
drainCalls: ()=>callsSettled(this.calls),
|
|
172
|
+
readyPass: this.readyPass,
|
|
156
173
|
timeoutMs: this.shutdownTimeout,
|
|
157
174
|
hookFailed: (name, error)=>failures.push({
|
|
158
175
|
name,
|
|
@@ -166,6 +183,11 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
166
183
|
})());
|
|
167
184
|
await this.closing;
|
|
168
185
|
}
|
|
186
|
+
/** Warns once, after `close()`, that a call runs against services already shut down. */ warnIfClosed() {
|
|
187
|
+
if (this.closing === undefined || this.warnedClosed) return;
|
|
188
|
+
this.warnedClosed = true;
|
|
189
|
+
new Logger('TestingModule').warn('This testing module is closed, and its services are shut down, or shutting down, so dispatch, invoke and emit run ' + 'against that state. Close it after the last call, in afterEach.');
|
|
190
|
+
}
|
|
169
191
|
async runReady(client, primary) {
|
|
170
192
|
// As the bot claims it before it comes online, so onReady reads the app's theme
|
|
171
193
|
claimAmbientAppTheme(this.container);
|
|
@@ -181,6 +203,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
181
203
|
}, [], {
|
|
182
204
|
resolveFailed: failed,
|
|
183
205
|
hookFailed: failed
|
|
206
|
+
}, {
|
|
207
|
+
runOnReady: this.readyPass.runOnReady
|
|
184
208
|
});
|
|
185
209
|
throwFailures('onReady', failures);
|
|
186
210
|
}
|
|
@@ -252,7 +276,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
252
276
|
* @returns Whether the handler ran, and the error a filter handled, if any. Rejects with an error no
|
|
253
277
|
* filter handles, or with the error a filter throws: the built-in fallback, which answers such
|
|
254
278
|
* errors in the bot, does not run here. Rejects before running anything with an interaction or a
|
|
255
|
-
* message the handler's route does not match, naming both.
|
|
279
|
+
* message the handler's route does not match, naming both. Given fewer arguments than the handler
|
|
280
|
+
* declares, with a first that is neither an interaction nor a message, such as a reaction without its
|
|
281
|
+
* `ReactionEvent`, it rejects under `useStrictMocks()`, and otherwise warns once and runs the handler.
|
|
256
282
|
*
|
|
257
283
|
* @example
|
|
258
284
|
* ```ts
|
|
@@ -274,6 +300,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
274
300
|
* expect(outcome.error).toBeInstanceOf(GuardDeniedError)
|
|
275
301
|
* ```
|
|
276
302
|
*/ async invoke(controller, methodName, ...args) {
|
|
303
|
+
this.warnIfClosed();
|
|
277
304
|
await this.init();
|
|
278
305
|
if (!this.controllers.includes(controller)) {
|
|
279
306
|
throw new Error(`${controller.name} is not a controller of this testing module. Add it to \`controllers\`.`);
|
|
@@ -303,6 +330,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
303
330
|
hooks = messageCommandHooks(route, params, first, start, given, this.messageOptions.types);
|
|
304
331
|
}
|
|
305
332
|
}
|
|
333
|
+
this.checkArgumentCount(controller, methodName, instance[methodName], first, callArgs.length);
|
|
306
334
|
const presenter = appPresenterOf(this.container);
|
|
307
335
|
// A message command's errors are drawn by the app's presenter too, as an interaction's are
|
|
308
336
|
const client = first instanceof BaseInteraction || first instanceof Message ? first.client : undefined;
|
|
@@ -319,6 +347,19 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
319
347
|
error
|
|
320
348
|
};
|
|
321
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Refuses under `useStrictMocks()`, or warns once otherwise, a call that gives a handler fewer arguments than it
|
|
352
|
+
* declares when `invoke` builds none for it, as for a reaction given without its `ReactionEvent`.
|
|
353
|
+
*/ checkArgumentCount(controller, methodName, method, first, given) {
|
|
354
|
+
if (first instanceof BaseInteraction || first instanceof Message || method.length <= given) return;
|
|
355
|
+
const reaction = getReactionHandlers(controller.prototype).some((handler)=>handler.method === methodName);
|
|
356
|
+
const problem = `${controller.name}.${methodName} takes ${method.length} arguments, and invoke was given ${given}: ` + (reaction ? 'pass its ReactionEvent after the reaction, as { user, action }.' : 'pass each argument it takes.');
|
|
357
|
+
if (strictMocks()) throw new Error(problem);
|
|
358
|
+
const key = `${controller.name}.${methodName}`;
|
|
359
|
+
if (this.warnedArgumentCounts.has(key)) return;
|
|
360
|
+
this.warnedArgumentCounts.add(key);
|
|
361
|
+
new Logger('TestingModule').warn(problem);
|
|
362
|
+
}
|
|
322
363
|
/** Counts `call` as under way until it settles. */ track(call) {
|
|
323
364
|
this.calls.add(call);
|
|
324
365
|
void call.then(()=>this.calls.delete(call), ()=>this.calls.delete(call));
|
|
@@ -364,6 +405,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
364
405
|
return this.dispatcher;
|
|
365
406
|
}
|
|
366
407
|
async dispatch(input, options) {
|
|
408
|
+
this.warnIfClosed();
|
|
367
409
|
await this.init();
|
|
368
410
|
this.registerClient(input);
|
|
369
411
|
const dispatcher = this.dispatcherOf();
|
|
@@ -381,6 +423,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
381
423
|
unhandled: (error)=>unhandled.push(error)
|
|
382
424
|
};
|
|
383
425
|
if (options) {
|
|
426
|
+
noteReactionDispatch(input);
|
|
384
427
|
await this.track(dispatcher.reaction(input, {
|
|
385
428
|
user: options.user,
|
|
386
429
|
action: options.action ?? ReactionHandlerAction.ADD
|
|
@@ -410,15 +453,20 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
410
453
|
};
|
|
411
454
|
}
|
|
412
455
|
/**
|
|
413
|
-
* Emits a client event to the module's `@On` and `@Once` handlers, through the
|
|
414
|
-
*
|
|
415
|
-
* module's controllers, class providers and their dependencies all receive it. A
|
|
416
|
-
* handles only the first event, as it would on a client.
|
|
456
|
+
* Emits a client event to the module's `@On` and `@Once` handlers, through the pipeline the app runs
|
|
457
|
+
* them in up to its event fallback: the global guards of the module's `app`, then each handler's own.
|
|
458
|
+
* Handlers on the module's controllers, class providers and their dependencies all receive it. A
|
|
459
|
+
* `@Once` handler handles only the first event, as it would on a client.
|
|
460
|
+
*
|
|
461
|
+
* The bot's event fallback doesn't run here. On a bot, a guard's `GuardDeniedError` is logged and the
|
|
462
|
+
* event skipped, and a handler's `UserError` is logged and, when the event carries a message, answered
|
|
463
|
+
* as a reply to it; here both reject `emit` as any error does, so a test checks them with `rejects`.
|
|
417
464
|
*
|
|
418
465
|
* @param event - The client event, such as `'guildMemberAdd'`.
|
|
419
466
|
* @param args - The event's arguments, typed from discord.js's `ClientEvents`.
|
|
420
|
-
* @returns How many handlers ran. Rejects once every handler has settled if any threw
|
|
421
|
-
*
|
|
467
|
+
* @returns How many handlers ran. Rejects once every handler has settled if any threw, a guard's
|
|
468
|
+
* `GuardDeniedError` and a handler's `UserError` included: with that error when one handler failed,
|
|
469
|
+
* or an `AggregateError` of them when several did.
|
|
422
470
|
*
|
|
423
471
|
* @example
|
|
424
472
|
* ```ts
|
|
@@ -430,6 +478,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
430
478
|
* expect(ran).toBe(1)
|
|
431
479
|
* ```
|
|
432
480
|
*/ async emit(event, ...args) {
|
|
481
|
+
this.warnIfClosed();
|
|
433
482
|
await this.init();
|
|
434
483
|
const calls = [];
|
|
435
484
|
for (const cls of this.eventClasses){
|
|
@@ -474,7 +523,10 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
474
523
|
this.appWarnUnanswered = appWarnUnanswered;
|
|
475
524
|
this.services = services;
|
|
476
525
|
this.shutdownTimeout = shutdownTimeout;
|
|
526
|
+
/** The `onReady` hooks' pass, which `close()` waits for, less one whose `onReady` called `close()`. */ this.readyPass = new ReadyPass();
|
|
527
|
+
this.warnedClosed = false;
|
|
477
528
|
/** The `@Once` handlers that have already handled their event, as a client forgets its once listeners. */ this.firedOnce = new Set();
|
|
529
|
+
/** The handlers already warned about for being given fewer arguments than they take, so each is named once. */ this.warnedArgumentCounts = new Set();
|
|
478
530
|
/** The calls under way, each removed once it settles: what `close()` lets finish before the cooldown store stops. */ this.calls = new Set();
|
|
479
531
|
}
|
|
480
532
|
}
|
|
@@ -539,6 +591,10 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
539
591
|
/**
|
|
540
592
|
* Replaces a guard with a stub wherever it applies, globally or on a controller or handler.
|
|
541
593
|
*
|
|
594
|
+
* It stands in for the guard wherever the guard is bound, a provider of the same token included, and wins over an
|
|
595
|
+
* `overrideProvider` of it. The stub needs a `canActivate`, its own or its prototype's: `compile()` refuses one without
|
|
596
|
+
* it under `useStrictMocks()`, and otherwise warns.
|
|
597
|
+
*
|
|
542
598
|
* @param guard - The guard class to replace.
|
|
543
599
|
* @example
|
|
544
600
|
* ```ts
|
|
@@ -556,6 +612,10 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
556
612
|
* Replaces an interceptor with a stub wherever it applies, globally or on a controller or handler.
|
|
557
613
|
* The stub's `intercept` receives the context and `next`; call `next.handle()` to run the handler.
|
|
558
614
|
*
|
|
615
|
+
* It stands in for the interceptor wherever it is bound, a provider of the same token included, and wins over an
|
|
616
|
+
* `overrideProvider` of it. A stub without `intercept` is refused by `compile()` under `useStrictMocks()`, and
|
|
617
|
+
* otherwise warned about.
|
|
618
|
+
*
|
|
559
619
|
* @param interceptor - The interceptor class to replace.
|
|
560
620
|
* @example
|
|
561
621
|
* ```ts
|
|
@@ -573,6 +633,10 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
573
633
|
* Replaces an exception filter with a stub wherever it applies. The filter's `@Catch` still decides
|
|
574
634
|
* which errors reach the stub.
|
|
575
635
|
*
|
|
636
|
+
* It stands in for the filter wherever it is bound, a provider of the same token included, and wins over an
|
|
637
|
+
* `overrideProvider` of it. A stub without `catch` is refused by `compile()` under `useStrictMocks()`, and otherwise
|
|
638
|
+
* warned about.
|
|
639
|
+
*
|
|
576
640
|
* @param filter - The filter class to replace.
|
|
577
641
|
* @example
|
|
578
642
|
* ```ts
|
|
@@ -705,9 +769,52 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
705
769
|
const providers = providerMap(this.wiring?.providers ?? [], '@MeoCord({ providers })');
|
|
706
770
|
for (const [token, provider] of providerMap(this.options.providers ?? [], "the testing module's providers"))providers.set(token, provider);
|
|
707
771
|
for (const [token, override] of this.overrides)providers.set(token, override);
|
|
772
|
+
// A stage override stands in for its class wherever the class is bound. A stage also provided takes the stub in
|
|
773
|
+
// place of its provider, one binding per token, and runs lifecycle hooks as that provider would; any other is
|
|
774
|
+
// bound on its own below, as the bot never makes a lifecycle unit of it. A stub that can't run is named here
|
|
775
|
+
const stageStubs = new Map();
|
|
776
|
+
for (const [method, overrides, kind] of [
|
|
777
|
+
[
|
|
778
|
+
'canActivate',
|
|
779
|
+
this.guardOverrides,
|
|
780
|
+
'overrideGuard'
|
|
781
|
+
],
|
|
782
|
+
[
|
|
783
|
+
'intercept',
|
|
784
|
+
this.interceptorOverrides,
|
|
785
|
+
'overrideInterceptor'
|
|
786
|
+
],
|
|
787
|
+
[
|
|
788
|
+
'catch',
|
|
789
|
+
this.filterOverrides,
|
|
790
|
+
'overrideFilter'
|
|
791
|
+
]
|
|
792
|
+
]){
|
|
793
|
+
for (const [stage, stub] of overrides){
|
|
794
|
+
if (providers.has(stage)) providers.set(stage, {
|
|
795
|
+
provide: stage,
|
|
796
|
+
useValue: stub
|
|
797
|
+
});
|
|
798
|
+
else stageStubs.set(stage, stub);
|
|
799
|
+
if (typeof stub[method] === 'function') continue;
|
|
800
|
+
const problem = `${kind}(${stage.name}).useValue(…) has no ${method} method.`;
|
|
801
|
+
if (strictMocks()) startupError(new Error(problem));
|
|
802
|
+
else warnOnce(new Logger('TestingModule'), `${problem} A call that reaches ${stage.name} fails; useStrictMocks() refuses it here.`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
708
805
|
// Each pass of checks ends before the next step that is no check, so a module with one error stops where it always did
|
|
709
806
|
stopOnStartupErrors();
|
|
807
|
+
// What the walk below takes as classes, reported with what the walk finds; an entry refused here is left out of it
|
|
710
808
|
const services = this.wiring?.services ?? [];
|
|
809
|
+
if (this.options.app) assertProvidedServices(this.options.app.name, services, providers, "@MeoCord({ providers }) or the testing module's providers");
|
|
810
|
+
for (const controller of this.options.controllers ?? []){
|
|
811
|
+
if (!isConstructor(controller)) startupError(new Error(`the testing module's controllers takes classes, not ${describeValue(controller)}.`));
|
|
812
|
+
}
|
|
813
|
+
const observers = [
|
|
814
|
+
...this.options.app ? appObservers(this.options.app) : [],
|
|
815
|
+
...this.options.observers ?? []
|
|
816
|
+
];
|
|
817
|
+
assertObservers("the testing module's observers", observers);
|
|
711
818
|
const appOptions = this.options.app ? Reflect.getMetadata(META.appOptions, this.options.app) : undefined;
|
|
712
819
|
// The app's own store, as the bot binds it, whether the module is given the app or built from it; none when the
|
|
713
820
|
// test provides the CooldownStore, so nothing builds the app's or asks for what it injects
|
|
@@ -716,18 +823,16 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
716
823
|
const themeResolver = themeResolverClass(stages?.themeFor?.resolvers);
|
|
717
824
|
// Every class the module runs, as the app finds them, except stage classes an override stands in for
|
|
718
825
|
const i18n = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.i18n;
|
|
826
|
+
const appPresenterClass = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.presenter;
|
|
719
827
|
const startup = startupClasses({
|
|
720
|
-
controllers: this.options.controllers ?? [],
|
|
828
|
+
controllers: (this.options.controllers ?? []).filter(isConstructor),
|
|
721
829
|
services,
|
|
722
830
|
providers,
|
|
723
831
|
stages,
|
|
724
|
-
presenter:
|
|
832
|
+
presenter: appPresenterClass,
|
|
725
833
|
cooldownStore: store,
|
|
726
834
|
themeFor: stages?.themeFor?.resolvers,
|
|
727
|
-
observers
|
|
728
|
-
...this.options.app ? appObservers(this.options.app) : [],
|
|
729
|
-
...this.options.observers ?? []
|
|
730
|
-
],
|
|
835
|
+
observers,
|
|
731
836
|
stubs: [
|
|
732
837
|
...this.guardOverrides.keys(),
|
|
733
838
|
...this.interceptorOverrides.keys(),
|
|
@@ -752,16 +857,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
752
857
|
throw new Error(`${needClient.map((cls)=>cls.name).join(', ')} ${needClient.length === 1 ? 'injects' : 'inject'} the Discord Client, ` + 'which a testing module does not make: give one in its providers, such as ' + '{ provide: Client, useValue: createMockClient() }.');
|
|
753
858
|
});
|
|
754
859
|
}
|
|
755
|
-
//
|
|
756
|
-
for (const [
|
|
757
|
-
container.bind(guardClass).toConstantValue(stub);
|
|
758
|
-
}
|
|
759
|
-
for (const [filterClass, stub] of this.filterOverrides){
|
|
760
|
-
container.bind(filterClass).toConstantValue(stub);
|
|
761
|
-
}
|
|
762
|
-
for (const [interceptorClass, stub] of this.interceptorOverrides){
|
|
763
|
-
container.bind(interceptorClass).toConstantValue(stub);
|
|
764
|
-
}
|
|
860
|
+
// A stage override nothing provides, bound before the classes so none of them binds the stage itself
|
|
861
|
+
for (const [stage, stub] of stageStubs)container.bind(stage).toConstantValue(stub);
|
|
765
862
|
// The app's cooldown policy, so a test of a failing store sees what the bot would do
|
|
766
863
|
if (appOptions && !container.isBound(COOLDOWN_POLICY)) container.bind(COOLDOWN_POLICY).toConstantValue(cooldownPolicyFrom(appOptions));
|
|
767
864
|
// The app's translator, unless a provider stands in for it
|
|
@@ -825,16 +922,12 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
825
922
|
// As the app does when it is created, so a test sees the refusal the bot would give
|
|
826
923
|
assertDistinctCommands(this.options.controllers ?? []);
|
|
827
924
|
if (this.options.app) prepareAppPresenter(container, this.options.app);
|
|
828
|
-
const observers = [
|
|
829
|
-
...this.options.app ? appObservers(this.options.app) : [],
|
|
830
|
-
...this.options.observers ?? []
|
|
831
|
-
];
|
|
832
|
-
assertObservers("the testing module's observers", observers);
|
|
833
|
-
// Only @Observer classes are bound as observers
|
|
834
|
-
stopOnStartupErrors();
|
|
835
925
|
bindObservers(container, observers);
|
|
836
926
|
// The store calls ask: the test's own when it provides CooldownStore, else the app's
|
|
837
927
|
const boundStore = store ?? (providers.has(CooldownStore) ? CooldownStore : undefined);
|
|
928
|
+
// What the stages and presenter inject, as the bot makes it; a stage the test replaced contributes nothing
|
|
929
|
+
const stageServices = stageDependencies(startup, providers, appPresenterClass);
|
|
930
|
+
for (const service of stageServices)bindClass(service);
|
|
838
931
|
// The order the app runs lifecycle hooks in: its cooldown store, then providers, controllers and observers, each
|
|
839
932
|
// after what it injects
|
|
840
933
|
const lifecycle = resolutionOrder(container, providers, [
|
|
@@ -843,6 +936,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
843
936
|
] : [],
|
|
844
937
|
...providers.keys(),
|
|
845
938
|
...services,
|
|
939
|
+
...stageServices,
|
|
846
940
|
...themeResolver ? [
|
|
847
941
|
themeResolver
|
|
848
942
|
] : [],
|
|
@@ -863,6 +957,11 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
863
957
|
...this.options.controllers ?? [],
|
|
864
958
|
...startup.classes
|
|
865
959
|
])Reflect.defineMetadata(META.container, container, cls);
|
|
960
|
+
// And each instance with the module that made it, as another module made from the same classes stamps them again
|
|
961
|
+
recordInstanceContainers(container, [
|
|
962
|
+
...this.options.controllers ?? [],
|
|
963
|
+
...startup.classes
|
|
964
|
+
]);
|
|
866
965
|
prepareThemes(container, appClasses);
|
|
867
966
|
// A handler with no builder is how a fixture is written, so only what is always a mistake is named
|
|
868
967
|
warnUnregisteredCommands(this.options.controllers ?? [], {
|
|
@@ -936,7 +1035,15 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
936
1035
|
* @param options - The controllers, providers, observers and app the module is built from.
|
|
937
1036
|
* @returns The builder, to override what the test replaces before `compile()`.
|
|
938
1037
|
*/ static create(options) {
|
|
939
|
-
|
|
1038
|
+
// A controller listed twice is one controller, as the bot and fromApp take it
|
|
1039
|
+
return new TestingModuleBuilder({
|
|
1040
|
+
...options,
|
|
1041
|
+
...options.controllers && {
|
|
1042
|
+
controllers: [
|
|
1043
|
+
...new Set(options.controllers)
|
|
1044
|
+
]
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
940
1047
|
}
|
|
941
1048
|
/**
|
|
942
1049
|
* Starts building a testing module from a whole `@MeoCord` app, wired as the bot wires it.
|
|
@@ -954,7 +1061,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
954
1061
|
* @param app - The class `@MeoCord` decorates.
|
|
955
1062
|
* @param options - Providers that replace the app's by token, extra controllers, and observers.
|
|
956
1063
|
* @returns The builder, whose `override*` methods still apply, for `compile()`.
|
|
957
|
-
* @throws TypeError when `app` is not a `@MeoCord` class.
|
|
1064
|
+
* @throws TypeError when `app` is not a `@MeoCord` class. After {@link reportAllStartupErrors}, an app whose
|
|
1065
|
+
* `@MeoCord` was refused throws the error it was refused for instead.
|
|
958
1066
|
*
|
|
959
1067
|
* @example
|
|
960
1068
|
* ```ts
|
|
@@ -995,7 +1103,13 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
995
1103
|
* ```
|
|
996
1104
|
*/ static fromApp(app, options = {}) {
|
|
997
1105
|
const appOptions = Reflect.getMetadata(META.appOptions, app);
|
|
998
|
-
if (!appOptions)
|
|
1106
|
+
if (!appOptions) {
|
|
1107
|
+
// A refused @MeoCord stores no options, so its own errors, kept with startupErrors: 'all', say why
|
|
1108
|
+
collectStartupErrors(()=>declaredErrorsOf([
|
|
1109
|
+
app
|
|
1110
|
+
]).forEach(startupError));
|
|
1111
|
+
throw new TypeError(`${app?.name ?? String(app)} is not a @MeoCord app: fromApp takes the class @MeoCord decorates.`);
|
|
1112
|
+
}
|
|
999
1113
|
const controllers = [
|
|
1000
1114
|
...new Set([
|
|
1001
1115
|
...appOptions.controllers,
|