chapterjs 0.1.4 → 0.2.0
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/dist/cli/build.js +9 -10
- package/dist/cli/dev.js +17 -16
- package/dist/cli/project.d.ts +38 -25
- package/dist/cli/project.js +199 -134
- package/dist/commands/command.d.ts +25 -11
- package/dist/commands/command.js +5 -5
- package/dist/commands/{convention.d.ts → declaration.d.ts} +24 -11
- package/dist/commands/{convention.js → declaration.js} +61 -33
- package/dist/commands/tree.d.ts +8 -4
- package/dist/commands/tree.js +16 -6
- package/dist/components/button.d.ts +13 -13
- package/dist/components/button.js +9 -11
- package/dist/components/component.d.ts +1 -1
- package/dist/components/component.js +1 -1
- package/dist/components/custom-id.d.ts +4 -4
- package/dist/components/custom-id.js +12 -12
- package/dist/components/declaration.d.ts +42 -0
- package/dist/components/{convention.js → declaration.js} +76 -73
- package/dist/components/declared.d.ts +29 -0
- package/dist/components/declared.js +49 -0
- package/dist/components/embed.d.ts +13 -12
- package/dist/components/embed.js +6 -6
- package/dist/components/instance.d.ts +3 -3
- package/dist/components/instance.js +2 -2
- package/dist/components/modal.d.ts +11 -10
- package/dist/components/modal.js +7 -8
- package/dist/components/router.d.ts +3 -2
- package/dist/components/router.js +14 -19
- package/dist/components/select.d.ts +14 -12
- package/dist/components/select.js +4 -4
- package/dist/events/declaration.d.ts +16 -0
- package/dist/events/declaration.js +80 -0
- package/dist/events/event.d.ts +42 -5
- package/dist/events/event.js +25 -11
- package/dist/events/registry.d.ts +1 -10
- package/dist/events/router.d.ts +3 -2
- package/dist/index.d.ts +11 -9
- package/dist/index.js +2 -0
- package/dist/loader/generated.d.ts +5 -32
- package/dist/loader/generated.js +21 -55
- package/dist/loader/loader.d.ts +57 -44
- package/dist/loader/loader.js +73 -89
- package/dist/main.d.ts +0 -1
- package/dist/main.js +2 -3
- package/dist/messages/commands.d.ts +3 -3
- package/dist/messages/commands.js +29 -10
- package/dist/messages/declaration.d.ts +89 -0
- package/dist/messages/declaration.js +455 -0
- package/dist/messages/messages.d.ts +65 -43
- package/dist/messages/messages.js +11 -10
- package/dist/messages/phrases.js +1 -1
- package/dist/messages/translate.d.ts +4 -4
- package/dist/messages/translate.js +4 -4
- package/dist/presence/declaration.d.ts +19 -0
- package/dist/presence/{convention.js → declaration.js} +13 -28
- package/dist/presence/presence.d.ts +7 -7
- package/dist/presence/presence.js +5 -5
- package/dist/structures/context.d.ts +1 -1
- package/dist/structures/interaction.d.ts +2 -2
- package/dist/structures/interaction.js +3 -3
- package/dist/structures/payload.d.ts +4 -4
- package/dist/structures/payload.js +4 -4
- package/dist/tasks/declaration.d.ts +16 -0
- package/dist/tasks/{convention.js → declaration.js} +14 -36
- package/dist/tasks/scheduler.d.ts +3 -1
- package/dist/tasks/scheduler.js +4 -4
- package/dist/tasks/task.d.ts +8 -9
- package/dist/tasks/task.js +6 -7
- package/package.json +1 -1
- package/dist/components/convention.d.ts +0 -35
- package/dist/components/file.d.ts +0 -30
- package/dist/components/file.js +0 -52
- package/dist/events/convention.d.ts +0 -25
- package/dist/events/convention.js +0 -97
- package/dist/events/types.d.ts +0 -7
- package/dist/events/types.js +0 -48
- package/dist/messages/convention.d.ts +0 -89
- package/dist/messages/convention.js +0 -347
- package/dist/presence/convention.d.ts +0 -26
- package/dist/tasks/convention.d.ts +0 -26
package/dist/cli/project.js
CHANGED
|
@@ -6,64 +6,66 @@ import { stat } from 'node:fs/promises';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { setPublicDir } from '../assets/asset.js';
|
|
8
8
|
import { listPublic, publicDeclarations } from '../assets/public.js';
|
|
9
|
-
import { eventTypedFolders } from '../events/types.js';
|
|
10
9
|
import { MissingForEvent } from '../events/registry.js';
|
|
11
10
|
import { sourcesJoinVoice } from '../voice/usage.js';
|
|
12
11
|
import { writeGenerated } from '../loader/generated.js';
|
|
13
|
-
import {
|
|
12
|
+
import { commandDeclaration, scanCommands } from '../commands/declaration.js';
|
|
14
13
|
import { CommandRouter } from '../commands/router.js';
|
|
15
14
|
import { findConflicts } from '../commands/tree.js';
|
|
16
|
-
import {
|
|
15
|
+
import { componentDeclaration, findDuplicates, } from '../components/declaration.js';
|
|
17
16
|
import { ComponentRouter } from '../components/router.js';
|
|
18
|
-
import {
|
|
19
|
-
import { DEFAULT_PRESENCE,
|
|
20
|
-
import { assembleMessages, commandsDeclarations,
|
|
17
|
+
import { taskDeclaration } from '../tasks/declaration.js';
|
|
18
|
+
import { DEFAULT_PRESENCE, presenceDeclaration, } from '../presence/declaration.js';
|
|
19
|
+
import { assembleMessages, commandsDeclarations, languageDeclaration, languageDefaultsDeclaration, messagesDeclarations, scanLanguages, } from '../messages/declaration.js';
|
|
21
20
|
import { translation } from '../messages/translate.js';
|
|
22
21
|
import { translateCommand, unknownCommands } from '../messages/commands.js';
|
|
23
22
|
import { TimerScheduler, } from '../tasks/scheduler.js';
|
|
24
23
|
import { createBot } from '../core/bot.js';
|
|
25
24
|
import { watchMemory } from '../core/memory.js';
|
|
26
25
|
import { GatewayIntent } from '../discord/intents.js';
|
|
27
|
-
import {
|
|
26
|
+
import { eventDeclaration } from '../events/declaration.js';
|
|
28
27
|
import { EventRouter, intentsFor, intentsOf, limitsFor, } from '../events/router.js';
|
|
29
28
|
import { GatewayFatalError, SessionLimitError } from '../gateway/errors.js';
|
|
30
|
-
import {
|
|
29
|
+
import { loadSources, readDeclarations, siteName, } from '../loader/loader.js';
|
|
31
30
|
import { locate, messageOf } from '../loader/locate.js';
|
|
32
31
|
import { DiscordApiError, DiscordUnavailableError, InvalidTokenError, } from '../rest/errors.js';
|
|
33
32
|
import { PreflightFailure } from './preflight.js';
|
|
34
33
|
/**
|
|
35
|
-
* Writes the types of a project (`.chapterjs/`):
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the
|
|
42
|
-
*
|
|
34
|
+
* Writes the types of a project (`.chapterjs/`): what the project adds to
|
|
35
|
+
* 'chapterjs', like the files of `public/`, its languages and its
|
|
36
|
+
* commands. With the project as it loaded (`info`), a language only
|
|
37
|
+
* offers the commands without `description`, `t` is typed from the
|
|
38
|
+
* default language and `default: true` is refused in every language that
|
|
39
|
+
* says it when another one does; without it (`sync`, which runs nothing),
|
|
40
|
+
* the commands and the languages are found by reading the files, every
|
|
41
|
+
* command is offered, and the first language or the one that says
|
|
42
|
+
* `default: true` types `t`.
|
|
43
43
|
* @returns how many files were written
|
|
44
44
|
*/
|
|
45
|
-
export async function writeTypes(cwd,
|
|
46
|
-
const [files,
|
|
45
|
+
export async function writeTypes(cwd, info) {
|
|
46
|
+
const [files, found, names] = await Promise.all([
|
|
47
47
|
listPublic(cwd),
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
info ? null : scanLanguages(cwd),
|
|
49
|
+
info ? null : scanCommands(cwd),
|
|
50
50
|
]);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const languages = info ? info.languages : found.languages;
|
|
52
|
+
const defaultLocale = info ? info.defaultLocale : found.defaultLocale;
|
|
53
|
+
const commands = info
|
|
54
|
+
? info.commands
|
|
55
|
+
: names.map(name => ({ name, described: false }));
|
|
56
|
+
return writeGenerated(cwd, publicDeclarations(files) +
|
|
57
|
+
messagesDeclarations(languages, defaultLocale) +
|
|
58
|
+
languageDefaultsDeclaration(info?.languageDefaults ?? []) +
|
|
59
|
+
commandsDeclarations(commands));
|
|
58
60
|
}
|
|
59
|
-
/** The
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
/** The kinds of declarations a project is made of: one per feature. */
|
|
62
|
+
export const DECLARATIONS = [
|
|
63
|
+
eventDeclaration,
|
|
64
|
+
commandDeclaration,
|
|
65
|
+
componentDeclaration,
|
|
66
|
+
taskDeclaration,
|
|
67
|
+
presenceDeclaration,
|
|
68
|
+
languageDeclaration,
|
|
67
69
|
];
|
|
68
70
|
/** Whether the folder is a project: it has a `src` folder. */
|
|
69
71
|
export async function hasSources(cwd, log) {
|
|
@@ -79,13 +81,45 @@ const PRIVATE_INTENTS = GatewayIntent.DirectMessages |
|
|
|
79
81
|
GatewayIntent.DirectMessageTyping |
|
|
80
82
|
GatewayIntent.DirectMessagePolls;
|
|
81
83
|
const megabytes = (bytes) => `${Math.round(bytes / 1024 / 1024)} MB`;
|
|
84
|
+
/** What tells one declaration from another: its file and its export. */
|
|
85
|
+
const keyOf = (site) => `${site.file}#${site.export}`;
|
|
86
|
+
/**
|
|
87
|
+
* Two declarations with the same name, where the name is the export's
|
|
88
|
+
* (a task): the later one is left out, with a message saying so.
|
|
89
|
+
*/
|
|
90
|
+
function sameNames(entries, nameOf, what) {
|
|
91
|
+
const seen = new Map();
|
|
92
|
+
const conflicts = [];
|
|
93
|
+
const valid = [...entries]
|
|
94
|
+
.sort((a, b) => a.file === b.file
|
|
95
|
+
? a.export < b.export
|
|
96
|
+
? -1
|
|
97
|
+
: 1
|
|
98
|
+
: a.file < b.file
|
|
99
|
+
? -1
|
|
100
|
+
: 1)
|
|
101
|
+
.filter(entry => {
|
|
102
|
+
const first = seen.get(nameOf(entry));
|
|
103
|
+
if (!first) {
|
|
104
|
+
seen.set(nameOf(entry), entry);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
conflicts.push({
|
|
108
|
+
file: entry.file,
|
|
109
|
+
export: entry.export,
|
|
110
|
+
message: `There is already a ${what} named ${nameOf(entry)}, in ${first.file}: the name of the export is the name of the ${what}, so rename one of them.`,
|
|
111
|
+
});
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
return { valid, conflicts };
|
|
115
|
+
}
|
|
82
116
|
export function createProject(options) {
|
|
83
117
|
const { cwd, log } = options;
|
|
84
118
|
// Where `asset()` reads the files of the project from.
|
|
85
119
|
setPublicDir(cwd);
|
|
86
|
-
const report = (
|
|
87
|
-
const where = locate(error, cwd);
|
|
88
|
-
log.error(`${where ? `${where.file}:${where.line}` :
|
|
120
|
+
const report = (failure) => {
|
|
121
|
+
const where = locate(failure.error, cwd);
|
|
122
|
+
log.error(`${where ? `${where.file}:${where.line}` : siteName(failure)} ${messageOf(failure.error)}`);
|
|
89
123
|
};
|
|
90
124
|
const router = new EventRouter((file, error) => report({ file, error }), (event, error) => {
|
|
91
125
|
const missing = error instanceof MissingForEvent ? error : null;
|
|
@@ -109,11 +143,13 @@ export function createProject(options) {
|
|
|
109
143
|
let components = new Map();
|
|
110
144
|
let tasks = new Map();
|
|
111
145
|
let presence = null;
|
|
146
|
+
/** The files the presence was declared in, to keep it when they break. */
|
|
147
|
+
let presenceFiles = new Set();
|
|
112
148
|
// Joining voice needs the bot's own voice state, sent with an intent.
|
|
113
149
|
let voice = options.joinsVoice ?? false;
|
|
114
150
|
const neededIntents = () => intentsFor(events.values()) | (voice ? GatewayIntent.GuildVoiceStates : 0);
|
|
115
151
|
let messages = null;
|
|
116
|
-
/** The last version of each language
|
|
152
|
+
/** The last version of each language that loaded. */
|
|
117
153
|
let languages = new Map();
|
|
118
154
|
let languageDefaults = [];
|
|
119
155
|
/** The bot running the files now, to give it what a reload changes. */
|
|
@@ -141,107 +177,134 @@ export function createProject(options) {
|
|
|
141
177
|
get messages() {
|
|
142
178
|
return messages;
|
|
143
179
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
180
|
+
typesInfo: () => ({
|
|
181
|
+
languages: [...languages.values()].map(entry => ({
|
|
182
|
+
file: entry.file,
|
|
183
|
+
export: entry.export,
|
|
184
|
+
name: entry.name,
|
|
185
|
+
locale: entry.language.locale,
|
|
186
|
+
})),
|
|
187
|
+
...(messages ? { defaultLocale: messages.default } : {}),
|
|
188
|
+
languageDefaults,
|
|
189
|
+
commands: [...commands.values()].map(({ command }) => ({
|
|
190
|
+
name: command.path.join(' '),
|
|
191
|
+
described: command.described,
|
|
192
|
+
})),
|
|
193
|
+
}),
|
|
147
194
|
report,
|
|
148
195
|
async load() {
|
|
149
196
|
const { built } = options;
|
|
150
197
|
if (options.joinsVoice === undefined) {
|
|
151
198
|
voice = await sourcesJoinVoice(cwd);
|
|
152
199
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
// Every file of src/ runs (or ran, in a build); what each exports is
|
|
201
|
+
// then read, kind by kind.
|
|
202
|
+
const sources = built ? { modules: built, failed: [] } : await loadSources(cwd);
|
|
203
|
+
const read = (declaration) => readDeclarations(sources.modules, declaration);
|
|
204
|
+
const [eventItems, commandItems, componentItems, taskItems, presenceItems, languageItems,] = [
|
|
205
|
+
read(eventDeclaration),
|
|
206
|
+
read(commandDeclaration),
|
|
207
|
+
read(componentDeclaration),
|
|
208
|
+
read(taskDeclaration),
|
|
209
|
+
read(presenceDeclaration),
|
|
210
|
+
read(languageDeclaration),
|
|
211
|
+
];
|
|
212
|
+
const broken = new Set(sources.failed.map(({ file }) => file));
|
|
213
|
+
/**
|
|
214
|
+
* The next version of a kind: what loaded now, plus the last good
|
|
215
|
+
* version of what failed (an export that can't be read, or a file
|
|
216
|
+
* that can't run at all).
|
|
217
|
+
*/
|
|
218
|
+
const next = (previous, items, make) => {
|
|
219
|
+
const result = new Map();
|
|
220
|
+
for (const item of items.loaded)
|
|
221
|
+
result.set(keyOf(item), make(item));
|
|
222
|
+
const failedKeys = new Set(items.failed.map(({ file, export: exported }) => keyOf({ file, export: exported ?? 'default' })));
|
|
223
|
+
for (const [key, entry] of previous) {
|
|
224
|
+
if (failedKeys.has(key) || broken.has(entry.file)) {
|
|
225
|
+
result.set(key, entry);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
};
|
|
230
|
+
events = next(events, eventItems, item => ({
|
|
231
|
+
file: item.file,
|
|
232
|
+
export: item.export,
|
|
233
|
+
event: item.value,
|
|
234
|
+
}));
|
|
180
235
|
router.set(events.values());
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
commands = new Map(valid.map(entry => [entry
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
components = new Map(unique.valid.map(entry => [entry
|
|
236
|
+
const { valid, conflicts } = findConflicts([
|
|
237
|
+
...next(commands, commandItems, item => ({
|
|
238
|
+
file: item.file,
|
|
239
|
+
export: item.export,
|
|
240
|
+
command: item.value,
|
|
241
|
+
})).values(),
|
|
242
|
+
]);
|
|
243
|
+
commands = new Map(valid.map(entry => [keyOf(entry), entry]));
|
|
244
|
+
const unique = findDuplicates([
|
|
245
|
+
...next(components, componentItems, item => ({
|
|
246
|
+
file: item.file,
|
|
247
|
+
export: item.export,
|
|
248
|
+
component: item.value,
|
|
249
|
+
})).values(),
|
|
250
|
+
]);
|
|
251
|
+
components = new Map(unique.valid.map(entry => [keyOf(entry), entry]));
|
|
197
252
|
componentRouter.set(components.values());
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
tasks = nextTasks;
|
|
253
|
+
const sameTask = sameNames([
|
|
254
|
+
...next(tasks, taskItems, item => ({
|
|
255
|
+
file: item.file,
|
|
256
|
+
export: item.export,
|
|
257
|
+
task: item.value,
|
|
258
|
+
})).values(),
|
|
259
|
+
], entry => entry.task.name, 'task');
|
|
260
|
+
tasks = new Map(sameTask.valid.map(entry => [keyOf(entry), entry]));
|
|
208
261
|
scheduler.set(tasks.values());
|
|
209
|
-
// One presence
|
|
210
|
-
const [first, ...extra] =
|
|
262
|
+
// One presence. A second one is left out.
|
|
263
|
+
const [first, ...extra] = presenceItems.loaded;
|
|
211
264
|
if (first)
|
|
212
265
|
presence = first.value;
|
|
213
|
-
else if (
|
|
266
|
+
else if (presenceItems.failed.length === 0 &&
|
|
267
|
+
![...broken].some(file => presenceFiles.has(file))) {
|
|
214
268
|
presence = null;
|
|
215
|
-
const twice = extra.map(({ file }) => ({
|
|
216
|
-
file,
|
|
217
|
-
error: new TypeError(`There are two presence files: ${first.file} is used, keep only one.`),
|
|
218
|
-
}));
|
|
219
|
-
const nextLanguages = new Map();
|
|
220
|
-
for (const { file, value } of messageFiles.loaded) {
|
|
221
|
-
nextLanguages.set(file, { file, language: value });
|
|
222
|
-
}
|
|
223
|
-
for (const { file } of messageFiles.failed) {
|
|
224
|
-
const previous = languages.get(file);
|
|
225
|
-
if (previous)
|
|
226
|
-
nextLanguages.set(file, previous);
|
|
227
269
|
}
|
|
228
|
-
|
|
229
|
-
|
|
270
|
+
presenceFiles = new Set(presenceItems.loaded.map(({ file }) => file));
|
|
271
|
+
const twice = extra.map(item => ({
|
|
272
|
+
file: item.file,
|
|
273
|
+
export: item.export,
|
|
274
|
+
error: new TypeError(`There are two presences: the one of ${siteName(first)} is used, keep only one.`),
|
|
275
|
+
}));
|
|
276
|
+
const nextLanguages = next(languages, languageItems, item => ({
|
|
277
|
+
file: item.file,
|
|
278
|
+
export: item.export,
|
|
279
|
+
name: item.name,
|
|
280
|
+
language: item.value,
|
|
281
|
+
}));
|
|
282
|
+
// What the files say now, before a declaration is put back to its
|
|
283
|
+
// last good version: two languages saying default: true are both
|
|
284
|
+
// underlined.
|
|
230
285
|
languageDefaults = [...nextLanguages.values()]
|
|
231
286
|
.filter(({ language }) => language.isDefault)
|
|
232
|
-
.map(({
|
|
287
|
+
.map(({ language }) => language.locale);
|
|
233
288
|
const previousLanguages = languages;
|
|
234
289
|
languages = nextLanguages;
|
|
235
290
|
let assembled = assembleMessages([...languages.values()]);
|
|
236
291
|
// A language that no longer matches the others keeps its last version
|
|
237
|
-
// that did, like a
|
|
238
|
-
const restored = assembled.failed.filter(
|
|
239
|
-
const
|
|
240
|
-
|
|
292
|
+
// that did, like a declaration that no longer loads.
|
|
293
|
+
const restored = assembled.failed.filter(failure => {
|
|
294
|
+
const key = keyOf({
|
|
295
|
+
file: failure.file,
|
|
296
|
+
export: failure.export ?? 'default',
|
|
297
|
+
});
|
|
298
|
+
const previous = previousLanguages.get(key);
|
|
299
|
+
return previous !== undefined && previous !== languages.get(key);
|
|
241
300
|
});
|
|
242
301
|
if (restored.length > 0) {
|
|
243
|
-
for (const
|
|
244
|
-
|
|
302
|
+
for (const failure of restored) {
|
|
303
|
+
const key = keyOf({
|
|
304
|
+
file: failure.file,
|
|
305
|
+
export: failure.export ?? 'default',
|
|
306
|
+
});
|
|
307
|
+
languages.set(key, previousLanguages.get(key));
|
|
245
308
|
}
|
|
246
309
|
const again = assembleMessages([...languages.values()]);
|
|
247
310
|
assembled = {
|
|
@@ -260,34 +323,36 @@ export function createProject(options) {
|
|
|
260
323
|
// be described is left out.
|
|
261
324
|
const badTexts = [...assembled.failed];
|
|
262
325
|
if (messages) {
|
|
263
|
-
const
|
|
264
|
-
for (const { file, path } of unknownCommands(messages,
|
|
326
|
+
const names = new Set([...commands.values()].map(entry => entry.command.path.join(' ')));
|
|
327
|
+
for (const { file, path } of unknownCommands(messages, names)) {
|
|
265
328
|
badTexts.push({
|
|
266
329
|
file,
|
|
267
|
-
error: new TypeError(`"commands" translates "${path}", which is not a command of this project${
|
|
330
|
+
error: new TypeError(`"commands" translates "${path}", which is not a command of this project${names.size > 0 ? ` (its commands are: ${[...names].join(', ')})` : ''}. The key is the name of the command, like 'ping' or 'mod ban'.`),
|
|
268
331
|
});
|
|
269
332
|
}
|
|
270
333
|
}
|
|
271
|
-
for (const [
|
|
272
|
-
const { command, failed } = translateCommand(
|
|
334
|
+
for (const [key, entry] of commands) {
|
|
335
|
+
const { command, failed } = translateCommand(siteName(entry), entry.command, messages);
|
|
273
336
|
badTexts.push(...failed);
|
|
274
337
|
if (command)
|
|
275
|
-
commands.set(
|
|
338
|
+
commands.set(key, { ...entry, command });
|
|
276
339
|
else
|
|
277
|
-
commands.delete(
|
|
340
|
+
commands.delete(key);
|
|
278
341
|
}
|
|
279
342
|
commandRouter.set(commands.values());
|
|
280
343
|
return [
|
|
281
|
-
...
|
|
282
|
-
...
|
|
283
|
-
...
|
|
284
|
-
...
|
|
285
|
-
...
|
|
286
|
-
...
|
|
344
|
+
...sources.failed,
|
|
345
|
+
...eventItems.failed,
|
|
346
|
+
...commandItems.failed,
|
|
347
|
+
...componentItems.failed,
|
|
348
|
+
...taskItems.failed,
|
|
349
|
+
...presenceItems.failed,
|
|
350
|
+
...languageItems.failed,
|
|
287
351
|
...badTexts,
|
|
288
352
|
...twice,
|
|
289
|
-
...[...conflicts, ...unique.conflicts].map(({ file, message }) => ({
|
|
353
|
+
...[...conflicts, ...unique.conflicts, ...sameTask.conflicts].map(({ file, export: exported, message }) => ({
|
|
290
354
|
file,
|
|
355
|
+
export: exported,
|
|
291
356
|
error: new TypeError(message),
|
|
292
357
|
})),
|
|
293
358
|
];
|
|
@@ -299,7 +364,7 @@ export function createProject(options) {
|
|
|
299
364
|
tasks.size === 0 &&
|
|
300
365
|
presence === null &&
|
|
301
366
|
messages === null) {
|
|
302
|
-
return
|
|
367
|
+
return "Nothing to run yet: export a command or an event from a file of src/, like export default command({ name: 'ping', ... })";
|
|
303
368
|
}
|
|
304
369
|
return `${[
|
|
305
370
|
...(commands.size > 0
|
|
@@ -13,7 +13,7 @@ interface OptionBase {
|
|
|
13
13
|
/**
|
|
14
14
|
* What the option is for, shown under its name (1-100 characters). Here
|
|
15
15
|
* when the command has its `description` here; otherwise in the default
|
|
16
|
-
* language file of
|
|
16
|
+
* language file of the project.
|
|
17
17
|
*/
|
|
18
18
|
description?: string;
|
|
19
19
|
/** Whether the user must fill it in. By default it is optional. */
|
|
@@ -270,13 +270,27 @@ export type CommandContext<Options extends CommandOptions = CommandOptions, Wher
|
|
|
270
270
|
/** Who used the command. */
|
|
271
271
|
user: User;
|
|
272
272
|
} & TranslationContext & (Where extends 'guild' ? CommandInGuild : Where extends 'dm' ? CommandInDm : CommandInGuild | CommandInPrivate);
|
|
273
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* The name of a slash command: lowercase letters, digits, `-` and `_`, 32
|
|
275
|
+
* characters at most (`'ping'`). A subcommand is written with its parents,
|
|
276
|
+
* separated by spaces: `'mod ban'` is `/mod ban`, `'mod roles add'` is
|
|
277
|
+
* `/mod roles add` (Discord allows nothing deeper).
|
|
278
|
+
* @see https://docs.discord.com/developers/interactions/application-commands#application-command-object-application-command-naming
|
|
279
|
+
*/
|
|
280
|
+
export type CommandName = string;
|
|
281
|
+
/** What a file gives to `command()`: what the command is and does. */
|
|
274
282
|
export interface CommandConfig<Options extends CommandOptions = CommandOptions, Where extends CommandWhere = 'guild'> {
|
|
283
|
+
/**
|
|
284
|
+
* What people type after the `/`: `'ping'` is `/ping`. A subcommand is
|
|
285
|
+
* written with its parents: `'mod ban'` is `/mod ban`, `'mod roles add'`
|
|
286
|
+
* is `/mod roles add`.
|
|
287
|
+
*/
|
|
288
|
+
name: CommandName;
|
|
275
289
|
/**
|
|
276
290
|
* What the command does, shown under its name (1-100 characters). Here,
|
|
277
291
|
* with `locales` for its translations; or left out, and every text of
|
|
278
292
|
* the command (description, options, their choices, translations) comes
|
|
279
|
-
* from the language files of
|
|
293
|
+
* from the language files of the project. One or the other.
|
|
280
294
|
*/
|
|
281
295
|
description?: string;
|
|
282
296
|
/** What the user can fill in (25 options at most). */
|
|
@@ -319,20 +333,20 @@ export interface CommandConfig<Options extends CommandOptions = CommandOptions,
|
|
|
319
333
|
/** What to do when someone uses the command. */
|
|
320
334
|
run: (context: CommandContext<Options, Where>) => unknown;
|
|
321
335
|
}
|
|
322
|
-
/** What `command()` returns: the
|
|
323
|
-
export interface
|
|
336
|
+
/** What `command()` returns: a declaration the framework finds in the exports of a file. */
|
|
337
|
+
export interface CommandDeclaration<Options extends CommandOptions = CommandOptions, Where extends CommandWhere = CommandWhere> {
|
|
324
338
|
/** What the file gave to `command()`, not checked yet. */
|
|
325
339
|
readonly config: CommandConfig<Options, Where>;
|
|
326
340
|
}
|
|
327
341
|
/**
|
|
328
|
-
* Declares a slash command. Export the result
|
|
329
|
-
*
|
|
330
|
-
* (`src/commands/mod/ban.ts` is `/mod ban`).
|
|
342
|
+
* Declares a slash command. Export the result from any file of `src/`:
|
|
343
|
+
* `name` is what people type after the `/` (`'mod ban'` for `/mod ban`).
|
|
331
344
|
*
|
|
332
345
|
* ```ts
|
|
333
346
|
* import { command } from 'chapterjs';
|
|
334
347
|
*
|
|
335
|
-
* export
|
|
348
|
+
* export const ping = command({
|
|
349
|
+
* name: 'ping',
|
|
336
350
|
* description: 'Replies with Pong!',
|
|
337
351
|
* async run({ interaction }) {
|
|
338
352
|
* await interaction.reply('Pong!');
|
|
@@ -340,7 +354,7 @@ export interface CommandFile<Options extends CommandOptions = CommandOptions, Wh
|
|
|
340
354
|
* });
|
|
341
355
|
* ```
|
|
342
356
|
*/
|
|
343
|
-
export declare function command<const Options extends CommandOptions = {}, const Where extends CommandWhere = 'guild'>(config: CommandConfig<Options, Where>):
|
|
357
|
+
export declare function command<const Options extends CommandOptions = {}, const Where extends CommandWhere = 'guild'>(config: CommandConfig<Options, Where>): CommandDeclaration<Options, Where>;
|
|
344
358
|
/** Whether a value was made by `command()`. */
|
|
345
|
-
export declare function
|
|
359
|
+
export declare function isCommandDeclaration(value: unknown): value is CommandDeclaration;
|
|
346
360
|
export {};
|
package/dist/commands/command.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// make the options a command declares typed in its `run` function.
|
|
3
3
|
const BRAND = Symbol.for('chapterjs.command');
|
|
4
4
|
/**
|
|
5
|
-
* Declares a slash command. Export the result
|
|
6
|
-
*
|
|
7
|
-
* (`src/commands/mod/ban.ts` is `/mod ban`).
|
|
5
|
+
* Declares a slash command. Export the result from any file of `src/`:
|
|
6
|
+
* `name` is what people type after the `/` (`'mod ban'` for `/mod ban`).
|
|
8
7
|
*
|
|
9
8
|
* ```ts
|
|
10
9
|
* import { command } from 'chapterjs';
|
|
11
10
|
*
|
|
12
|
-
* export
|
|
11
|
+
* export const ping = command({
|
|
12
|
+
* name: 'ping',
|
|
13
13
|
* description: 'Replies with Pong!',
|
|
14
14
|
* async run({ interaction }) {
|
|
15
15
|
* await interaction.reply('Pong!');
|
|
@@ -21,7 +21,7 @@ export function command(config) {
|
|
|
21
21
|
return Object.freeze({ [BRAND]: true, config });
|
|
22
22
|
}
|
|
23
23
|
/** Whether a value was made by `command()`. */
|
|
24
|
-
export function
|
|
24
|
+
export function isCommandDeclaration(value) {
|
|
25
25
|
return (typeof value === 'object' &&
|
|
26
26
|
value !== null &&
|
|
27
27
|
value[BRAND] === true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Locale } from '../discord/types/common.js';
|
|
2
2
|
import { type CooldownLimits } from '../interactions/cooldown.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Declaration } from '../loader/loader.js';
|
|
4
4
|
import { type AutocompleteContext, type CommandContext, type CommandOption, type CommandWhere, type Suggestion } from './command.js';
|
|
5
5
|
/** A name or a description in other languages, as Discord takes them. */
|
|
6
6
|
export type Localizations = Readonly<Partial<Record<Locale, string>>>;
|
|
@@ -18,20 +18,20 @@ export type LoadedOption = CommandOption & {
|
|
|
18
18
|
};
|
|
19
19
|
/** An `autocomplete` function of a file, as the router runs it. */
|
|
20
20
|
export type AutocompleteFunction = (context: AutocompleteContext) => readonly Suggestion[] | Promise<readonly Suggestion[]>;
|
|
21
|
-
/** A command
|
|
21
|
+
/** A command, checked: what the rest of the feature works with. */
|
|
22
22
|
export interface LoadedCommand {
|
|
23
23
|
/** The name and description of the command in other languages. */
|
|
24
24
|
readonly names: Localizations;
|
|
25
25
|
readonly descriptions: Localizations;
|
|
26
26
|
/** The translations of the options, by option name. */
|
|
27
27
|
readonly optionLocales: Readonly<Record<string, OptionLocales>>;
|
|
28
|
-
/** `['mod', 'ban']` for `
|
|
28
|
+
/** `['mod', 'ban']` for `name: 'mod ban'`: `/mod ban`. */
|
|
29
29
|
readonly path: readonly string[];
|
|
30
30
|
/**
|
|
31
31
|
* Whether the file gives its own texts (`description`, `locales`). When
|
|
32
|
-
* it does not, the language files
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* it does not, the language files do: `description` and the
|
|
33
|
+
* descriptions of the options are empty until `project.ts` fills them
|
|
34
|
+
* in from the default language.
|
|
35
35
|
*/
|
|
36
36
|
readonly described: boolean;
|
|
37
37
|
readonly description: string;
|
|
@@ -56,9 +56,22 @@ export declare function checkLocales(locales: unknown, options: readonly LoadedO
|
|
|
56
56
|
optionLocales: Record<string, OptionLocales>;
|
|
57
57
|
};
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
* One
|
|
61
|
-
*
|
|
62
|
-
* nothing deeper.
|
|
59
|
+
* The parts of the name of a command: `'mod ban'` is `['mod', 'ban']`.
|
|
60
|
+
* One part is a command, two a subcommand, three a subcommand in a group:
|
|
61
|
+
* Discord allows nothing deeper.
|
|
63
62
|
*/
|
|
64
|
-
export declare
|
|
63
|
+
export declare function readCommandName(name: unknown): string[];
|
|
64
|
+
/**
|
|
65
|
+
* A slash command, declared anywhere in `src/` with `command()`: `name` is
|
|
66
|
+
* what people type after the `/`, with its parents for a subcommand
|
|
67
|
+
* (`'mod ban'` is `/mod ban`, `'mod roles add'` is `/mod roles add`). A
|
|
68
|
+
* file may declare several.
|
|
69
|
+
*/
|
|
70
|
+
export declare const commandDeclaration: Declaration<LoadedCommand>;
|
|
71
|
+
/**
|
|
72
|
+
* The names of the commands of a project, found without running its
|
|
73
|
+
* files: a text search for `command({ name: '...' })`. Approximate on
|
|
74
|
+
* purpose, for `sync`, which runs nothing: `dev` and `build` then write
|
|
75
|
+
* the exact ones. Whether a command is described is not known: none is.
|
|
76
|
+
*/
|
|
77
|
+
export declare function scanCommands(cwd: string): Promise<string[]>;
|