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.
Files changed (80) hide show
  1. package/dist/cli/build.js +9 -10
  2. package/dist/cli/dev.js +17 -16
  3. package/dist/cli/project.d.ts +38 -25
  4. package/dist/cli/project.js +199 -134
  5. package/dist/commands/command.d.ts +25 -11
  6. package/dist/commands/command.js +5 -5
  7. package/dist/commands/{convention.d.ts → declaration.d.ts} +24 -11
  8. package/dist/commands/{convention.js → declaration.js} +61 -33
  9. package/dist/commands/tree.d.ts +8 -4
  10. package/dist/commands/tree.js +16 -6
  11. package/dist/components/button.d.ts +13 -13
  12. package/dist/components/button.js +9 -11
  13. package/dist/components/component.d.ts +1 -1
  14. package/dist/components/component.js +1 -1
  15. package/dist/components/custom-id.d.ts +4 -4
  16. package/dist/components/custom-id.js +12 -12
  17. package/dist/components/declaration.d.ts +42 -0
  18. package/dist/components/{convention.js → declaration.js} +76 -73
  19. package/dist/components/declared.d.ts +29 -0
  20. package/dist/components/declared.js +49 -0
  21. package/dist/components/embed.d.ts +13 -12
  22. package/dist/components/embed.js +6 -6
  23. package/dist/components/instance.d.ts +3 -3
  24. package/dist/components/instance.js +2 -2
  25. package/dist/components/modal.d.ts +11 -10
  26. package/dist/components/modal.js +7 -8
  27. package/dist/components/router.d.ts +3 -2
  28. package/dist/components/router.js +14 -19
  29. package/dist/components/select.d.ts +14 -12
  30. package/dist/components/select.js +4 -4
  31. package/dist/events/declaration.d.ts +16 -0
  32. package/dist/events/declaration.js +80 -0
  33. package/dist/events/event.d.ts +42 -5
  34. package/dist/events/event.js +25 -11
  35. package/dist/events/registry.d.ts +1 -10
  36. package/dist/events/router.d.ts +3 -2
  37. package/dist/index.d.ts +11 -9
  38. package/dist/index.js +2 -0
  39. package/dist/loader/generated.d.ts +5 -32
  40. package/dist/loader/generated.js +21 -55
  41. package/dist/loader/loader.d.ts +57 -44
  42. package/dist/loader/loader.js +73 -89
  43. package/dist/main.d.ts +0 -1
  44. package/dist/main.js +2 -3
  45. package/dist/messages/commands.d.ts +3 -3
  46. package/dist/messages/commands.js +29 -10
  47. package/dist/messages/declaration.d.ts +89 -0
  48. package/dist/messages/declaration.js +455 -0
  49. package/dist/messages/messages.d.ts +65 -43
  50. package/dist/messages/messages.js +11 -10
  51. package/dist/messages/phrases.js +1 -1
  52. package/dist/messages/translate.d.ts +4 -4
  53. package/dist/messages/translate.js +4 -4
  54. package/dist/presence/declaration.d.ts +19 -0
  55. package/dist/presence/{convention.js → declaration.js} +13 -28
  56. package/dist/presence/presence.d.ts +7 -7
  57. package/dist/presence/presence.js +5 -5
  58. package/dist/structures/context.d.ts +1 -1
  59. package/dist/structures/interaction.d.ts +2 -2
  60. package/dist/structures/interaction.js +3 -3
  61. package/dist/structures/payload.d.ts +4 -4
  62. package/dist/structures/payload.js +4 -4
  63. package/dist/tasks/declaration.d.ts +16 -0
  64. package/dist/tasks/{convention.js → declaration.js} +14 -36
  65. package/dist/tasks/scheduler.d.ts +3 -1
  66. package/dist/tasks/scheduler.js +4 -4
  67. package/dist/tasks/task.d.ts +8 -9
  68. package/dist/tasks/task.js +6 -7
  69. package/package.json +1 -1
  70. package/dist/components/convention.d.ts +0 -35
  71. package/dist/components/file.d.ts +0 -30
  72. package/dist/components/file.js +0 -52
  73. package/dist/events/convention.d.ts +0 -25
  74. package/dist/events/convention.js +0 -97
  75. package/dist/events/types.d.ts +0 -7
  76. package/dist/events/types.js +0 -48
  77. package/dist/messages/convention.d.ts +0 -89
  78. package/dist/messages/convention.js +0 -347
  79. package/dist/presence/convention.d.ts +0 -26
  80. package/dist/tasks/convention.d.ts +0 -26
@@ -1,14 +1,15 @@
1
- // `src/commands/`: the path of a file is the name of a slash command.
2
- // Everything a file declares is checked when it loads, against the limits
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 { isCommandFile, } from './command.js';
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
- * `src/commands/`: each file is a slash command, named after its path.
393
- * One folder makes a command with subcommands (`mod/ban.ts` is `/mod ban`),
394
- * two make a group (`mod/roles/add.ts` is `/mod roles add`): Discord allows
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 const commandsConvention = {
398
- folder: 'commands',
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
- check(path) {
402
- const parts = path.replace(/\.[^./]+$/, '').split('/');
403
- if (parts.length > 3) {
404
- fail(`This file is ${parts.length - 1} folders deep: Discord allows a command, a group and a subcommand, so 2 folders at most (src/commands/mod/roles/add.ts is /mod roles add).`);
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: command({ description: '...', run({ interaction }) { ... } })`);
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 of src/messages/: put its "locales" there too (commands: { ... }), or give it a description here.`);
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
+ }
@@ -1,21 +1,25 @@
1
1
  import { type BulkOverwriteGuildApplicationCommandsJSONParams } from '../discord/types/application-command.js';
2
- import type { LoadedCommand } from './convention.js';
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 files that can't both be commands. */
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 files that can't be commands together, and leaves them out:
18
- * - two files with the same name (in different `(group)` folders);
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
  */
@@ -1,9 +1,10 @@
1
- // Turns the command files of a project into what Discord registers: one
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 files that can't be commands together, and leaves them out:
80
- * - two files with the same name (in different `(group)` folders);
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) => (a.file < b.file ? -1 : 1))
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
- message: `${commandName(entry.command.path)} is already ${first.file}: two files can't be the same command. A folder in parentheses only groups files, it is not part of the name.`,
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
- message: `${commandName(path)} can't be a command and have subcommands (${child.file} is ${commandName(child.command.path)}): Discord only lets people use the subcommands. Move this file into the folder, for example as ${[...path, 'run'].join('/')}.ts.`,
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 button file gives to `button()`. */
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: the default export of a button file. Import it
39
- * where you send a message, and call it with the data the button carries
40
- * (`ban({ userId })`); a button without data is used as is (`confirm`), or
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 ButtonFile<Data extends DataShape = {}> = {
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 as the default export of a file of
49
- * `src/components/buttons/`: the path of the file is what tells the
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 default button({
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>): ButtonFile<Data>;
65
- /** A button file, checked. */
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
- readonly path: string;
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 the files of `src/components/buttons/` import it from
2
- // 'chapterjs': what the button looks like, what it carries, and what to do
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 { createFile } from './file.js';
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 as the default export of a file of
19
- * `src/components/buttons/`: the path of the file is what tells the
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 default button({
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 createFile('button', config, {
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.path.split('/').pop();
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.path, loaded.data, values),
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 `src/messages/`, in the language of who will read the message. */
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 of `src/components/` shares: where it
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 path, then its values in the order
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(path: string, shape: DataShape, values: Record<string, string | number | boolean>): string;
61
- /** The path and the encoded values of a `custom_id` the framework wrote. */
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
- path: string;
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 path
2
- // of the file of the component (`buttons/ban`), followed by the data 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 path, then its values in the order
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(path, shape, values) {
84
- const parts = [path];
85
- for (const name of Object.keys(shape))
86
- parts.push(escape(String(values[name])));
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 ${path} is ${id.length - path.length - 1} characters long once encoded, and Discord leaves ${Limits.CustomId - path.length - 1} for it (its id is ${path}, ${path.length} characters, out of ${Limits.CustomId}). Carry less: an id instead of a name, or a shorter file name.`);
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 path and the encoded values of a `custom_id` the framework wrote. */
93
+ /** The name and the encoded values of a `custom_id` the framework wrote. */
94
94
  export function decodeCustomId(id) {
95
- const [path = '', ...parts] = split(id);
96
- return { path, parts };
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 };