meocord 4.2.0 → 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 +264 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +185 -168
- package/dist/cjs/decorator/index.cjs +21 -6
- package/dist/cjs/testing/index.cjs +1594 -286
- 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 +84 -10
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- 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/fallback.js +20 -11
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +55 -32
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/providers.js +7 -2
- 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/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +26 -10
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +205 -30
- package/dist/esm/testing/meocord-testing-module.js +134 -28
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1026 -172
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +37 -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 +12 -1
- package/dist/types/common/index.d.cts +6 -6
- package/dist/types/common/index.d.ts +6 -6
- 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 +175 -53
- package/dist/types/testing/index.d.ts +175 -53
- 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
|
@@ -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 };
|
|
@@ -1,26 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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;
|
|
5
9
|
/** Notes that a mock was made, which useStrictMocks() must come before. */ function noteMockMade() {
|
|
6
|
-
mockMade = true;
|
|
10
|
+
settings.mockMade = true;
|
|
7
11
|
}
|
|
8
12
|
/**
|
|
9
13
|
* Has every mock from `meocord/testing` compute the values discord.js computes, where it reads a placeholder otherwise.
|
|
10
14
|
*
|
|
11
15
|
* Call it once in the test runner's setup file. A message's `editable`, `deletable`, `pinnable`, `crosspostable`,
|
|
12
16
|
* `bulkDeletable`, `hasThread` and `partial`, a member's `manageable`, `kickable`, `bannable` and `moderatable`, a
|
|
13
|
-
* role's `editable`, a channel's and a thread's `viewable`, `manageable`, `deletable`, `joinable` and the rest,
|
|
14
|
-
* `partial` on users, channels and reactions then run discord.js's own getters against the mock,
|
|
15
|
-
* is `null` when the channel caches no thread under the message's id.
|
|
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.
|
|
16
24
|
*
|
|
17
25
|
* @remarks
|
|
18
|
-
* Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* a
|
|
23
|
-
*
|
|
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.
|
|
24
44
|
*
|
|
25
45
|
* @throws Error when a mock already exists, which was made without it.
|
|
26
46
|
*
|
|
@@ -35,11 +55,11 @@ let mockMade = false;
|
|
|
35
55
|
* @see {@link useMockFn}
|
|
36
56
|
* @see {@link https://meocord.dev/docs/4.2/mocks | Mocks}
|
|
37
57
|
*/ function useStrictMocks() {
|
|
38
|
-
if (strict) return;
|
|
39
|
-
if (mockMade) {
|
|
58
|
+
if (settings.strict) return;
|
|
59
|
+
if (settings.mockMade) {
|
|
40
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.");
|
|
41
61
|
}
|
|
42
|
-
strict = true;
|
|
62
|
+
settings.strict = true;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
export { noteMockMade, strictMocks, useStrictMocks };
|
|
@@ -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
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
|
|
@@ -66,14 +66,12 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
|
|
|
66
66
|
errors: [],
|
|
67
67
|
warnings: []
|
|
68
68
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (typeof loaded !== 'object' || loaded === null) {
|
|
72
|
-
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)})`);
|
|
73
71
|
return problems;
|
|
74
72
|
}
|
|
75
73
|
const shape = configShape(problems);
|
|
76
|
-
for (const [name, value] of Object.entries(
|
|
74
|
+
for (const [name, value] of Object.entries(config)){
|
|
77
75
|
const check = shape[name];
|
|
78
76
|
if (!check) {
|
|
79
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 };
|
|
@@ -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 };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { writeFileSync, existsSync, readFileSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { refuse } from './refusal.util.js';
|
|
4
3
|
import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -42,23 +41,24 @@ import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
|
|
|
42
41
|
writeFileSync(path.join(distDir, PLATFORM_MANIFEST), `${JSON.stringify(currentPlatform(), null, 2)}\n`);
|
|
43
42
|
}
|
|
44
43
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* Why a bundle cannot start on this platform, naming both, or `undefined` when it can: its manifest names the platform
|
|
45
|
+
* its native addons were built for. The pre-entry checks it before any package loads, where a native addon built for
|
|
46
|
+
* another platform would otherwise fail with an error of its own.
|
|
47
47
|
* @param distDir - Directory holding the manifest. Defaults to the built bundle's directory, which a process manager's
|
|
48
48
|
* wrapper in `argv[1]` would hide, and else to the entry script's.
|
|
49
|
-
*/ function
|
|
50
|
-
if (!distDir) return;
|
|
49
|
+
*/ function platformMismatch(distDir = defaultDistDir()) {
|
|
50
|
+
if (!distDir) return undefined;
|
|
51
51
|
const manifest = path.join(distDir, PLATFORM_MANIFEST);
|
|
52
|
-
if (!existsSync(manifest)) return;
|
|
52
|
+
if (!existsSync(manifest)) return undefined;
|
|
53
53
|
let built;
|
|
54
54
|
try {
|
|
55
55
|
built = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
56
56
|
} catch {
|
|
57
|
-
return;
|
|
57
|
+
return undefined;
|
|
58
58
|
}
|
|
59
59
|
const running = currentPlatform();
|
|
60
|
-
if (isSamePlatform(built, running)) return;
|
|
61
|
-
|
|
60
|
+
if (isSamePlatform(built, running)) return undefined;
|
|
61
|
+
return `${path.basename(distDir)}: this build carries native addons compiled for ${describePlatform(built)}, but is ` + `running on ${describePlatform(running)}. Compiled binaries only load on the platform they were built for. ` + 'Build on the same platform you deploy to -- for a container, run `meocord build` inside the image.';
|
|
62
62
|
}
|
|
63
63
|
/** The built bundle's directory, or the entry script's outside a built bot. */ function defaultDistDir() {
|
|
64
64
|
const entry = isBuiltApplication() ? bundleEntry() : undefined;
|
|
@@ -66,4 +66,4 @@ import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
|
|
|
66
66
|
return process.argv[1] ? path.dirname(process.argv[1]) : undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export { PLATFORM_MANIFEST,
|
|
69
|
+
export { PLATFORM_MANIFEST, currentPlatform, describePlatform, isSamePlatform, platformMismatch, writePlatformManifest };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Set by discord.js in every process its ShardingManager spawns. */ const SHARDING_MANAGER_ENV = 'SHARDING_MANAGER';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Whether a process with `env` was spawned as a shard by a sharding manager. Read before anything builds a
|
|
4
|
+
* `ShardingManager`, whose constructor writes the variable into its own process too.
|
|
5
|
+
*/ function isShardProcess(env) {
|
|
3
6
|
return env[SHARDING_MANAGER_ENV] !== undefined;
|
|
4
7
|
}
|
|
5
8
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { refuse } from './refusal.util.js';
|
|
2
|
-
import { isShardProcess } from './shard-process.util.js';
|
|
3
|
-
export { SHARDING_MANAGER_ENV } from './shard-process.util.js';
|
|
4
2
|
|
|
5
3
|
/** Whether a shard's manager is gone: the IPC channel it asks a shard to stop through is closed. */ function managerGone(proc = process) {
|
|
6
4
|
return proc.connected === false;
|
|
@@ -10,10 +8,10 @@ export { SHARDING_MANAGER_ENV } from './shard-process.util.js';
|
|
|
10
8
|
return env.NODE_ENV !== 'development' || config.sharding.development === true;
|
|
11
9
|
}
|
|
12
10
|
/**
|
|
13
|
-
* The role of this process: a shard when a manager spawned it, the manager when process sharding is
|
|
14
|
-
* and otherwise a single process running every shard it has.
|
|
15
|
-
*/ function shardingRole(config, env = process.env) {
|
|
16
|
-
if (
|
|
11
|
+
* The role of this process: a shard when a manager spawned it, as `shard` says, the manager when process sharding is
|
|
12
|
+
* on, and otherwise a single process running every shard it has.
|
|
13
|
+
*/ function shardingRole(config, shard, env = process.env) {
|
|
14
|
+
if (shard) return 'shard';
|
|
17
15
|
return processShardingEnabled(config, env) ? 'manager' : 'single';
|
|
18
16
|
}
|
|
19
17
|
/**
|
|
@@ -52,4 +50,4 @@ export { SHARDING_MANAGER_ENV } from './shard-process.util.js';
|
|
|
52
50
|
return clientOptions;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
export { clientOptionsWithSharding,
|
|
53
|
+
export { clientOptionsWithSharding, managerGone, processShardingEnabled, shardingRole };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import packageJson from '../package.json.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The name of a key the ES module and CommonJS builds of this meocord version share, and no other version has, for
|
|
5
|
+
* `Symbol.for`: a process that loads both builds of one install keeps one state under it, and two installed versions
|
|
6
|
+
* keep theirs apart. `Symbol.for(sharedName(...))` written out types as a `unique symbol`.
|
|
7
|
+
*/ const sharedName = (name)=>`meocord@${packageJson.version}:${name}`;
|
|
8
|
+
/** The key named {@link sharedName}. */ function sharedKey(name) {
|
|
9
|
+
return Symbol.for(sharedName(name));
|
|
10
|
+
}
|
|
11
|
+
/** The state both builds of this version keep on `globalThis` under `name`, made by whichever asks first. */ function shared(name, make) {
|
|
12
|
+
const key = sharedKey(name);
|
|
13
|
+
const store = globalThis;
|
|
14
|
+
if (!Object.hasOwn(store, key)) Object.defineProperty(store, key, {
|
|
15
|
+
value: make(),
|
|
16
|
+
configurable: true
|
|
17
|
+
});
|
|
18
|
+
return store[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { shared, sharedKey, sharedName };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parseTsconfig, createPathsMatcher } from 'get-tsconfig';
|
|
4
|
+
|
|
5
|
+
// Stands in for a pattern's `*` while its targets are worked out, then is put back
|
|
6
|
+
const STAR = '__meocord_paths_star__';
|
|
7
|
+
/**
|
|
8
|
+
* The project's tsconfig `paths`, each target an absolute path, as `tsc` reads them: through `extends`, from `baseUrl`,
|
|
9
|
+
* and with `${configDir}`. Read with get-tsconfig, whatever TypeScript the project installed. Undefined when there are
|
|
10
|
+
* none.
|
|
11
|
+
*/ function projectPaths(cwd = process.cwd()) {
|
|
12
|
+
const configPath = path.join(cwd, 'tsconfig.json');
|
|
13
|
+
if (!existsSync(configPath)) return undefined;
|
|
14
|
+
const tsconfig = {
|
|
15
|
+
path: configPath,
|
|
16
|
+
config: parseTsconfig(configPath)
|
|
17
|
+
};
|
|
18
|
+
const paths = tsconfig.config.compilerOptions?.paths;
|
|
19
|
+
// The matcher resolves each target from the file that declares it, or from baseUrl, as tsc does
|
|
20
|
+
const match = createPathsMatcher(tsconfig);
|
|
21
|
+
if (!paths || !match) return undefined;
|
|
22
|
+
// Resolved, so each is the platform's own absolute path: get-tsconfig writes Windows paths with forward slashes
|
|
23
|
+
return Object.fromEntries(Object.keys(paths).map((alias)=>[
|
|
24
|
+
alias,
|
|
25
|
+
match(alias.replace('*', STAR)).map((target)=>path.resolve(target).replace(STAR, '*'))
|
|
26
|
+
]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { projectPaths };
|