meocord 4.2.2 → 4.2.3
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 +130 -0
- package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
- package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
- package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
- package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +432 -181
- package/dist/cjs/decorator/index.cjs +85 -120
- package/dist/cjs/testing/index.cjs +1664 -784
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +27 -5
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +17 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +83 -58
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +4 -4
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +44 -28
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +80 -53
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +5 -9
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +28 -3
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +27 -29
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +6 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +29 -13
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -355
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +195 -77
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +639 -113
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +1 -0
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +15 -4
- package/dist/types/common/index.d.cts +13 -9
- package/dist/types/common/index.d.ts +13 -9
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +137 -54
- package/dist/types/testing/index.d.ts +137 -54
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { sharedName } from '../util/shared-state.util.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order, under a key
|
|
5
|
+
* both builds of this meocord version read.
|
|
6
|
+
*/ const RESPONSE_LOG = Symbol.for(sharedName('responseLog'));
|
|
7
|
+
|
|
8
|
+
export { RESPONSE_LOG };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DiscordAPIError } from 'discord.js';
|
|
2
2
|
import { existingResponse } from '../common/response/response-state.js';
|
|
3
3
|
import { callOrder } from '../common/response/call-order.js';
|
|
4
|
-
import { RESPONSE_LOG } from './
|
|
4
|
+
import { RESPONSE_LOG } from './response-log.js';
|
|
5
5
|
|
|
6
6
|
const VISIBLE = new Set([
|
|
7
7
|
'reply',
|
|
@@ -14,9 +14,10 @@ const VISIBLE = new Set([
|
|
|
14
14
|
* Reports how an interaction was answered: where its answer stands, and every answer call it got, in order.
|
|
15
15
|
*
|
|
16
16
|
* Use it after `invoke` or `dispatch` to check what the member sees. A mock interaction records each `reply`,
|
|
17
|
-
* `deferReply`, `editReply`, `followUp`, `deleteReply`, `update`, `deferUpdate` and `showModal`,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* `deferReply`, `editReply`, `followUp`, `deleteReply`, `update`, `deferUpdate` and `showModal`, and under
|
|
18
|
+
* `useStrictMocks()` each `launchActivity`, `sendPremiumRequired` and autocomplete `respond` too, whether the handler made
|
|
19
|
+
* it through `respond()` or with discord.js directly, so both are reported, each once. For a message or a reaction, read
|
|
20
|
+
* the mock's own methods.
|
|
20
21
|
*
|
|
21
22
|
* @param interaction - The interaction a handler answered.
|
|
22
23
|
* @returns The state, whether anything the member can see was sent, and the calls, in order.
|
|
@@ -61,7 +62,8 @@ const VISIBLE = new Set([
|
|
|
61
62
|
sent,
|
|
62
63
|
calls: calls
|
|
63
64
|
};
|
|
64
|
-
|
|
65
|
+
// An autocomplete's choices are its answer, as observers read it, where its mock records them, under strict mocks
|
|
66
|
+
const answered = interaction.isRepliable() ? interaction.replied : Boolean(log) && interaction.isAutocomplete() && interaction.responded;
|
|
65
67
|
const deferred = interaction.isRepliable() ? interaction.deferred : false;
|
|
66
68
|
return {
|
|
67
69
|
state: answered ? 'replied' : deferred ? 'deferred' : 'unanswered',
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { findComponentRouteConflicts, buildComponentRoutes, matchComponentRoute, readCustomId } from '../core/component-routes.js';
|
|
2
2
|
import { isCustomIdRouted } from '../util/interaction.util.js';
|
|
3
|
-
import { staticMessageStarts,
|
|
4
|
-
import { needsGuild } from '../core/message-params.js';
|
|
3
|
+
import { staticMessageStarts, buildMessageRoutes, matchMessageRoute, fitsScope } from '../core/message-routes.js';
|
|
4
|
+
import { takesGuildParam, needsGuild, isGuildType } from '../core/message-params.js';
|
|
5
|
+
import { warnOnce } from '../common/deprecation.js';
|
|
6
|
+
import { Logger } from '../common/logger.js';
|
|
7
|
+
import { strictMocks } from './strict-mocks.js';
|
|
5
8
|
import { META } from '../util/metadata-keys.js';
|
|
9
|
+
import { describeValue } from '../util/value.util.js';
|
|
6
10
|
|
|
11
|
+
const logger = new Logger('resolveRoute');
|
|
12
|
+
/** Why an app import reads `undefined`, for its refusal. */ const CIRCULAR_IMPORT_HINT = ' An app import that reads undefined is often a circular import.';
|
|
13
|
+
/** Why an app is an object, for its refusal. */ const MODULE_HINT = ' An object here is often a module namespace, as `import * as App` gives: pass the class it exports.';
|
|
14
|
+
/** An app that isn't a class, as its refusal names it: the value, and its likely cause where there is one. */ function notAClass(app) {
|
|
15
|
+
const hint = app === undefined ? CIRCULAR_IMPORT_HINT : typeof app === 'object' && app !== null ? MODULE_HINT : '';
|
|
16
|
+
return {
|
|
17
|
+
name: describeValue(app),
|
|
18
|
+
hint
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Whether a message gives a route's `member`, `role` or `channel` flag, which dispatch refuses in a DM. */ const givesGuildFlag = (route, params)=>route.flags.some((flag)=>flag.type !== undefined && isGuildType(flag.type) && Object.hasOwn(params, flag.flag));
|
|
7
22
|
/** The options `@MeoCord` declares on an application class. */ function appOptionsOf(app) {
|
|
23
|
+
// An app that isn't a class at all, as a circular import gives, has no metadata to read
|
|
24
|
+
if (typeof app !== 'function') {
|
|
25
|
+
const { name, hint } = notAClass(app);
|
|
26
|
+
throw new TypeError(`${name} is not decorated with @MeoCord().${hint}`);
|
|
27
|
+
}
|
|
8
28
|
const options = Reflect.getMetadata(META.appOptions, app);
|
|
9
29
|
if (!options) throw new TypeError(`${app.name || 'The given class'} is not decorated with @MeoCord().`);
|
|
10
30
|
return options;
|
|
@@ -52,11 +72,33 @@ import { META } from '../util/metadata-keys.js';
|
|
|
52
72
|
if ('content' in input) {
|
|
53
73
|
const { messages = {} } = appOptionsOf(app);
|
|
54
74
|
const starts = staticMessageStarts(app, messages, input);
|
|
55
|
-
const
|
|
75
|
+
const routes = buildMessageRoutes(controllersOf(app), messages);
|
|
76
|
+
let matched = matchMessageRoute(routes, input.content, starts);
|
|
77
|
+
const named = (route)=>`${route.controllerClass.name}.${String(route.method)}`;
|
|
78
|
+
// Without `dm`: under strict mocks, read as sent in a server, as dispatch routes such a message whatever the
|
|
79
|
+
// controllers' order; otherwise as from anywhere, warning where that answers differently
|
|
80
|
+
if (starts.inGuild === undefined) {
|
|
81
|
+
const inServer = matchMessageRoute(routes, input.content, {
|
|
82
|
+
...starts,
|
|
83
|
+
inGuild: true
|
|
84
|
+
});
|
|
85
|
+
if (strictMocks()) matched = inServer && fitsScope(inServer.route.scope, true) ? inServer : undefined;
|
|
86
|
+
else if (matched && inServer && matched.route !== inServer.route && !fitsScope(matched.route.scope, true)) {
|
|
87
|
+
warnOnce(logger, `resolveRoute resolved a message without dm to ${named(matched.route)}, which works in a DM alone, where dispatch runs ` + `${named(inServer.route)} for a message sent in a server. Pass dm: true for a DM. Under useStrictMocks() it resolves to ` + `${named(inServer.route)}.`);
|
|
88
|
+
} else if (matched && !fitsScope(matched.route.scope, true)) {
|
|
89
|
+
// Only a handler that works in a DM alone takes it, which dispatch runs in a DM only
|
|
90
|
+
warnOnce(logger, `resolveRoute found only ${named(matched.route)}, which works in a DM alone, for a message without dm; dispatch runs it ` + 'only in a DM. Pass dm: true. Under useStrictMocks() this resolves to undefined.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
56
93
|
if (!matched) return undefined;
|
|
57
94
|
const { route, params } = matched;
|
|
58
|
-
// Dispatch
|
|
59
|
-
if (starts.inGuild === false && (!fitsScope(route.scope, false) ||
|
|
95
|
+
// Dispatch never runs a route that does not work in a DM; one sent with a prefix or mention is told it works in a server only
|
|
96
|
+
if (starts.inGuild === false && (!fitsScope(route.scope, false) || takesGuildParam(route))) return undefined;
|
|
97
|
+
// Nor one with a required flag only a server has, or given one: refused under strict mocks, warned about otherwise
|
|
98
|
+
if (starts.inGuild === false && (needsGuild(route) || givesGuildFlag(route, params))) {
|
|
99
|
+
if (strictMocks()) return undefined;
|
|
100
|
+
warnOnce(logger, `resolveRoute resolved a DM to ${named(route)}, which dispatch answers "This command ` + 'works in a server only.": a member, role or channel flag is found only in a server. Under useStrictMocks() this ' + 'resolves to undefined.');
|
|
101
|
+
}
|
|
60
102
|
return {
|
|
61
103
|
controller: route.controllerClass,
|
|
62
104
|
method: route.method,
|
|
@@ -140,4 +182,4 @@ import { META } from '../util/metadata-keys.js';
|
|
|
140
182
|
return target;
|
|
141
183
|
}
|
|
142
184
|
|
|
143
|
-
export { findRouteConflicts, resolveRoute };
|
|
185
|
+
export { findRouteConflicts, notAClass, resolveRoute };
|
|
@@ -41,6 +41,7 @@ const settings = shared('strictMocks', ()=>({
|
|
|
41
41
|
*
|
|
42
42
|
* `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
|
|
43
43
|
* reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
|
|
44
|
+
* It refuses an autocomplete that dispatch gives to another `@Autocomplete` handler, or to none, the same way.
|
|
44
45
|
*
|
|
45
46
|
* `compile()` also refuses an `overrideGuard`, `overrideInterceptor` or `overrideFilter` stub without the stage's
|
|
46
47
|
* method, `canActivate`, `intercept` or `catch`; without strict mocks, it warns and builds the module.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { assertValidTheme } from '../core/theme-validation.js';
|
|
2
2
|
import { defaultTheme, runWithTheme, mergeTheme, copyLayer } from '../core/theme-scope.js';
|
|
3
|
+
import { shared } from '../util/shared-state.util.js';
|
|
3
4
|
|
|
4
|
-
// Themes createMockTheme made, which withTheme uses as they are
|
|
5
|
-
const mocks = new WeakSet();
|
|
5
|
+
// Themes createMockTheme made through either build of this version, which withTheme uses as they are
|
|
6
|
+
const mocks = shared('mockThemes', ()=>new WeakSet());
|
|
6
7
|
function mockTheme(overrides, where) {
|
|
7
8
|
if (overrides === undefined) return defaultTheme();
|
|
8
9
|
assertValidTheme(overrides, where);
|
|
@@ -23,21 +23,29 @@ import chalk from 'chalk';
|
|
|
23
23
|
* Loads `meocord.config.ts` and checks its shape, as a command that builds and a watch-mode reload both do, printing
|
|
24
24
|
* the warnings for options it does not know.
|
|
25
25
|
*
|
|
26
|
+
* @param options.building - Whether the command builds, which refuses a `webpack` hook; one that runs without building
|
|
27
|
+
* only warns about it.
|
|
26
28
|
* @returns The config, or the report of why it cannot be used: missing, failing to load, or with options of the wrong
|
|
27
29
|
* type, each of them listed. A config that fails to load also gives the reason alone, as `loadError`.
|
|
28
|
-
*/ function checkSourceConfig() {
|
|
30
|
+
*/ function checkSourceConfig({ building = true } = {}) {
|
|
29
31
|
const meocordConfigPath = path.resolve(process.cwd(), 'meocord.config.ts');
|
|
30
32
|
if (!fs.existsSync(meocordConfigPath)) return {
|
|
31
33
|
problem: 'Configuration file "meocord.config.ts" is missing!'
|
|
32
34
|
};
|
|
33
35
|
const loaded = readMeoCordSourceConfig();
|
|
36
|
+
// Named as itself; with no loadError, watch mode reports it as a problem, not as the config failing to compile
|
|
37
|
+
if ('error' in loaded && loaded.file === 'tsconfig.json') {
|
|
38
|
+
return {
|
|
39
|
+
problem: `tsconfig.json could not be read, so nothing was built or started:\n ${loaded.error}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
34
42
|
if ('error' in loaded) {
|
|
35
43
|
return {
|
|
36
44
|
problem: `meocord.config.ts could not be loaded, so nothing was built or started:\n ${loaded.error}`,
|
|
37
45
|
loadError: loaded.error
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
|
-
const problem = shapeProblem(loaded.config);
|
|
48
|
+
const problem = shapeProblem(loaded.config, building);
|
|
41
49
|
return problem ? {
|
|
42
50
|
problem
|
|
43
51
|
} : {
|
|
@@ -51,9 +59,12 @@ import chalk from 'chalk';
|
|
|
51
59
|
* Loads `meocord.config.ts` and checks its shape, exiting when it is missing, fails to load, or has
|
|
52
60
|
* options of the wrong type; options it does not know are reported and left alone.
|
|
53
61
|
*
|
|
62
|
+
* @param options.building - Whether the command builds, as {@link checkSourceConfig} takes it.
|
|
54
63
|
* @returns The config, which a build compiles and the bot then runs with.
|
|
55
|
-
*/ async function compileAndValidateConfig() {
|
|
56
|
-
const checked = checkSourceConfig(
|
|
64
|
+
*/ async function compileAndValidateConfig({ building = true } = {}) {
|
|
65
|
+
const checked = checkSourceConfig({
|
|
66
|
+
building
|
|
67
|
+
});
|
|
57
68
|
if ('problem' in checked) {
|
|
58
69
|
reportConfigProblem(checked.problem);
|
|
59
70
|
await wait(100);
|
|
@@ -80,17 +91,27 @@ import chalk from 'chalk';
|
|
|
80
91
|
process.exit(1);
|
|
81
92
|
return;
|
|
82
93
|
}
|
|
83
|
-
return compileAndValidateConfig(
|
|
94
|
+
return compileAndValidateConfig({
|
|
95
|
+
building: false
|
|
96
|
+
});
|
|
84
97
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Exits with every problem when a configuration a command runs with, without building, has options of the wrong type,
|
|
100
|
+
* and warns about unknown ones.
|
|
101
|
+
*/ async function assertConfigShape(config) {
|
|
102
|
+
const problem = shapeProblem(config, false);
|
|
87
103
|
if (!problem) return;
|
|
88
104
|
console.error(chalk.red(problem));
|
|
89
105
|
await wait(100);
|
|
90
106
|
process.exit(1);
|
|
91
107
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Every option of the wrong type in a configuration, as one report, or undefined; options it does not know are warned
|
|
110
|
+
* about. A `webpack` hook is refused only for a command that builds, as the build refuses it.
|
|
111
|
+
*/ function shapeProblem(config, building = true) {
|
|
112
|
+
const { errors, warnings } = configProblems(config, {
|
|
113
|
+
building
|
|
114
|
+
});
|
|
94
115
|
for (const warning of warnings)console.warn(chalk.yellow(`meocord.config.ts: ${warning}`));
|
|
95
116
|
if (errors.length === 0) return undefined;
|
|
96
117
|
return `meocord.config.ts has ${errors.length} problem(s):\n${errors.map((error)=>` - ${error}`).join('\n')}`;
|
|
@@ -2,20 +2,22 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
4
|
import { Logger } from '../common/logger.js';
|
|
5
|
-
import '../
|
|
6
|
-
import '../core/theme-validation.js';
|
|
5
|
+
import '../common/theme.js';
|
|
7
6
|
import '../common/decorator.js';
|
|
8
7
|
import 'reflect-metadata';
|
|
9
8
|
import 'discord.js';
|
|
9
|
+
import '../common/deprecation.js';
|
|
10
10
|
import '../common/response/response-state.js';
|
|
11
|
+
import '../core/theme-scope.js';
|
|
11
12
|
import '../common/errors.js';
|
|
13
|
+
import '../common/cooldown-store.js';
|
|
12
14
|
import 'node:crypto';
|
|
13
15
|
import '../decorator/controller.decorator.js';
|
|
14
16
|
import '../core/scalar-types.js';
|
|
15
|
-
import '
|
|
16
|
-
import 'node:url';
|
|
17
|
+
import './refusal.util.js';
|
|
17
18
|
import '../common/translator.js';
|
|
18
19
|
import '../common/meocord-text.js';
|
|
20
|
+
import '../core/theme-resolvers.js';
|
|
19
21
|
import { kebabCase, startCase, camelCase } from 'lodash-es';
|
|
20
22
|
import { fileURLToPath } from 'url';
|
|
21
23
|
|
|
@@ -34,16 +34,11 @@ import { listed } from './user-text.util.js';
|
|
|
34
34
|
const productionFiles = envFiles('production');
|
|
35
35
|
// In Bun's order, a later file winning, as the config's dotenv also settles them
|
|
36
36
|
const production = Object.assign({}, ...productionFiles.map((file)=>read(root, file)));
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
for (const [key, value] of Object.entries(read(root, file)))development.set(key, {
|
|
40
|
-
value,
|
|
41
|
-
file
|
|
42
|
-
});
|
|
43
|
-
}
|
|
37
|
+
const files = envFiles(env.NODE_ENV);
|
|
38
|
+
// Listed file by file, in Bun's order
|
|
44
39
|
const loaded = [
|
|
45
|
-
...
|
|
46
|
-
].filter(([key, { value }])
|
|
40
|
+
...bunLoadedValues(env, root, files)
|
|
41
|
+
].filter(([key, { value, file }])=>!productionFiles.includes(file) && env[key] === value && production[key] !== value).sort(([, a], [, b])=>files.indexOf(a.file) - files.indexOf(b.file));
|
|
47
42
|
if (loaded.length === 0) return none;
|
|
48
43
|
return {
|
|
49
44
|
files: [
|
|
@@ -61,13 +56,77 @@ import { listed } from './user-text.util.js';
|
|
|
61
56
|
* The environment a bot started in `root` inherits: `env` without the values its .env files gave it. Neither dotenv nor
|
|
62
57
|
* Bun replaces a variable already set, so an inherited copy would keep a value a file no longer holds; the bot reads the
|
|
63
58
|
* files itself as it starts. A variable the shell sets to another value is kept.
|
|
64
|
-
*/ function inheritedEnvironment(env, root) {
|
|
59
|
+
*/ function inheritedEnvironment(env, root, bun = process.versions.bun !== undefined) {
|
|
60
|
+
const files = envFiles(env.NODE_ENV);
|
|
65
61
|
const fromFiles = new Map();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
const add = (key, value)=>fromFiles.set(key, (fromFiles.get(key) ?? new Set()).add(value));
|
|
63
|
+
// As dotenv sets them, and, when the CLI runs on Bun, as Bun set them before any code ran
|
|
64
|
+
for (const file of files)for (const [key, value] of Object.entries(read(root, file)))add(key, value);
|
|
65
|
+
if (bun) for (const [key, { value }] of bunLoadedValues(env, root, files))add(key, value);
|
|
69
66
|
return Object.fromEntries(Object.entries(env).filter(([key, value])=>value === undefined || !fromFiles.get(key)?.has(value)));
|
|
70
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The value Bun gives each variable the .env files set, and the file it comes from: Bun loads them before any code
|
|
70
|
+
* runs, the file that wins in `files` first, sets only a variable not already set, and expands `$NAME`, `${NAME}` and
|
|
71
|
+
* `${NAME:-default}` line by line from what is set so far, a later line of the same file giving its text as written.
|
|
72
|
+
* A variable the shell set keeps the shell's value, which `env` holds, so it differs from the one given here.
|
|
73
|
+
*/ function bunLoadedValues(env, root, files) {
|
|
74
|
+
// Each variable's first line in Bun's order: the one Bun loads it from
|
|
75
|
+
const lines = [];
|
|
76
|
+
const first = new Map();
|
|
77
|
+
for (const file of [
|
|
78
|
+
...files
|
|
79
|
+
].reverse()){
|
|
80
|
+
const own = read(root, file);
|
|
81
|
+
for (const [key, text] of Object.entries(own)){
|
|
82
|
+
if (!first.has(key)) first.set(key, lines.length);
|
|
83
|
+
lines.push({
|
|
84
|
+
key,
|
|
85
|
+
text,
|
|
86
|
+
file,
|
|
87
|
+
own
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const values = new Map();
|
|
92
|
+
const fromShell = new Map();
|
|
93
|
+
// A variable the shell set holds a value its line doesn't give; one its own line refers to back counts as from it
|
|
94
|
+
const shellSet = (key)=>{
|
|
95
|
+
if (!fromShell.has(key)) {
|
|
96
|
+
fromShell.set(key, false);
|
|
97
|
+
fromShell.set(key, env[key] !== undefined && env[key] !== valueAt(first.get(key)));
|
|
98
|
+
}
|
|
99
|
+
return fromShell.get(key);
|
|
100
|
+
};
|
|
101
|
+
const valueAt = (index)=>{
|
|
102
|
+
if (!values.has(index)) {
|
|
103
|
+
const { text, file, own } = lines[index];
|
|
104
|
+
const lookup = (name)=>{
|
|
105
|
+
const at = first.get(name);
|
|
106
|
+
if (at === undefined || shellSet(name) || at < index) return env[name] ?? '';
|
|
107
|
+
// Not loaded yet: its text as written in this file, or unset until a later file loads it
|
|
108
|
+
return lines[at].file === file ? own[name] : '';
|
|
109
|
+
};
|
|
110
|
+
values.set(index, expandAsBun(text, lookup));
|
|
111
|
+
}
|
|
112
|
+
return values.get(index);
|
|
113
|
+
};
|
|
114
|
+
return new Map([
|
|
115
|
+
...first
|
|
116
|
+
].map(([key, index])=>[
|
|
117
|
+
key,
|
|
118
|
+
{
|
|
119
|
+
value: valueAt(index),
|
|
120
|
+
file: lines[index].file
|
|
121
|
+
}
|
|
122
|
+
]));
|
|
123
|
+
}
|
|
124
|
+
/** A `$NAME` or `${NAME}` reference, with `${NAME:-default}`'s default, or a `\$` Bun reads as a dollar sign. */ const BUN_REFERENCE = /\\\$|\$\{([A-Za-z0-9_]+)(?::-([^}]*))?\}|\$([A-Za-z0-9_]+)/g;
|
|
125
|
+
/** A .env value as Bun expands it, each reference read through `lookup`; any other `$` stays as it is. */ const expandAsBun = (text, lookup)=>text.replace(BUN_REFERENCE, (match, braced, fallback, bare)=>{
|
|
126
|
+
if (match === '\\$') return '$';
|
|
127
|
+
const value = lookup(braced ?? bare);
|
|
128
|
+
return fallback !== undefined && value === '' ? fallback : value;
|
|
129
|
+
});
|
|
71
130
|
/** A .env file's values, or none when it is not there to read. */ function read(root, file) {
|
|
72
131
|
let text;
|
|
73
132
|
try {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/** The byte order mark an editor may write at the start of a UTF-8 file, which `JSON.parse` refuses. */ const BYTE_ORDER_MARK = '\uFEFF';
|
|
4
|
+
/** The text without a leading byte order mark, as Node and tsc read a JSON file. */ const withoutByteOrderMark = (text)=>text.startsWith(BYTE_ORDER_MARK) ? text.slice(BYTE_ORDER_MARK.length) : text;
|
|
1
5
|
/**
|
|
2
6
|
* Removes the comments and trailing commas `tsconfig.json` allows, leaving strings untouched, so an
|
|
3
7
|
* `https://` URL or a `/*` glob inside one survives.
|
|
@@ -37,8 +41,11 @@
|
|
|
37
41
|
}
|
|
38
42
|
return result;
|
|
39
43
|
}
|
|
40
|
-
/** Parses JSON with the comments and trailing commas `tsconfig.json` allows. */ function parseJsonc(text) {
|
|
41
|
-
return JSON.parse(stripJsonc(text));
|
|
44
|
+
/** Parses JSON with the comments and trailing commas `tsconfig.json` allows, and a leading byte order mark. */ function parseJsonc(text) {
|
|
45
|
+
return JSON.parse(stripJsonc(withoutByteOrderMark(text)));
|
|
46
|
+
}
|
|
47
|
+
/** Reads a JSON file, such as a `package.json`, as Node reads one: UTF-8, with a leading byte order mark ignored. */ function readJsonFile(file) {
|
|
48
|
+
return JSON.parse(withoutByteOrderMark(readFileSync(file, 'utf8')));
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
export { parseJsonc, stripJsonc };
|
|
51
|
+
export { parseJsonc, readJsonFile, stripJsonc };
|
|
@@ -3,6 +3,7 @@ import { findModulePackageDir } from './common.util.js';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs__default from 'node:fs';
|
|
5
5
|
import wait from './wait.util.js';
|
|
6
|
+
import { readJsonFile } from './json.util.js';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
|
|
8
9
|
function configureCommandHelp(command) {
|
|
@@ -120,10 +121,10 @@ function configureCommandHelp(command) {
|
|
|
120
121
|
throw new Error('Cannot locate the "MeoCord" package directory.');
|
|
121
122
|
}
|
|
122
123
|
// Read and parse the root package.json
|
|
123
|
-
const { dependencies } =
|
|
124
|
+
const { dependencies } = readJsonFile(packageJsonPath);
|
|
124
125
|
// Read and parse the MeoCord package.json
|
|
125
126
|
const internalPackageJsonPath = path.join(meocordPath, 'package.json');
|
|
126
|
-
const { name: internalPackageName } =
|
|
127
|
+
const { name: internalPackageName } = readJsonFile(internalPackageJsonPath);
|
|
127
128
|
// Validate that MeoCord is listed as a dependency in the root package.json
|
|
128
129
|
if (!dependencies?.[internalPackageName]) {
|
|
129
130
|
throw new Error('The package.json does not list "MeoCord" as a dependency. Ensure you are in the root directory.');
|
|
@@ -49,9 +49,22 @@ let problem;
|
|
|
49
49
|
* module, or imports a file that isn't there. Otherwise its code threw as it ran.
|
|
50
50
|
*/ function fixedInConfig(error) {
|
|
51
51
|
const code = error?.code;
|
|
52
|
-
if (error
|
|
52
|
+
if (unparsable(error) || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
|
|
53
53
|
return typeof code === 'string' && code.startsWith('ERR_REQUIRE_');
|
|
54
54
|
}
|
|
55
|
+
/** The name of Bun's error for a module it could not parse; several of them come together in an AggregateError. */ const BUN_PARSE_ERROR = 'BuildMessage';
|
|
56
|
+
/**
|
|
57
|
+
* Whether a module could not be parsed, so none of its code ran: on Node, a SyntaxError thrown from compiling it, whose
|
|
58
|
+
* first frame is Node's own; on Bun, its parse error. A SyntaxError the config's code throws as it runs, from
|
|
59
|
+
* `JSON.parse` or `new RegExp` on an environment value, is not one.
|
|
60
|
+
*/ function unparsable(error) {
|
|
61
|
+
const bunParseError = (each)=>each?.name === BUN_PARSE_ERROR;
|
|
62
|
+
if (bunParseError(error)) return true;
|
|
63
|
+
if (error instanceof AggregateError) return error.errors.length > 0 && error.errors.every(bunParseError);
|
|
64
|
+
if (!(error instanceof SyntaxError)) return false;
|
|
65
|
+
const firstFrame = error.stack?.split('\n').find((line)=>line.trimStart().startsWith('at '));
|
|
66
|
+
return firstFrame === undefined || /[( ]node:/.test(firstFrame);
|
|
67
|
+
}
|
|
55
68
|
/**
|
|
56
69
|
* The package a module-not-found error names, when it is a package rather than a file: `dotenv` for `dotenv/config`,
|
|
57
70
|
* `@scope/name` for `@scope/name/sub`, with the installed package that imports it, if one does. Undefined for any other
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { shutdownTimeoutProblem } from './shutdown-timeout.util.js';
|
|
2
|
+
import { quoteForLog } from './user-text.util.js';
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
+
/** Why a config that still declares a `webpack` hook, which MeoCord does not run, is refused. */ const WEBPACK_HOOK_REFUSAL = 'meocord.config.ts still declares a `webpack` hook, which MeoCord no longer runs. ' + 'Rename it to `rsbuild` and reshape its body for Rsbuild: images, fonts, svg and media ' + 'need no rules any more, `output.filename` accepts a function for custom asset paths, ' + 'and raw rules go through `tools.rspack`. See the 4.0.0 release notes.';
|
|
5
|
+
// A string as other messages quote user text, so a line break in it can't start a line of its own
|
|
6
|
+
const describe = (value)=>typeof value === 'string' ? quoteForLog(value) : typeof value === 'number' || typeof value === 'boolean' ? String(value) : Array.isArray(value) ? 'an array' : value === null ? 'null' : typeof value;
|
|
4
7
|
const optional = (check)=>(value, key)=>value === undefined ? undefined : check(value, key);
|
|
5
8
|
const string = (value, key)=>typeof value === 'string' ? undefined : `${key} must be a string (got ${describe(value)})`;
|
|
6
9
|
const boolean = (value, key)=>typeof value === 'boolean' ? undefined : `${key} must be true or false (got ${describe(value)})`;
|
|
@@ -60,8 +63,10 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
|
|
|
60
63
|
* Checks a loaded configuration's shape, so a wrong type fails at once with what to fix instead of
|
|
61
64
|
* surfacing later as an odd build or runtime error.
|
|
62
65
|
*
|
|
66
|
+
* @param options.building - Whether the command builds, which refuses a `webpack` hook; one that only runs a build
|
|
67
|
+
* made before is warned that the hook has no effect.
|
|
63
68
|
* @returns Every error and warning found; the command should stop when there are errors.
|
|
64
|
-
*/ function configProblems(config) {
|
|
69
|
+
*/ function configProblems(config, { building = true } = {}) {
|
|
65
70
|
const problems = {
|
|
66
71
|
errors: [],
|
|
67
72
|
warnings: []
|
|
@@ -72,6 +77,11 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
|
|
|
72
77
|
}
|
|
73
78
|
const shape = configShape(problems);
|
|
74
79
|
for (const [name, value] of Object.entries(config)){
|
|
80
|
+
// A webpack hook: a build refuses it, so there it's that error, not an option with no effect
|
|
81
|
+
if (name === 'webpack' && building) {
|
|
82
|
+
problems.errors.push(WEBPACK_HOOK_REFUSAL);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
75
85
|
const check = shape[name];
|
|
76
86
|
if (!check) {
|
|
77
87
|
problems.warnings.push(`${name} is not a MeoCord option, so it has no effect.`);
|
|
@@ -83,4 +93,4 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
|
|
|
83
93
|
return problems;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
export { configProblems };
|
|
96
|
+
export { WEBPACK_HOOK_REFUSAL, configProblems };
|
|
@@ -10,7 +10,7 @@ import { projectPaths } from './tsconfig-paths.util.js';
|
|
|
10
10
|
*/ function loadMeoCordSourceConfig() {
|
|
11
11
|
const result = readMeoCordSourceConfig();
|
|
12
12
|
if ('error' in result) {
|
|
13
|
-
console.error(`[MeoCord] Failed to load config: ${result.error}`);
|
|
13
|
+
console.error(result.file === 'tsconfig.json' ? `[MeoCord] Failed to read tsconfig.json: ${result.error}` : `[MeoCord] Failed to load config: ${result.error}`);
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
16
16
|
return result.config;
|
|
@@ -19,14 +19,24 @@ import { projectPaths } from './tsconfig-paths.util.js';
|
|
|
19
19
|
* Loads `meocord.config.ts` from source, reporting a failure instead of logging it, for the CLI to stop
|
|
20
20
|
* on. jiti's message names the file, line and column of a syntax error.
|
|
21
21
|
*
|
|
22
|
-
* @returns The config (undefined when the file does not exist), or the reason it could not be loaded
|
|
22
|
+
* @returns The config (undefined when the file does not exist), or the reason it could not be loaded, naming
|
|
23
|
+
* `tsconfig.json` as the file at fault when it was that file, whose `paths` the config is loaded with.
|
|
23
24
|
*/ function readMeoCordSourceConfig() {
|
|
24
25
|
const configPath = path.resolve(process.cwd(), 'meocord.config.ts');
|
|
25
26
|
if (!existsSync(configPath)) return {
|
|
26
27
|
config: undefined
|
|
27
28
|
};
|
|
29
|
+
// Read first, so a tsconfig.json that can't be read is reported as its own, not as the config failing to load
|
|
30
|
+
let aliases;
|
|
31
|
+
try {
|
|
32
|
+
aliases = jitiAliases(projectPaths() ?? {});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
error: error instanceof Error ? error.message : String(error),
|
|
36
|
+
file: 'tsconfig.json'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
28
39
|
try {
|
|
29
|
-
const aliases = jitiAliases(projectPaths() ?? {});
|
|
30
40
|
// The module as written, so an ES module with no default export is told apart from a CommonJS one
|
|
31
41
|
const jiti = createJiti(import.meta.url, {
|
|
32
42
|
interopDefault: false,
|
|
@@ -5,8 +5,9 @@ import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* File written beside a bundle that carries native addons, naming the platform it was built for.
|
|
7
7
|
*/ const PLATFORM_MANIFEST = 'meocord.platform.json';
|
|
8
|
+
/** The one operating system whose binaries are built against a C library of its choosing, glibc or musl. */ const LIBC_PLATFORM = 'linux';
|
|
8
9
|
/** The C library this process runs against, when the runtime can say. */ function detectLibc() {
|
|
9
|
-
if (process.platform !==
|
|
10
|
+
if (process.platform !== LIBC_PLATFORM) return undefined;
|
|
10
11
|
try {
|
|
11
12
|
const report = process.report?.getReport?.();
|
|
12
13
|
if (!report?.header) return undefined;
|
|
@@ -66,4 +67,4 @@ import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
|
|
|
66
67
|
return process.argv[1] ? path.dirname(process.argv[1]) : undefined;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
export { PLATFORM_MANIFEST, currentPlatform, describePlatform, isSamePlatform, platformMismatch, writePlatformManifest };
|
|
70
|
+
export { LIBC_PLATFORM, PLATFORM_MANIFEST, currentPlatform, describePlatform, isSamePlatform, platformMismatch, writePlatformManifest };
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
2
|
+
import { warnOnce, warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
3
3
|
import { isExplainedError } from '../common/explained-error.js';
|
|
4
4
|
import { Logger } from '../common/logger.js';
|
|
5
5
|
import { isBuiltApplication, STARTUP_ERRORS_KEY } from './bundle-entry.util.js';
|
|
6
6
|
import { comparablePath, framePath } from './source-path.util.js';
|
|
7
|
+
import { shared } from './shared-state.util.js';
|
|
7
8
|
|
|
8
9
|
const REFUSAL = Symbol.for('meocord.refusal');
|
|
9
|
-
/**
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* One per process for this meocord version, so the two builds report a refusal once: whether this process reports
|
|
12
|
+
* refusals yet (see `reportRefusals`), and the refusals someone else reports, and ends the process for, which it leaves
|
|
13
|
+
* to them.
|
|
14
|
+
*/ const refusals = shared('refusalReporting', ()=>({
|
|
15
|
+
reporting: false,
|
|
16
|
+
handedOff: new WeakSet()
|
|
17
|
+
}));
|
|
11
18
|
/** Leaves a refusal to whoever reports it instead, such as a shard's manager: going uncaught, it is neither logged nor exits. */ function handOffRefusal(error) {
|
|
12
|
-
handedOff.add(error);
|
|
19
|
+
refusals.handedOff.add(error);
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Marks an error MeoCord raises for code it refuses as the application loads, such as a decorator given what it
|
|
@@ -20,18 +27,29 @@ const REFUSAL = Symbol.for('meocord.refusal');
|
|
|
20
27
|
enumerable: false
|
|
21
28
|
});
|
|
22
29
|
// Before the error is thrown, so a built application reports it however the throw ends
|
|
23
|
-
if (!reporting && isBuiltApplication()) reportRefusals();
|
|
30
|
+
if (!refusals.reporting && isBuiltApplication()) reportRefusals();
|
|
24
31
|
return error;
|
|
25
32
|
}
|
|
26
|
-
/**
|
|
33
|
+
/** The name of the class a member decorator is applied in: a static member's target is the class itself. */ function ownerName(target) {
|
|
34
|
+
return (typeof target === 'function' ? target : target.constructor).name;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* What a decorator is applied to, as a refusal names it: `Class.method` on a method, a static one included, `Class` on
|
|
38
|
+
* a class.
|
|
39
|
+
*/ function decoratedName(target, propertyKey) {
|
|
27
40
|
if (propertyKey === undefined) return target.name || 'a class';
|
|
28
|
-
return `${target
|
|
41
|
+
return `${ownerName(target)}.${String(propertyKey)}`;
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
44
|
* Refuses a decorator that goes only on a method, or on a member, where it is applied to a class, directly or through
|
|
32
|
-
* `applyDecorators`, rather than store what nothing reads or fail on the method it lacks.
|
|
45
|
+
* `applyDecorators`, rather than store what nothing reads or fail on the method it lacks. On a static member, where
|
|
46
|
+
* nothing reads it either, it warns; the next major version (5.0) refuses it there too.
|
|
33
47
|
*/ function refuseOnClass(decorator, target, propertyKey, place = 'a method') {
|
|
34
48
|
if (propertyKey === undefined) throw refuse(new Error(`${decoratedName(target)}: ${decorator} goes on ${place}, not on a class.`));
|
|
49
|
+
if (typeof target === 'function') {
|
|
50
|
+
const where = place === 'a method' ? 'an instance method, not a static one; it never runs' : 'an instance member, not a static one, where nothing reads it';
|
|
51
|
+
warnOnce(new Logger(decorator.slice(1)), `${decoratedName(target, propertyKey)}: ${decorator} goes on ${where}. The next major version (5.0) refuses it.`);
|
|
52
|
+
}
|
|
35
53
|
}
|
|
36
54
|
/** Refuses a decorator that goes only on a class where it is applied to a method, directly or through `applyDecorators`. */ function refuseOnMethod(decorator, target, propertyKey) {
|
|
37
55
|
if (propertyKey !== undefined) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${decorator} goes on a class, not on a method.`));
|
|
@@ -214,9 +232,9 @@ function keepOrThrow(error, target, propertyKey) {
|
|
|
214
232
|
* report. Installed by the first refusal in a built application. Bun has no monitor event for a rejection, so there a
|
|
215
233
|
* listener reports a refused rejection and rejects any other again without itself, unless the app listens itself.
|
|
216
234
|
*/ function reportRefusals(log = (text)=>new Logger('MeoCord').error(text), exit = (code)=>process.exit(code), { bun = process.versions.bun !== undefined, reject = (reason)=>void Promise.reject(reason) } = {}) {
|
|
217
|
-
reporting = true;
|
|
235
|
+
refusals.reporting = true;
|
|
218
236
|
const report = (error)=>{
|
|
219
|
-
if (handedOff.has(error)) return;
|
|
237
|
+
if (refusals.handedOff.has(error)) return;
|
|
220
238
|
if (!isExplainedError(error)) log(describeRefusal(error, process.cwd()));
|
|
221
239
|
exit(1);
|
|
222
240
|
};
|
|
@@ -235,4 +253,4 @@ function keepOrThrow(error, target, propertyKey) {
|
|
|
235
253
|
process.on('unhandledRejection', onRejection);
|
|
236
254
|
}
|
|
237
255
|
|
|
238
|
-
export { allDeclaredErrors, collectStartupErrors, declarationOf, declaredErrorsOf, declaring, decoratedName, deprecatedOnMethod, describeRefusal, handOffRefusal, isRefusal, keepsDeclaredErrors, logStartupErrors, refuse, refuseOnClass, refuseOnMethod, reportRefusals, setStartupErrorsMode, sourceFileOf, startupError, startupErrorsOf, stopOnStartupErrors };
|
|
256
|
+
export { allDeclaredErrors, collectStartupErrors, declarationOf, declaredErrorsOf, declaring, decoratedName, deprecatedOnMethod, describeRefusal, handOffRefusal, isRefusal, keepsDeclaredErrors, logStartupErrors, ownerName, refuse, refuseOnClass, refuseOnMethod, reportRefusals, setStartupErrorsMode, sourceFileOf, startupError, startupErrorsOf, stopOnStartupErrors };
|
|
@@ -4,8 +4,21 @@
|
|
|
4
4
|
*/ /** Set by `meocord register`: register the commands over REST and exit, without logging in. */ const REGISTER_ONLY_ENV = 'MEOCORD_REGISTER_ONLY';
|
|
5
5
|
/** Set by `meocord register --guild <id>`: register every command to that guild only. */ const REGISTER_GUILD_ENV = 'MEOCORD_REGISTER_GUILD';
|
|
6
6
|
/** Set by `meocord start --force-register` and by `meocord register`: send even an unchanged payload. */ const FORCE_REGISTER_ENV = 'MEOCORD_FORCE_REGISTER';
|
|
7
|
+
/**
|
|
8
|
+
* Set by the CLI for every bot it starts: `development` or `production`, the mode the command runs in. A bundle built
|
|
9
|
+
* with `bundleDependencies` has `process.env.NODE_ENV` written in as the mode it was built in, MeoCord's own reads
|
|
10
|
+
* included, so the command's mode reaches it through this variable instead.
|
|
11
|
+
*/ const REGISTER_MODE_ENV = 'MEOCORD_REGISTER_MODE';
|
|
12
|
+
/**
|
|
13
|
+
* Whether commands register as development does, to `commands.developmentGuild`: as the CLI command that started the
|
|
14
|
+
* bot says, else as `NODE_ENV` says.
|
|
15
|
+
*/ function registersAsDevelopment(env = process.env) {
|
|
16
|
+
const mode = env[REGISTER_MODE_ENV];
|
|
17
|
+
if (mode === 'development' || mode === 'production') return mode === 'development';
|
|
18
|
+
return process.env.NODE_ENV === 'development';
|
|
19
|
+
}
|
|
7
20
|
/** Whether this process was started to register commands and exit. */ function isRegisterOnly(env = process.env) {
|
|
8
21
|
return env[REGISTER_ONLY_ENV] === '1';
|
|
9
22
|
}
|
|
10
23
|
|
|
11
|
-
export { FORCE_REGISTER_ENV, REGISTER_GUILD_ENV, REGISTER_ONLY_ENV, isRegisterOnly };
|
|
24
|
+
export { FORCE_REGISTER_ENV, REGISTER_GUILD_ENV, REGISTER_MODE_ENV, REGISTER_ONLY_ENV, isRegisterOnly, registersAsDevelopment };
|