chapterjs 0.1.4 → 0.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/dist/cli/build.js +9 -10
- package/dist/cli/dev.js +29 -16
- package/dist/cli/project.d.ts +38 -25
- package/dist/cli/project.js +199 -134
- package/dist/cli/update-check.d.ts +42 -0
- package/dist/cli/update-check.js +125 -0
- 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
|
@@ -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[]>;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// of Discord, so a mistake is explained before Discord refuses it.
|
|
1
|
+
// What a file declares with `command()`, checked when it loads against the
|
|
2
|
+
// limits of Discord, so a mistake is explained before Discord refuses it.
|
|
4
3
|
// https://docs.discord.com/developers/interactions/application-commands
|
|
5
4
|
import { ALL_PERMISSIONS } from '../discord/permissions.js';
|
|
6
5
|
import { ChannelType } from '../discord/types/channel.js';
|
|
7
6
|
import { Locale } from '../discord/types/common.js';
|
|
8
7
|
import { PermissionFlags } from '../discord/types/permissions.js';
|
|
9
8
|
import { COOLDOWN_SCOPES, NO_COOLDOWN, } from '../interactions/cooldown.js';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { listSources } from '../loader/loader.js';
|
|
10
11
|
import { DURATION_EXAMPLE, parseDuration } from '../util/duration.js';
|
|
11
|
-
import {
|
|
12
|
+
import { isCommandDeclaration, } from './command.js';
|
|
12
13
|
/**
|
|
13
14
|
* The names of commands and options: lowercase letters of any language,
|
|
14
15
|
* digits, `-` and `_`, 32 characters at most.
|
|
@@ -39,6 +40,7 @@ const OPTION_KEYS = {
|
|
|
39
40
|
channel: ['channelTypes'],
|
|
40
41
|
};
|
|
41
42
|
const COMMAND_KEYS = [
|
|
43
|
+
'name',
|
|
42
44
|
'description',
|
|
43
45
|
'options',
|
|
44
46
|
'locales',
|
|
@@ -388,41 +390,45 @@ function checkPermissions(permissions) {
|
|
|
388
390
|
}
|
|
389
391
|
return bits & ALL_PERMISSIONS;
|
|
390
392
|
}
|
|
393
|
+
const EXAMPLE = `command({ name: 'ping', description: '...', run({ interaction }) { ... } })`;
|
|
391
394
|
/**
|
|
392
|
-
*
|
|
393
|
-
* One
|
|
394
|
-
*
|
|
395
|
-
* nothing deeper.
|
|
395
|
+
* The parts of the name of a command: `'mod ban'` is `['mod', 'ban']`.
|
|
396
|
+
* One part is a command, two a subcommand, three a subcommand in a group:
|
|
397
|
+
* Discord allows nothing deeper.
|
|
396
398
|
*/
|
|
397
|
-
export
|
|
398
|
-
|
|
399
|
+
export function readCommandName(name) {
|
|
400
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
401
|
+
return fail(`This command has no "name": what people type after the /, like ${EXAMPLE}. A subcommand is written with its parents: name: 'mod ban' is /mod ban.`);
|
|
402
|
+
}
|
|
403
|
+
const parts = name.trim().split(/\s+/);
|
|
404
|
+
if (parts.length > 3) {
|
|
405
|
+
fail(`The name ${JSON.stringify(name)} has ${parts.length} words: Discord allows a command, a group and a subcommand, so 3 at most ('mod roles add' is /mod roles add).`);
|
|
406
|
+
}
|
|
407
|
+
for (const part of parts) {
|
|
408
|
+
if (!isName(part)) {
|
|
409
|
+
fail(`"${part}" can't be in the name of a command: use lowercase letters, digits, - and _ only, 32 characters at most.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return parts;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A slash command, declared anywhere in `src/` with `command()`: `name` is
|
|
416
|
+
* what people type after the `/`, with its parents for a subcommand
|
|
417
|
+
* (`'mod ban'` is `/mod ban`, `'mod roles add'` is `/mod roles add`). A
|
|
418
|
+
* file may declare several.
|
|
419
|
+
*/
|
|
420
|
+
export const commandDeclaration = {
|
|
399
421
|
one: 'command',
|
|
400
422
|
many: 'commands',
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
for (const part of parts) {
|
|
407
|
-
if (!isName(part)) {
|
|
408
|
-
fail(`"${part}" can't be in the name of a command: use lowercase letters, digits, - and _ only, 32 characters at most. The path of the file is the name of the command.`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
},
|
|
412
|
-
read(exports, path) {
|
|
413
|
-
const file = exports.default;
|
|
414
|
-
const parts = path.replace(/\.[^./]+$/, '').split('/');
|
|
415
|
-
if (!isCommandFile(file)) {
|
|
416
|
-
const example = `import { command } from 'chapterjs'; export default command({ description: '...', run({ interaction }) { ... } })`;
|
|
417
|
-
return fail('default' in exports
|
|
418
|
-
? `The default export of this file must be what command() returns: ${example}`
|
|
419
|
-
: `This file has no default export. It should look like: ${example}`);
|
|
420
|
-
}
|
|
421
|
-
const config = file.config;
|
|
423
|
+
is: isCommandDeclaration,
|
|
424
|
+
list: true,
|
|
425
|
+
read(value) {
|
|
426
|
+
const config = value.config;
|
|
422
427
|
if (!isRecord(config)) {
|
|
423
|
-
return fail(`command() needs an object:
|
|
428
|
+
return fail(`command() needs an object: ${EXAMPLE}`);
|
|
424
429
|
}
|
|
425
430
|
checkKeys('a command', config, COMMAND_KEYS);
|
|
431
|
+
const parts = readCommandName(config.name);
|
|
426
432
|
// No description: the language files describe the command, options
|
|
427
433
|
// included, and hold its translations.
|
|
428
434
|
const described = config.description !== undefined;
|
|
@@ -430,7 +436,7 @@ export const commandsConvention = {
|
|
|
430
436
|
? checkDescription('this command', config.description)
|
|
431
437
|
: '';
|
|
432
438
|
if (!described && config.locales !== undefined) {
|
|
433
|
-
fail(`This command has no "description", so its texts come from the language files
|
|
439
|
+
fail(`This command has no "description", so its texts come from the language files: put its "locales" there too (commands: { ... }), or give it a description here.`);
|
|
434
440
|
}
|
|
435
441
|
if (typeof config.run !== 'function') {
|
|
436
442
|
fail(`This command has no "run": the function to run when someone uses it, like async run({ interaction }) { await interaction.reply('Pong!'); }`);
|
|
@@ -462,3 +468,25 @@ export const commandsConvention = {
|
|
|
462
468
|
});
|
|
463
469
|
},
|
|
464
470
|
};
|
|
471
|
+
/** `command({ ... name: '...' ... })`, as written in a file. */
|
|
472
|
+
const DECLARED = /\bcommand\s*\(\s*\{[^]*?\bname\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
473
|
+
/**
|
|
474
|
+
* The names of the commands of a project, found without running its
|
|
475
|
+
* files: a text search for `command({ name: '...' })`. Approximate on
|
|
476
|
+
* purpose, for `sync`, which runs nothing: `dev` and `build` then write
|
|
477
|
+
* the exact ones. Whether a command is described is not known: none is.
|
|
478
|
+
*/
|
|
479
|
+
export async function scanCommands(cwd) {
|
|
480
|
+
const names = new Set();
|
|
481
|
+
for (const { path } of await listSources(cwd)) {
|
|
482
|
+
const code = await readFile(path, 'utf8').catch(() => '');
|
|
483
|
+
if (!code.includes('command('))
|
|
484
|
+
continue;
|
|
485
|
+
for (const match of code.matchAll(DECLARED)) {
|
|
486
|
+
const name = match[1].trim().split(/\s+/).join(' ');
|
|
487
|
+
if (name !== '')
|
|
488
|
+
names.add(name);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return [...names].sort();
|
|
492
|
+
}
|
package/dist/commands/tree.d.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { type BulkOverwriteGuildApplicationCommandsJSONParams } from '../discord/types/application-command.js';
|
|
2
|
-
import type { LoadedCommand } from './
|
|
2
|
+
import type { LoadedCommand } from './declaration.js';
|
|
3
3
|
/** A command with the file it comes from. */
|
|
4
4
|
export interface CommandEntry {
|
|
5
5
|
file: string;
|
|
6
|
+
/** The export it is: `'default'`, or its name. */
|
|
7
|
+
export: string;
|
|
6
8
|
command: LoadedCommand;
|
|
7
9
|
}
|
|
8
|
-
/** Two
|
|
10
|
+
/** Two declarations that can't both be commands. */
|
|
9
11
|
export interface CommandConflict {
|
|
10
12
|
file: string;
|
|
13
|
+
export: string;
|
|
11
14
|
message: string;
|
|
12
15
|
}
|
|
13
16
|
export type CommandPayload = BulkOverwriteGuildApplicationCommandsJSONParams;
|
|
14
17
|
/** `/mod ban` for `['mod', 'ban']`. */
|
|
15
18
|
export declare const commandName: (path: readonly string[]) => string;
|
|
16
19
|
/**
|
|
17
|
-
* Finds the
|
|
18
|
-
*
|
|
20
|
+
* Finds the declarations that can't be commands together, and leaves them
|
|
21
|
+
* out:
|
|
22
|
+
* - two commands with the same name;
|
|
19
23
|
* - a command that also has subcommands: Discord only lets people use the
|
|
20
24
|
* subcommands.
|
|
21
25
|
*/
|
package/dist/commands/tree.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Turns the
|
|
1
|
+
// Turns the commands of a project into what Discord registers: one
|
|
2
2
|
// application command per top-level name, with its subcommands and groups.
|
|
3
3
|
// https://docs.discord.com/developers/interactions/application-commands#subcommands-and-subcommand-groups
|
|
4
4
|
import { ApplicationCommandOptionType, ApplicationCommandType, } from '../discord/types/application-command.js';
|
|
5
5
|
import { ApplicationIntegrationType } from '../discord/types/application.js';
|
|
6
6
|
import { InteractionContextType } from '../discord/types/interaction.js';
|
|
7
|
+
import { siteName } from '../loader/loader.js';
|
|
7
8
|
const TYPES = {
|
|
8
9
|
string: ApplicationCommandOptionType.String,
|
|
9
10
|
integer: ApplicationCommandOptionType.Integer,
|
|
@@ -76,8 +77,9 @@ function toOption(option, locales, autocomplete) {
|
|
|
76
77
|
/** `/mod ban` for `['mod', 'ban']`. */
|
|
77
78
|
export const commandName = (path) => `/${path.join(' ')}`;
|
|
78
79
|
/**
|
|
79
|
-
* Finds the
|
|
80
|
-
*
|
|
80
|
+
* Finds the declarations that can't be commands together, and leaves them
|
|
81
|
+
* out:
|
|
82
|
+
* - two commands with the same name;
|
|
81
83
|
* - a command that also has subcommands: Discord only lets people use the
|
|
82
84
|
* subcommands.
|
|
83
85
|
*/
|
|
@@ -85,7 +87,13 @@ export function findConflicts(entries) {
|
|
|
85
87
|
const conflicts = [];
|
|
86
88
|
const seen = new Map();
|
|
87
89
|
const unique = [...entries]
|
|
88
|
-
.sort((a, b) =>
|
|
90
|
+
.sort((a, b) => a.file === b.file
|
|
91
|
+
? a.export < b.export
|
|
92
|
+
? -1
|
|
93
|
+
: 1
|
|
94
|
+
: a.file < b.file
|
|
95
|
+
? -1
|
|
96
|
+
: 1)
|
|
89
97
|
.filter(entry => {
|
|
90
98
|
const key = entry.command.path.join(' ');
|
|
91
99
|
const first = seen.get(key);
|
|
@@ -95,7 +103,8 @@ export function findConflicts(entries) {
|
|
|
95
103
|
}
|
|
96
104
|
conflicts.push({
|
|
97
105
|
file: entry.file,
|
|
98
|
-
|
|
106
|
+
export: entry.export,
|
|
107
|
+
message: `${commandName(entry.command.path)} is already declared in ${siteName(first)}: two commands can't have the same name.`,
|
|
99
108
|
});
|
|
100
109
|
return false;
|
|
101
110
|
});
|
|
@@ -108,7 +117,8 @@ export function findConflicts(entries) {
|
|
|
108
117
|
return true;
|
|
109
118
|
conflicts.push({
|
|
110
119
|
file: entry.file,
|
|
111
|
-
|
|
120
|
+
export: entry.export,
|
|
121
|
+
message: `${commandName(path)} can't be a command and have subcommands (${siteName(child)} is ${commandName(child.command.path)}): Discord only lets people use the subcommands. Make this one a subcommand too, for example name: '${[...path, 'run'].join(' ')}'.`,
|
|
112
122
|
});
|
|
113
123
|
return false;
|
|
114
124
|
});
|
|
@@ -29,30 +29,29 @@ export interface ButtonLook<Data extends DataShape = DataShape> {
|
|
|
29
29
|
}
|
|
30
30
|
/** What `run` receives when the button is clicked. */
|
|
31
31
|
export type ButtonContext<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = InteractiveContext<Data> & TranslationContext & PlaceOf<Where, ComponentInteraction>;
|
|
32
|
-
/** What a
|
|
32
|
+
/** What a file gives to `button()`. */
|
|
33
33
|
export interface ButtonConfig<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> extends ButtonLook<Data>, InteractiveConfig<Data, Where> {
|
|
34
34
|
/** What to do when someone clicks the button. */
|
|
35
35
|
run: (context: ButtonContext<Data, Where>) => unknown;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* What `button()` returns:
|
|
39
|
-
* where you send a message, and call it with the data
|
|
40
|
-
* (`ban({ userId })`); a button without data is used as
|
|
41
|
-
* called with a look (`confirm({ disabled: true })`).
|
|
38
|
+
* What `button()` returns: a declaration the framework finds in the exports
|
|
39
|
+
* of a file. Import it where you send a message, and call it with the data
|
|
40
|
+
* the button carries (`ban({ userId })`); a button without data is used as
|
|
41
|
+
* is (`confirm`), or called with a look (`confirm({ disabled: true })`).
|
|
42
42
|
*/
|
|
43
|
-
export type
|
|
43
|
+
export type ButtonDeclaration<Data extends DataShape = {}> = {
|
|
44
44
|
/** What the file gave to `button()`, not checked yet. */
|
|
45
45
|
readonly config: unknown;
|
|
46
46
|
} & ({} extends Data ? ((look?: ButtonLook) => ButtonComponent) & ButtonComponent : {} extends DataInputOf<Data> ? ((data?: DataInputOf<Data>, look?: ButtonLook) => ButtonComponent) & ButtonComponent : (data: DataInputOf<Data>, look?: ButtonLook) => ButtonComponent);
|
|
47
47
|
/**
|
|
48
|
-
* Declares a button. Export the result
|
|
49
|
-
*
|
|
50
|
-
* button apart, so you never write an id.
|
|
48
|
+
* Declares a button. Export the result from any file of `src/`: the name
|
|
49
|
+
* of the export is what tells the button apart, so you never write an id.
|
|
51
50
|
*
|
|
52
51
|
* ```ts
|
|
53
52
|
* import { button } from 'chapterjs';
|
|
54
53
|
*
|
|
55
|
-
* export
|
|
54
|
+
* export const confirm = button({
|
|
56
55
|
* label: 'Confirm',
|
|
57
56
|
* style: 'success',
|
|
58
57
|
* async run({ interaction }) {
|
|
@@ -61,11 +60,12 @@ export type ButtonFile<Data extends DataShape = {}> = {
|
|
|
61
60
|
* });
|
|
62
61
|
* ```
|
|
63
62
|
*/
|
|
64
|
-
export declare function button<const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ButtonConfig<Data, Where>):
|
|
65
|
-
/** A button
|
|
63
|
+
export declare function button<const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ButtonConfig<Data, Where>): ButtonDeclaration<Data>;
|
|
64
|
+
/** A button, checked. */
|
|
66
65
|
export interface LoadedButton {
|
|
67
66
|
readonly kind: 'button';
|
|
68
|
-
|
|
67
|
+
/** The name of the export that declares it: its id for Discord. */
|
|
68
|
+
readonly name: string;
|
|
69
69
|
readonly look: Required<Pick<ButtonLook, 'style' | 'disabled'>> & Pick<ButtonLook, 'label' | 'emoji'>;
|
|
70
70
|
readonly data: DataShape;
|
|
71
71
|
readonly where: ComponentWhere;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// `button()`, as
|
|
2
|
-
//
|
|
3
|
-
// when it is clicked.
|
|
1
|
+
// `button()`, as user files import it from 'chapterjs': what the button
|
|
2
|
+
// looks like, what it carries, and what to do when it is clicked.
|
|
4
3
|
// https://docs.discord.com/developers/components/reference#button
|
|
5
4
|
import { Limits } from '../discord/api.js';
|
|
6
5
|
import { ButtonStyle, ComponentType, } from '../discord/types/component.js';
|
|
7
6
|
import { isDynamic, resolveText } from './component.js';
|
|
8
7
|
import { checkData, encodeCustomId, } from './custom-id.js';
|
|
9
|
-
import {
|
|
8
|
+
import { createComponent } from './declared.js';
|
|
10
9
|
import { emojiOf } from './instance.js';
|
|
11
10
|
export const BUTTON_STYLES = {
|
|
12
11
|
primary: ButtonStyle.Primary,
|
|
@@ -15,14 +14,13 @@ export const BUTTON_STYLES = {
|
|
|
15
14
|
danger: ButtonStyle.Danger,
|
|
16
15
|
};
|
|
17
16
|
/**
|
|
18
|
-
* Declares a button. Export the result
|
|
19
|
-
*
|
|
20
|
-
* button apart, so you never write an id.
|
|
17
|
+
* Declares a button. Export the result from any file of `src/`: the name
|
|
18
|
+
* of the export is what tells the button apart, so you never write an id.
|
|
21
19
|
*
|
|
22
20
|
* ```ts
|
|
23
21
|
* import { button } from 'chapterjs';
|
|
24
22
|
*
|
|
25
|
-
* export
|
|
23
|
+
* export const confirm = button({
|
|
26
24
|
* label: 'Confirm',
|
|
27
25
|
* style: 'success',
|
|
28
26
|
* async run({ interaction }) {
|
|
@@ -32,7 +30,7 @@ export const BUTTON_STYLES = {
|
|
|
32
30
|
* ```
|
|
33
31
|
*/
|
|
34
32
|
export function button(config) {
|
|
35
|
-
return
|
|
33
|
+
return createComponent('button', config, {
|
|
36
34
|
asPiece: true,
|
|
37
35
|
pieceKind: 'button',
|
|
38
36
|
});
|
|
@@ -74,7 +72,7 @@ export function readLook(what, look) {
|
|
|
74
72
|
/** The button as Discord takes it, for the data and the look given. */
|
|
75
73
|
export function renderButton(loaded, args) {
|
|
76
74
|
const hasData = Object.keys(loaded.data).length > 0;
|
|
77
|
-
const name = loaded
|
|
75
|
+
const { name } = loaded;
|
|
78
76
|
if (args.length > (hasData ? 2 : 1)) {
|
|
79
77
|
fail(`${name} takes ${hasData ? 'its data, then a look' : 'a look'} at most: ${hasData ? `${name}({ ... }, { disabled: true })` : `${name}({ disabled: true })`}.`);
|
|
80
78
|
}
|
|
@@ -86,7 +84,7 @@ export function renderButton(loaded, args) {
|
|
|
86
84
|
const base = {
|
|
87
85
|
type: ComponentType.Button,
|
|
88
86
|
style: BUTTON_STYLES[look.style],
|
|
89
|
-
custom_id: encodeCustomId(loaded.
|
|
87
|
+
custom_id: encodeCustomId(loaded.name, loaded.data, values),
|
|
90
88
|
};
|
|
91
89
|
if (look.emoji !== undefined)
|
|
92
90
|
base.emoji = emojiOf(look.emoji, name);
|
|
@@ -12,7 +12,7 @@ import type { Translator } from '../messages/messages.js';
|
|
|
12
12
|
* component carries.
|
|
13
13
|
*/
|
|
14
14
|
export interface TextContext<Data extends DataShape = DataShape> {
|
|
15
|
-
/** The texts of
|
|
15
|
+
/** The texts of the language files, in the language of who will read the message. */
|
|
16
16
|
t: Translator;
|
|
17
17
|
/** What the component carries, as declared in `data`. */
|
|
18
18
|
data: [DataShape] extends [Data] ? Record<string, string | number | boolean> : DataValuesOf<Data>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What every interactive component
|
|
1
|
+
// What every interactive component (button, menu, form) shares: where it
|
|
2
2
|
// works, who may use it, what it carries, and what its `run` receives
|
|
3
3
|
// about the place it was used in.
|
|
4
4
|
/** Whether a text is computed when the message is sent. */
|
|
@@ -53,14 +53,14 @@ export declare const needsNoData: (shape: DataShape) => boolean;
|
|
|
53
53
|
/** Checks the values a file gives to a component against its `data`. */
|
|
54
54
|
export declare function checkData(what: string, shape: DataShape, values: unknown): Record<string, string | number | boolean>;
|
|
55
55
|
/**
|
|
56
|
-
* The `custom_id` of a component: its
|
|
56
|
+
* The `custom_id` of a component: its name, then its values in the order
|
|
57
57
|
* of the declaration. Discord accepts 100 characters: when the data does
|
|
58
58
|
* not fit, the error says by how much.
|
|
59
59
|
*/
|
|
60
|
-
export declare function encodeCustomId(
|
|
61
|
-
/** The
|
|
60
|
+
export declare function encodeCustomId(name: string, shape: DataShape, values: Record<string, string | number | boolean>): string;
|
|
61
|
+
/** The name and the encoded values of a `custom_id` the framework wrote. */
|
|
62
62
|
export declare function decodeCustomId(id: string): {
|
|
63
|
-
|
|
63
|
+
name: string;
|
|
64
64
|
parts: string[];
|
|
65
65
|
};
|
|
66
66
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The `custom_id` of a component is never written by users: it is the
|
|
2
|
-
// of the
|
|
3
|
-
// component carries, encoded by the framework. Stateless by design: it
|
|
1
|
+
// The `custom_id` of a component is never written by users: it is the name
|
|
2
|
+
// of the export that declares the component (`ban`), followed by the data
|
|
3
|
+
// the component carries, encoded by the framework. Stateless by design: it
|
|
4
4
|
// survives a restart, and every process of the bot reads it the same way.
|
|
5
5
|
// https://docs.discord.com/developers/components/reference#anatomy-of-a-component-custom-id
|
|
6
6
|
import { Limits } from '../discord/api.js';
|
|
@@ -76,24 +76,24 @@ export function checkData(what, shape, values) {
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* The `custom_id` of a component: its
|
|
79
|
+
* The `custom_id` of a component: its name, then its values in the order
|
|
80
80
|
* of the declaration. Discord accepts 100 characters: when the data does
|
|
81
81
|
* not fit, the error says by how much.
|
|
82
82
|
*/
|
|
83
|
-
export function encodeCustomId(
|
|
84
|
-
const parts = [
|
|
85
|
-
for (const
|
|
86
|
-
parts.push(escape(String(values[
|
|
83
|
+
export function encodeCustomId(name, shape, values) {
|
|
84
|
+
const parts = [name];
|
|
85
|
+
for (const field of Object.keys(shape))
|
|
86
|
+
parts.push(escape(String(values[field])));
|
|
87
87
|
const id = parts.join(SEPARATOR);
|
|
88
88
|
if (id.length > Limits.CustomId) {
|
|
89
|
-
throw new RangeError(`The data of ${
|
|
89
|
+
throw new RangeError(`The data of ${name} is ${id.length - name.length - 1} characters long once encoded, and Discord leaves ${Limits.CustomId - name.length - 1} for it (its id is ${name}, ${name.length} characters, out of ${Limits.CustomId}). Carry less: an id instead of a name, or a shorter export name.`);
|
|
90
90
|
}
|
|
91
91
|
return id;
|
|
92
92
|
}
|
|
93
|
-
/** The
|
|
93
|
+
/** The name and the encoded values of a `custom_id` the framework wrote. */
|
|
94
94
|
export function decodeCustomId(id) {
|
|
95
|
-
const [
|
|
96
|
-
return {
|
|
95
|
+
const [name = '', ...parts] = split(id);
|
|
96
|
+
return { name, parts };
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
99
|
* The values a `custom_id` carries, typed as the component declares them.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Declaration } from '../loader/loader.js';
|
|
2
|
+
import { type LoadedButton } from './button.js';
|
|
3
|
+
import { isEmbedDeclaration } from './embed.js';
|
|
4
|
+
import { type ComponentKind } from './declared.js';
|
|
5
|
+
import { type LoadedModal } from './modal.js';
|
|
6
|
+
import { type LoadedSelect } from './select.js';
|
|
7
|
+
/** An embed, checked (when it is static). */
|
|
8
|
+
export interface LoadedEmbed {
|
|
9
|
+
readonly kind: 'embed';
|
|
10
|
+
/** The name of the export that declares it. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
/** A component, checked: what the router works with. */
|
|
14
|
+
export type LoadedComponent = LoadedButton | LoadedSelect | LoadedModal | LoadedEmbed;
|
|
15
|
+
/** What each kind is called in messages. */
|
|
16
|
+
export declare const WHAT: Record<ComponentKind, string>;
|
|
17
|
+
/**
|
|
18
|
+
* A component (a button, a select menu, a form or an embed), declared
|
|
19
|
+
* anywhere in `src/` with `button()`, `select()`, `modal()` or `embed()`,
|
|
20
|
+
* and exported: the name of the export is its id, so a file may declare
|
|
21
|
+
* several, each under its own export.
|
|
22
|
+
*/
|
|
23
|
+
export declare const componentDeclaration: Declaration<LoadedComponent>;
|
|
24
|
+
/** Two components of the same kind with the same name: the later one is left out. */
|
|
25
|
+
export declare function findDuplicates<T extends {
|
|
26
|
+
file: string;
|
|
27
|
+
export: string;
|
|
28
|
+
component: {
|
|
29
|
+
kind: ComponentKind;
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
}>(entries: readonly T[]): {
|
|
33
|
+
valid: T[];
|
|
34
|
+
conflicts: {
|
|
35
|
+
file: string;
|
|
36
|
+
export: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
/** Whether a declaration is an embed: nothing to route. */
|
|
41
|
+
export declare const isEmbedLoaded: (component: LoadedComponent) => component is LoadedEmbed;
|
|
42
|
+
export { isEmbedDeclaration };
|