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
|
@@ -42,7 +42,7 @@ const VISIBLE = new Set([
|
|
|
42
42
|
* @group Testing
|
|
43
43
|
* @category Inspection
|
|
44
44
|
* @see {@link TestingModule}
|
|
45
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
45
|
+
* @see {@link https://meocord.dev/docs/4.2/invoke-and-dispatch | Invoke and dispatch}
|
|
46
46
|
*/ function getResponse(interaction) {
|
|
47
47
|
const state = existingResponse(interaction);
|
|
48
48
|
// A mock records every answer it gets; respond()'s edits through the channel are its own, so they are merged in
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findComponentRouteConflicts, buildComponentRoutes, matchComponentRoute } from '../core/component-routes.js';
|
|
1
|
+
import { findComponentRouteConflicts, buildComponentRoutes, matchComponentRoute, readCustomId } from '../core/component-routes.js';
|
|
2
2
|
import { isCustomIdRouted } from '../util/interaction.util.js';
|
|
3
3
|
import { staticMessageStarts, matchMessageRoute, buildMessageRoutes, fitsScope } from '../core/message-routes.js';
|
|
4
4
|
import { needsGuild } from '../core/message-params.js';
|
|
@@ -47,7 +47,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
47
47
|
* @group Testing
|
|
48
48
|
* @category Inspection
|
|
49
49
|
* @see {@link findRouteConflicts}
|
|
50
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
50
|
+
* @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
|
|
51
51
|
*/ function resolveRoute(app, input) {
|
|
52
52
|
if ('content' in input) {
|
|
53
53
|
const { messages = {} } = appOptionsOf(app);
|
|
@@ -73,7 +73,10 @@ import { META } from '../util/metadata-keys.js';
|
|
|
73
73
|
const { route, params, text } = matched;
|
|
74
74
|
const method = route.meta.methodName;
|
|
75
75
|
const typed = Object.keys(route.types).length > 0;
|
|
76
|
-
|
|
76
|
+
// The routes ranked behind it that the customId matches too, which dispatch never reaches for it
|
|
77
|
+
const behind = routes.slice(routes.indexOf(route) + 1);
|
|
78
|
+
const alsoMatches = behind.filter((other)=>other.meta.type === input.type && readCustomId(other.pattern, other.meta.regex, input.customId)).map((other)=>other.pattern);
|
|
79
|
+
const resolved = {
|
|
77
80
|
controller: route.controllerClass,
|
|
78
81
|
method,
|
|
79
82
|
handler: route.controllerClass.prototype[method],
|
|
@@ -82,28 +85,34 @@ import { META } from '../util/metadata-keys.js';
|
|
|
82
85
|
values: params
|
|
83
86
|
}
|
|
84
87
|
};
|
|
88
|
+
return alsoMatches.length > 0 ? readableOnly(resolved, {
|
|
89
|
+
alsoMatches
|
|
90
|
+
}) : resolved;
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
|
-
* Finds component patterns that can match the same `customId`, which MeoCord otherwise only warns
|
|
93
|
+
* Finds component patterns that rank equally and can match the same `customId`, which MeoCord otherwise only warns
|
|
94
|
+
* about at startup.
|
|
88
95
|
*
|
|
89
|
-
* Use it in a test to keep the warning from reaching production
|
|
90
|
-
*
|
|
96
|
+
* Use it in a test to keep the warning from reaching production. Patterns rank segment by segment, a literal before a
|
|
97
|
+
* param and then the narrower type, so two collide only with the same literals and equally narrow params at each
|
|
98
|
+
* position, such as `{action:close|reopen}` and `{step:close|confirm}`, which both take `close`; the one listed
|
|
99
|
+
* first runs. Patterns are compared within a component type.
|
|
91
100
|
*
|
|
92
101
|
* @param app - The application class decorated with `@MeoCord`.
|
|
93
|
-
* @returns Each pair of patterns that can match the same `customId`, with its component type.
|
|
102
|
+
* @returns Each pair of patterns that rank equally and can match the same `customId`, with its component type.
|
|
94
103
|
*
|
|
95
104
|
* @example
|
|
96
105
|
* ```ts
|
|
97
106
|
* import { expect } from 'vitest'
|
|
98
107
|
*
|
|
99
108
|
* @Controller()
|
|
100
|
-
* class
|
|
101
|
-
* @Command('
|
|
102
|
-
* async
|
|
103
|
-
* @Command('
|
|
104
|
-
* async
|
|
109
|
+
* class TicketController {
|
|
110
|
+
* @Command('ticket/{action:close|reopen}', CommandType.BUTTON)
|
|
111
|
+
* async act(interaction: ButtonInteraction) { await respond(interaction).send('Done.') }
|
|
112
|
+
* @Command('ticket/{step:close|confirm}', CommandType.BUTTON)
|
|
113
|
+
* async step(interaction: ButtonInteraction) { await respond(interaction).send('Next.') }
|
|
105
114
|
* }
|
|
106
|
-
* @MeoCord({ controllers: [
|
|
115
|
+
* @MeoCord({ controllers: [TicketController], clientOptions: { intents: [] } })
|
|
107
116
|
* class App {}
|
|
108
117
|
* expect(findRouteConflicts(App)).toHaveLength(1)
|
|
109
118
|
* ```
|
|
@@ -112,7 +121,23 @@ import { META } from '../util/metadata-keys.js';
|
|
|
112
121
|
* @category Inspection
|
|
113
122
|
* @see {@link resolveRoute}
|
|
114
123
|
*/ function findRouteConflicts(app) {
|
|
115
|
-
return findComponentRouteConflicts(buildComponentRoutes(controllersOf(app)))
|
|
124
|
+
return findComponentRouteConflicts(buildComponentRoutes(controllersOf(app))).map(({ type, patterns, runs })=>readableOnly({
|
|
125
|
+
type: type,
|
|
126
|
+
patterns
|
|
127
|
+
}, {
|
|
128
|
+
runs
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `target` with `fields` added as properties a test reads but `toEqual` and its kin, which compare enumerable keys,
|
|
133
|
+
* don't see, so an assertion written for the fields it had before still holds.
|
|
134
|
+
*/ function readableOnly(target, fields) {
|
|
135
|
+
for (const [key, value] of Object.entries(fields))Object.defineProperty(target, key, {
|
|
136
|
+
value,
|
|
137
|
+
enumerable: false,
|
|
138
|
+
configurable: true
|
|
139
|
+
});
|
|
140
|
+
return target;
|
|
116
141
|
}
|
|
117
142
|
|
|
118
143
|
export { findRouteConflicts, resolveRoute };
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { shared } from '../util/shared-state.util.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The id of the bot every mock client is logged in as, so a test can mention it. Fixed, and below the
|
|
3
5
|
* ids other mocks take, so it is the same on every run and in any test order, and no mock shares it.
|
|
4
6
|
*/ const MOCK_BOT_ID = '1300000000000000000';
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The last id a mock was given, for both builds of this meocord version; ids count up from a real snowflake, so each
|
|
9
|
+
* mock's is its own.
|
|
10
|
+
*/ const ids = shared('mockIds', ()=>({
|
|
11
|
+
last: 1400000000000000000n
|
|
12
|
+
}));
|
|
13
|
+
/** A snowflake-shaped id nothing else a mock holds in this run has, for a mock or the media it resolves. */ const nextSnowflake = ()=>String(++ids.last);
|
|
7
14
|
|
|
8
15
|
export { MOCK_BOT_ID, nextSnowflake };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { setStartupErrorsMode } from '../util/refusal.util.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Has decorators keep the startup errors they find, as a bot built with `startupErrors: 'all'` does, so a testing
|
|
5
|
+
* module reports every one at once.
|
|
6
|
+
*
|
|
7
|
+
* Call it once in the test runner's setup file, before any test imports a controller: a decorator runs when its class
|
|
8
|
+
* is defined, as its file is imported. A decorator given what it cannot use then throws nothing as the file loads, and
|
|
9
|
+
* `MeoCordTestingModule.create(...).compile()` logs every startup error of the classes it runs, each with its handler
|
|
10
|
+
* and file, then throws the first. Without it, a decorator throws its error as its class is defined.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // vitest.setup.ts
|
|
15
|
+
* reportAllStartupErrors()
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @group Testing
|
|
19
|
+
* @category Module
|
|
20
|
+
* @see {@link MeoCordTestingModule}
|
|
21
|
+
* @see {@link https://meocord.dev/docs/4.2/configuration | Configuration}
|
|
22
|
+
*/ function reportAllStartupErrors() {
|
|
23
|
+
setStartupErrorsMode('all');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { reportAllStartupErrors };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { shared } from '../util/shared-state.util.js';
|
|
2
|
+
|
|
3
|
+
// Whether useStrictMocks() is on, and whether a mock was made before it could be, for both builds of this version
|
|
4
|
+
const settings = shared('strictMocks', ()=>({
|
|
5
|
+
strict: false,
|
|
6
|
+
mockMade: false
|
|
7
|
+
}));
|
|
8
|
+
/** Whether mocks compute discord.js's state, as {@link useStrictMocks} turns on. */ const strictMocks = ()=>settings.strict;
|
|
9
|
+
/** Notes that a mock was made, which useStrictMocks() must come before. */ function noteMockMade() {
|
|
10
|
+
settings.mockMade = true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Has every mock from `meocord/testing` compute the values discord.js computes, where it reads a placeholder otherwise.
|
|
14
|
+
*
|
|
15
|
+
* Call it once in the test runner's setup file. A message's `editable`, `deletable`, `pinnable`, `crosspostable`,
|
|
16
|
+
* `bulkDeletable`, `hasThread` and `partial`, a member's `manageable`, `kickable`, `bannable` and `moderatable`, a
|
|
17
|
+
* role's `editable`, a channel's and a thread's `viewable`, `manageable`, `deletable`, `joinable` and the rest, a voice
|
|
18
|
+
* channel's `full`, and `partial` on users, channels and reactions then run discord.js's own getters against the mock,
|
|
19
|
+
* and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
|
|
20
|
+
* `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
|
|
21
|
+
* `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
|
|
22
|
+
* `mentions.repliedUser` and a modal's `message` are `null`, and a reaction has a whole message of its own, so
|
|
23
|
+
* MeoCord's dispatcher fetches neither. No placeholder warning is logged.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
|
|
27
|
+
* server's member cache from the start, @everyone has the permissions Discord gives it in a new server, and a channel
|
|
28
|
+
* or thread made without a server has one of its own, a thread with a text channel as its parent. A message has empty
|
|
29
|
+
* `content`, a thread is neither archived nor locked, and a reaction's `count` is 1. A user, server or channel made
|
|
30
|
+
* with a generated id was created when the mock was made, as a message is. An interaction's `memberPermissions` apply
|
|
31
|
+
* its channel's overwrites, a thread's parent's for a thread, to its member's permissions. An interaction is in its
|
|
32
|
+
* message's channel and server, a guild's or a member's server, or, given a `guildId` alone, a server the bot isn't in.
|
|
33
|
+
* A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
|
|
34
|
+
* to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
|
|
35
|
+
* one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
|
|
36
|
+
* `mockResolvedValue`, is refused too, where without it the mock runs it and warns. The next major version (5.0)
|
|
37
|
+
* computes these values, and refuses those answers, without the call.
|
|
38
|
+
*
|
|
39
|
+
* `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
|
|
40
|
+
* reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
|
|
41
|
+
*
|
|
42
|
+
* `compile()` also refuses an `overrideGuard`, `overrideInterceptor` or `overrideFilter` stub without the stage's
|
|
43
|
+
* method, `canActivate`, `intercept` or `catch`; without strict mocks, it warns and builds the module.
|
|
44
|
+
*
|
|
45
|
+
* @throws Error when a mock already exists, which was made without it.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* // vitest.setup.ts
|
|
50
|
+
* useStrictMocks()
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @group Testing
|
|
54
|
+
* @category Mocks
|
|
55
|
+
* @see {@link useMockFn}
|
|
56
|
+
* @see {@link https://meocord.dev/docs/4.2/mocks | Mocks}
|
|
57
|
+
*/ function useStrictMocks() {
|
|
58
|
+
if (settings.strict) return;
|
|
59
|
+
if (settings.mockMade) {
|
|
60
|
+
throw new Error('useStrictMocks() goes before any mock is made: a mock made before it reads placeholders, and the two kinds would ' + "mix. Call it once, in the test runner's setup file.");
|
|
61
|
+
}
|
|
62
|
+
settings.strict = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { noteMockMade, strictMocks, useStrictMocks };
|
|
@@ -35,7 +35,7 @@ function mockTheme(overrides, where) {
|
|
|
35
35
|
* @group Testing
|
|
36
36
|
* @category Mocks
|
|
37
37
|
* @see {@link withTheme}
|
|
38
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
38
|
+
* @see {@link https://meocord.dev/docs/4.2/theming | Theming}
|
|
39
39
|
*/ function createMockTheme(...[overrides]) {
|
|
40
40
|
return mockTheme(overrides, 'createMockTheme');
|
|
41
41
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, realpathSync } from 'node:fs';
|
|
|
2
2
|
|
|
3
3
|
/** Where the built bundle's pre-entry records the bundle's own path. */ const BUNDLE_ENTRY_KEY = Symbol.for('meocord.bundleEntry');
|
|
4
4
|
/** Where the built bundle's pre-entry records the mode the bundle was built in. */ const BUILD_MODE_KEY = Symbol.for('meocord.buildMode');
|
|
5
|
+
/** Where the built bot's pre-entry records `startupErrors` from its config, before the application's modules load. */ const STARTUP_ERRORS_KEY = Symbol.for('meocord.startupErrorsMode');
|
|
5
6
|
/**
|
|
6
7
|
* The file a shard process runs: the built bundle, as its pre-entry recorded it, else the script this
|
|
7
8
|
* process was started with; undefined when there is none, as for code run with `node -e` outside a built bundle.
|
|
@@ -19,4 +20,4 @@ import { existsSync, realpathSync } from 'node:fs';
|
|
|
19
20
|
return typeof recorded === 'string' ? recorded : undefined;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export { BUILD_MODE_KEY, BUNDLE_ENTRY_KEY, buildMode, bundleEntry, isBuiltApplication };
|
|
23
|
+
export { BUILD_MODE_KEY, BUNDLE_ENTRY_KEY, STARTUP_ERRORS_KEY, buildMode, bundleEntry, isBuiltApplication };
|
|
@@ -19,28 +19,48 @@ import chalk from 'chalk';
|
|
|
19
19
|
console.error(chalk.red(`Error finding package directory for ${moduleName}: Module ${moduleName} not found in node_modules.`));
|
|
20
20
|
return null;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Loads `meocord.config.ts` and checks its shape, as a command that builds and a watch-mode reload both do, printing
|
|
24
|
+
* the warnings for options it does not know.
|
|
25
|
+
*
|
|
26
|
+
* @returns The config, or the report of why it cannot be used: missing, failing to load, or with options of the wrong
|
|
27
|
+
* type, each of them listed. A config that fails to load also gives the reason alone, as `loadError`.
|
|
28
|
+
*/ function checkSourceConfig() {
|
|
29
|
+
const meocordConfigPath = path.resolve(process.cwd(), 'meocord.config.ts');
|
|
30
|
+
if (!fs.existsSync(meocordConfigPath)) return {
|
|
31
|
+
problem: 'Configuration file "meocord.config.ts" is missing!'
|
|
32
|
+
};
|
|
33
|
+
const loaded = readMeoCordSourceConfig();
|
|
34
|
+
if ('error' in loaded) {
|
|
35
|
+
return {
|
|
36
|
+
problem: `meocord.config.ts could not be loaded, so nothing was built or started:\n ${loaded.error}`,
|
|
37
|
+
loadError: loaded.error
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const problem = shapeProblem(loaded.config);
|
|
41
|
+
return problem ? {
|
|
42
|
+
problem
|
|
43
|
+
} : {
|
|
44
|
+
config: loaded.config
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Prints why a config cannot be used, as {@link checkSourceConfig} reports it. */ function reportConfigProblem(problem) {
|
|
48
|
+
console.error(chalk.red(problem));
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* Loads `meocord.config.ts` and checks its shape, exiting when it is missing, fails to load, or has
|
|
24
52
|
* options of the wrong type; options it does not know are reported and left alone.
|
|
25
53
|
*
|
|
26
54
|
* @returns The config, which a build compiles and the bot then runs with.
|
|
27
55
|
*/ async function compileAndValidateConfig() {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
56
|
+
const checked = checkSourceConfig();
|
|
57
|
+
if ('problem' in checked) {
|
|
58
|
+
reportConfigProblem(checked.problem);
|
|
31
59
|
await wait(100);
|
|
32
60
|
process.exit(1);
|
|
33
61
|
return;
|
|
34
62
|
}
|
|
35
|
-
|
|
36
|
-
if ('error' in loaded) {
|
|
37
|
-
console.error(chalk.red(`meocord.config.ts could not be loaded, so nothing was built or started:\n ${loaded.error}`));
|
|
38
|
-
await wait(100);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await assertConfigShape(loaded.config);
|
|
43
|
-
return loaded.config;
|
|
63
|
+
return checked.config;
|
|
44
64
|
}
|
|
45
65
|
/**
|
|
46
66
|
* Checks the configuration a command that skips the build runs with: the compiled config when a build
|
|
@@ -63,13 +83,18 @@ import chalk from 'chalk';
|
|
|
63
83
|
return compileAndValidateConfig();
|
|
64
84
|
}
|
|
65
85
|
/** Exits with every problem when a configuration has options of the wrong type, and warns about unknown ones. */ async function assertConfigShape(config) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.error(chalk.red(`meocord.config.ts has ${errors.length} problem(s):\n${errors.map((error)=>` - ${error}`).join('\n')}`));
|
|
86
|
+
const problem = shapeProblem(config);
|
|
87
|
+
if (!problem) return;
|
|
88
|
+
console.error(chalk.red(problem));
|
|
70
89
|
await wait(100);
|
|
71
90
|
process.exit(1);
|
|
72
91
|
}
|
|
92
|
+
/** Every option of the wrong type in a configuration, as one report, or undefined; options it does not know are warned about. */ function shapeProblem(config) {
|
|
93
|
+
const { errors, warnings } = configProblems(config);
|
|
94
|
+
for (const warning of warnings)console.warn(chalk.yellow(`meocord.config.ts: ${warning}`));
|
|
95
|
+
if (errors.length === 0) return undefined;
|
|
96
|
+
return `meocord.config.ts has ${errors.length} problem(s):\n${errors.map((error)=>` - ${error}`).join('\n')}`;
|
|
97
|
+
}
|
|
73
98
|
/**
|
|
74
99
|
* Exits unless the config {@link compileAndValidateConfig} or {@link validateRunConfig} returned has a Discord token.
|
|
75
100
|
* Starting or registering needs one and building does not, so a new app builds before it has a token.
|
|
@@ -80,10 +105,13 @@ import chalk from 'chalk';
|
|
|
80
105
|
process.exit(1);
|
|
81
106
|
}
|
|
82
107
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Sets `NODE_ENV` for a command run in `mode`, before its config loads: development whatever the shell holds, so `--dev`
|
|
110
|
+
* reads the development files and registers as development, and production unless the shell set a mode of its own.
|
|
111
|
+
*/ function setEnvironment(mode) {
|
|
112
|
+
if (mode === 'development' || !process.env.NODE_ENV) {
|
|
85
113
|
process.env.NODE_ENV = mode;
|
|
86
114
|
}
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
export { assertConfigShape, compileAndValidateConfig, findModulePackageDir, setEnvironment, validateDiscordToken, validateRunConfig };
|
|
117
|
+
export { assertConfigShape, checkSourceConfig, compileAndValidateConfig, findModulePackageDir, reportConfigProblem, setEnvironment, validateDiscordToken, validateRunConfig };
|
|
@@ -19,8 +19,8 @@ const slots = globalThis;
|
|
|
19
19
|
* on Windows a signal ends a process outright, skipping its `onShutdown` hooks. The bundle's pre-entry calls it first,
|
|
20
20
|
* so a stop is heard however early it comes; until an app or shard manager says how it stops, the process exits at
|
|
21
21
|
* once, as SIGTERM's default would.
|
|
22
|
-
*/ function listenForDevRunnerStop() {
|
|
23
|
-
if (!underDevRunner() || slots[LISTENING]) return;
|
|
22
|
+
*/ function listenForDevRunnerStop(shard = isShardProcess(process.env)) {
|
|
23
|
+
if (!underDevRunner(shard) || slots[LISTENING]) return;
|
|
24
24
|
slots[LISTENING] = true;
|
|
25
25
|
process.on('message', (message)=>{
|
|
26
26
|
if (!isDevRunnerCommand(message)) return;
|
|
@@ -31,19 +31,19 @@ const slots = globalThis;
|
|
|
31
31
|
// A 'message' listener keeps the process alive for its channel; what the bot does decides that, as without one
|
|
32
32
|
process.channel?.unref();
|
|
33
33
|
}
|
|
34
|
-
/** Has a stop from `meocord start --dev` run `stop`, as SIGINT and SIGTERM do
|
|
35
|
-
listenForDevRunnerStop();
|
|
34
|
+
/** Has a stop from `meocord start --dev` run `stop`, as SIGINT and SIGTERM do, in a process whose role is `shard`. */ function onDevRunnerStop(stop, shard) {
|
|
35
|
+
listenForDevRunnerStop(shard);
|
|
36
36
|
slots[STOP] = stop;
|
|
37
37
|
}
|
|
38
|
-
/** Whether `meocord start --dev` runs this process and listens on its channel. */ function underDevRunner() {
|
|
39
|
-
return process.env[DEV_RUNNER_ENV] === '1' && !!process.send && !
|
|
38
|
+
/** Whether `meocord start --dev` runs this process, a shard or not as `shard` says, and listens on its channel. */ function underDevRunner(shard) {
|
|
39
|
+
return process.env[DEV_RUNNER_ENV] === '1' && !!process.send && !shard;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Tells `meocord start --dev`, when it runs this process, whether the bot could log in, and waits until it is sent, or
|
|
43
43
|
* until the dev runner is taken to be gone. A caller sets the exit code before sending, so it holds however the send
|
|
44
44
|
* ends. A shard tells its manager instead, which tells the dev runner in turn.
|
|
45
|
-
*/ async function tellDevRunner(message) {
|
|
46
|
-
if (underDevRunner()) await sendToParent(message);
|
|
45
|
+
*/ async function tellDevRunner(message, shard) {
|
|
46
|
+
if (underDevRunner(shard)) await sendToParent(message);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export { DEV_RUNNER_ENV, isDevRunnerCommand, isDevRunnerMessage, listenForDevRunnerStop, onDevRunnerStop, tellDevRunner, underDevRunner };
|
|
@@ -7,16 +7,15 @@ import '../core/theme-validation.js';
|
|
|
7
7
|
import '../common/decorator.js';
|
|
8
8
|
import 'reflect-metadata';
|
|
9
9
|
import 'discord.js';
|
|
10
|
-
import '../common/
|
|
11
|
-
import '../common/
|
|
12
|
-
import './interaction.util.js';
|
|
13
|
-
import '../common/response/flags.js';
|
|
14
|
-
import '../common/response/components.js';
|
|
10
|
+
import '../common/response/response-state.js';
|
|
11
|
+
import '../common/errors.js';
|
|
15
12
|
import 'node:crypto';
|
|
16
13
|
import '../decorator/controller.decorator.js';
|
|
17
14
|
import '../core/scalar-types.js';
|
|
18
15
|
import 'node:fs';
|
|
19
16
|
import 'node:url';
|
|
17
|
+
import '../common/translator.js';
|
|
18
|
+
import '../common/meocord-text.js';
|
|
20
19
|
import { kebabCase, startCase, camelCase } from 'lodash-es';
|
|
21
20
|
import { fileURLToPath } from 'url';
|
|
22
21
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ApplicationCommandOptionType, ModalSubmitInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, RoleSelectMenuInteraction, UserSelectMenuInteraction, StringSelectMenuInteraction, ButtonInteraction, PrimaryEntryPointCommandInteraction, ContextMenuCommandInteraction, ChatInputCommandInteraction
|
|
1
|
+
import { ApplicationCommandOptionType, MessageComponentInteraction, ModalSubmitInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, RoleSelectMenuInteraction, UserSelectMenuInteraction, StringSelectMenuInteraction, ButtonInteraction, PrimaryEntryPointCommandInteraction, ContextMenuCommandInteraction, ChatInputCommandInteraction } from 'discord.js';
|
|
2
2
|
import { CommandType } from '../enum/controller.enum.js';
|
|
3
3
|
import { quoteForLog } from './user-text.util.js';
|
|
4
|
-
import { withArticle } from './value.util.js';
|
|
4
|
+
import { setOwn, withArticle } from './value.util.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The discord.js interaction class each command type handles, shared by `@Command`, the dispatcher and the testing
|
|
@@ -99,7 +99,7 @@ import { withArticle } from './value.util.js';
|
|
|
99
99
|
walk(option.options ?? []);
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
102
|
-
params
|
|
102
|
+
setOwn(params, option.name, resolveOptionValue(option));
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
105
|
walk(data);
|
|
@@ -38,12 +38,20 @@ let problem;
|
|
|
38
38
|
const reason = (problem.error instanceof Error ? problem.error.message : String(problem.error)).replace(/\.$/, '');
|
|
39
39
|
const missing = missingPackage(problem.error);
|
|
40
40
|
const neededBy = missing?.importer ? `, which ${missing.importer} imports,` : '';
|
|
41
|
-
const fix = missing ? `Install ${missing.name}${neededBy} in the project
|
|
42
|
-
return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}
|
|
41
|
+
const fix = missing ? `Install ${missing.name}${neededBy} in the project, then run \`meocord build\`.` : fixedInConfig(problem.error) ? 'Fix meocord.config.ts, then run `meocord build`.' : 'It threw as it loaded: check the environment it reads, such as its .env files.';
|
|
42
|
+
return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}`;
|
|
43
43
|
}
|
|
44
44
|
const where = problem?.path ?? 'meocord.config.mjs';
|
|
45
45
|
return `MeoCord config not found at ${where} (working directory ${process.cwd()}). Run \`meocord build\`, and start the bot from the dist it writes.`;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the config's file or imports are at fault, which a rebuild after fixing it mends: it could not be read as a
|
|
49
|
+
* module, or imports a file that isn't there. Otherwise its code threw as it ran.
|
|
50
|
+
*/ function fixedInConfig(error) {
|
|
51
|
+
const code = error?.code;
|
|
52
|
+
if (error instanceof SyntaxError || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
|
|
53
|
+
return typeof code === 'string' && code.startsWith('ERR_REQUIRE_');
|
|
54
|
+
}
|
|
47
55
|
/**
|
|
48
56
|
* The package a module-not-found error names, when it is a package rather than a file: `dotenv` for `dotenv/config`,
|
|
49
57
|
* `@scope/name` for `@scope/name/sub`, with the installed package that imports it, if one does. Undefined for any other
|
|
@@ -46,6 +46,7 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
|
|
|
46
46
|
optionalExternals: optional(stringList('package names')),
|
|
47
47
|
rsbuild: optional(func),
|
|
48
48
|
sourceMappedStacks: optional(boolean),
|
|
49
|
+
startupErrors: optional(oneOf('first', 'all')),
|
|
49
50
|
shutdownTimeout: optional((value, key)=>{
|
|
50
51
|
const problem = shutdownTimeoutProblem(value);
|
|
51
52
|
return problem && `${key} ${problem}`;
|
|
@@ -65,14 +66,12 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
|
|
|
65
66
|
errors: [],
|
|
66
67
|
warnings: []
|
|
67
68
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (typeof loaded !== 'object' || loaded === null) {
|
|
71
|
-
problems.errors.push(`it must export an object as its default export (got ${describe(loaded)})`);
|
|
69
|
+
if (typeof config !== 'object' || config === null) {
|
|
70
|
+
problems.errors.push(`it must export an object as its default export (got ${describe(config)})`);
|
|
72
71
|
return problems;
|
|
73
72
|
}
|
|
74
73
|
const shape = configShape(problems);
|
|
75
|
-
for (const [name, value] of Object.entries(
|
|
74
|
+
for (const [name, value] of Object.entries(config)){
|
|
76
75
|
const check = shape[name];
|
|
77
76
|
if (!check) {
|
|
78
77
|
problems.warnings.push(`${name} is not a MeoCord option, so it has no effect.`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
3
|
import { createJiti } from 'jiti';
|
|
4
|
-
import {
|
|
4
|
+
import { comparablePath, framePath } from './source-path.util.js';
|
|
5
|
+
import { projectPaths } from './tsconfig-paths.util.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Loads `meocord.config.ts` from source on every call, so a build always uses the current config
|
|
@@ -25,31 +26,75 @@ import { parseJsonc } from './json.util.js';
|
|
|
25
26
|
config: undefined
|
|
26
27
|
};
|
|
27
28
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (existsSync(tsConfigPath)) {
|
|
31
|
-
const tsConfig = parseJsonc(readFileSync(tsConfigPath, 'utf-8'));
|
|
32
|
-
const paths = tsConfig?.compilerOptions?.paths;
|
|
33
|
-
if (paths) {
|
|
34
|
-
for (const [key, values] of Object.entries(paths)){
|
|
35
|
-
const aliasKey = key.replace('/*', '');
|
|
36
|
-
aliases[aliasKey] = path.resolve(process.cwd(), values[0].replace('/*', ''));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
29
|
+
const aliases = jitiAliases(projectPaths() ?? {});
|
|
30
|
+
// The module as written, so an ES module with no default export is told apart from a CommonJS one
|
|
40
31
|
const jiti = createJiti(import.meta.url, {
|
|
41
|
-
interopDefault:
|
|
32
|
+
interopDefault: false,
|
|
42
33
|
alias: aliases,
|
|
43
34
|
moduleCache: false
|
|
44
35
|
});
|
|
45
|
-
return
|
|
46
|
-
config: jiti(configPath)
|
|
47
|
-
};
|
|
36
|
+
return configOf(jiti(configPath));
|
|
48
37
|
} catch (error) {
|
|
49
38
|
return {
|
|
50
|
-
error: error
|
|
39
|
+
error: loadError(error, process.cwd())
|
|
51
40
|
};
|
|
52
41
|
}
|
|
53
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The config a loaded module gives: an ES module's default export, or what a CommonJS module assigns to
|
|
45
|
+
* `module.exports`. An ES module with no default export has none.
|
|
46
|
+
*/ function configOf(loaded) {
|
|
47
|
+
const module = loaded;
|
|
48
|
+
if (module?.__esModule !== true) return {
|
|
49
|
+
config: loaded
|
|
50
|
+
};
|
|
51
|
+
if ('default' in module) return {
|
|
52
|
+
config: module.default
|
|
53
|
+
};
|
|
54
|
+
// A module that exports nothing, as an empty file, is an empty config, whose values come from the environment
|
|
55
|
+
const names = Object.keys(module);
|
|
56
|
+
if (names.length === 0) return {
|
|
57
|
+
config: {}
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
error: `it must export an object as its default export, and exports only ${names.join(', ')}`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Why the config failed to load, with the first place in the project it failed at when the message does not name one,
|
|
65
|
+
* as for an error the config throws: jiti's message names the file, line and column of a syntax error.
|
|
66
|
+
*/ function loadError(error, root) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
const frame = error instanceof Error ? projectFrame(error.stack, root) : undefined;
|
|
69
|
+
return frame && !message.includes(frame) ? `${message}\n at ${frame}` : message;
|
|
70
|
+
}
|
|
71
|
+
// A stack frame's location, `file:line:column`, alone or in parentheses after the function: a path or a file:// URL
|
|
72
|
+
const FRAME_LOCATION = /\(?((?:file:\/\/)?(?:\/?[A-Za-z]:)?[\\/][^()]+?):(\d+):(\d+)\)?$/;
|
|
73
|
+
/**
|
|
74
|
+
* The first stack frame in a file of the project's own, as `file:line:column` from the project. On Windows a frame's
|
|
75
|
+
* file can be a file:// URL, or written with forward slashes or a lower-case drive, so each is compared as a path.
|
|
76
|
+
*/ function projectFrame(stack, root, windows = process.platform === 'win32') {
|
|
77
|
+
const paths = windows ? path.win32 : path.posix;
|
|
78
|
+
const base = comparablePath(paths.join(paths.resolve(root), paths.sep), windows);
|
|
79
|
+
for (const line of stack?.split('\n').slice(1) ?? []){
|
|
80
|
+
const frame = FRAME_LOCATION.exec(line.trim());
|
|
81
|
+
if (!frame) continue;
|
|
82
|
+
const [, location, row, column] = frame;
|
|
83
|
+
const file = paths.resolve(framePath(location, windows));
|
|
84
|
+
if (!comparablePath(file, windows).startsWith(base) || file.split(paths.sep).includes('node_modules')) continue;
|
|
85
|
+
return `${paths.relative(root, file).split(paths.sep).join('/')}:${row}:${column}`;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* jiti's aliases for tsconfig `paths`: each alias, and its first target, without a trailing `/*`, or `\*` in a Windows
|
|
91
|
+
* path, as the build reads the same paths.
|
|
92
|
+
*/ function jitiAliases(paths) {
|
|
93
|
+
const unstarred = (text)=>text.replace(/[\\/]\*$/, '');
|
|
94
|
+
return Object.fromEntries(Object.entries(paths).map(([alias, [target]])=>[
|
|
95
|
+
unstarred(alias),
|
|
96
|
+
unstarred(target)
|
|
97
|
+
]));
|
|
98
|
+
}
|
|
54
99
|
|
|
55
|
-
export { loadMeoCordSourceConfig, readMeoCordSourceConfig };
|
|
100
|
+
export { jitiAliases, loadMeoCordSourceConfig, projectFrame, readMeoCordSourceConfig };
|
|
@@ -24,6 +24,7 @@ import { MetadataKey } from '../enum/metadata-key.enum.js';
|
|
|
24
24
|
/** How many guard wrappers `@UseGuard` and `@Controller` put around one method. */ guardWrappers: 'meocord:guard-wrappers',
|
|
25
25
|
/** The prototype whose method a class-level `@UseGuard` re-declared on a subclass. */ inheritedFrom: 'meocord:inherited-from',
|
|
26
26
|
/** `false` when `@Controller({ inheritStages: false })` stops class stages at this class. */ inheritStages: 'meocord:inherit-stages',
|
|
27
|
+
/** `true` when `@Controller({ inheritedRoutes: 'replace' })` drops the routes this class's re-decorated methods inherit. */ replacesInheritedRoutes: 'meocord:replaces-inherited-routes',
|
|
27
28
|
/** A controller's class-level `@Cooldown`s. */ classCooldowns: 'meocord:class-cooldowns',
|
|
28
29
|
/** A method's `@Cooldown`s, in declaration order. */ methodCooldowns: 'meocord:method-cooldowns',
|
|
29
30
|
/** The error types a filter's `@Catch` names; empty to catch everything. */ catchTypes: 'meocord:catch-types',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gives a decorator's `wrapper` the `length` and `name` of the method it replaces, so whatever reads the handler, such
|
|
3
|
+
* as `invoke` counting the arguments it takes, sees the method as it was declared.
|
|
4
|
+
*/ function wrapsMethod(wrapper, original) {
|
|
5
|
+
Object.defineProperty(wrapper, 'length', {
|
|
6
|
+
value: original.length,
|
|
7
|
+
configurable: true
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(wrapper, 'name', {
|
|
10
|
+
value: original.name,
|
|
11
|
+
configurable: true
|
|
12
|
+
});
|
|
13
|
+
return wrapper;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { wrapsMethod };
|