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.
Files changed (82) hide show
  1. package/dist/cli/build.js +9 -10
  2. package/dist/cli/dev.js +29 -16
  3. package/dist/cli/project.d.ts +38 -25
  4. package/dist/cli/project.js +199 -134
  5. package/dist/cli/update-check.d.ts +42 -0
  6. package/dist/cli/update-check.js +125 -0
  7. package/dist/commands/command.d.ts +25 -11
  8. package/dist/commands/command.js +5 -5
  9. package/dist/commands/{convention.d.ts → declaration.d.ts} +24 -11
  10. package/dist/commands/{convention.js → declaration.js} +61 -33
  11. package/dist/commands/tree.d.ts +8 -4
  12. package/dist/commands/tree.js +16 -6
  13. package/dist/components/button.d.ts +13 -13
  14. package/dist/components/button.js +9 -11
  15. package/dist/components/component.d.ts +1 -1
  16. package/dist/components/component.js +1 -1
  17. package/dist/components/custom-id.d.ts +4 -4
  18. package/dist/components/custom-id.js +12 -12
  19. package/dist/components/declaration.d.ts +42 -0
  20. package/dist/components/{convention.js → declaration.js} +76 -73
  21. package/dist/components/declared.d.ts +29 -0
  22. package/dist/components/declared.js +49 -0
  23. package/dist/components/embed.d.ts +13 -12
  24. package/dist/components/embed.js +6 -6
  25. package/dist/components/instance.d.ts +3 -3
  26. package/dist/components/instance.js +2 -2
  27. package/dist/components/modal.d.ts +11 -10
  28. package/dist/components/modal.js +7 -8
  29. package/dist/components/router.d.ts +3 -2
  30. package/dist/components/router.js +14 -19
  31. package/dist/components/select.d.ts +14 -12
  32. package/dist/components/select.js +4 -4
  33. package/dist/events/declaration.d.ts +16 -0
  34. package/dist/events/declaration.js +80 -0
  35. package/dist/events/event.d.ts +42 -5
  36. package/dist/events/event.js +25 -11
  37. package/dist/events/registry.d.ts +1 -10
  38. package/dist/events/router.d.ts +3 -2
  39. package/dist/index.d.ts +11 -9
  40. package/dist/index.js +2 -0
  41. package/dist/loader/generated.d.ts +5 -32
  42. package/dist/loader/generated.js +21 -55
  43. package/dist/loader/loader.d.ts +57 -44
  44. package/dist/loader/loader.js +73 -89
  45. package/dist/main.d.ts +0 -1
  46. package/dist/main.js +2 -3
  47. package/dist/messages/commands.d.ts +3 -3
  48. package/dist/messages/commands.js +29 -10
  49. package/dist/messages/declaration.d.ts +89 -0
  50. package/dist/messages/declaration.js +455 -0
  51. package/dist/messages/messages.d.ts +65 -43
  52. package/dist/messages/messages.js +11 -10
  53. package/dist/messages/phrases.js +1 -1
  54. package/dist/messages/translate.d.ts +4 -4
  55. package/dist/messages/translate.js +4 -4
  56. package/dist/presence/declaration.d.ts +19 -0
  57. package/dist/presence/{convention.js → declaration.js} +13 -28
  58. package/dist/presence/presence.d.ts +7 -7
  59. package/dist/presence/presence.js +5 -5
  60. package/dist/structures/context.d.ts +1 -1
  61. package/dist/structures/interaction.d.ts +2 -2
  62. package/dist/structures/interaction.js +3 -3
  63. package/dist/structures/payload.d.ts +4 -4
  64. package/dist/structures/payload.js +4 -4
  65. package/dist/tasks/declaration.d.ts +16 -0
  66. package/dist/tasks/{convention.js → declaration.js} +14 -36
  67. package/dist/tasks/scheduler.d.ts +3 -1
  68. package/dist/tasks/scheduler.js +4 -4
  69. package/dist/tasks/task.d.ts +8 -9
  70. package/dist/tasks/task.js +6 -7
  71. package/package.json +1 -1
  72. package/dist/components/convention.d.ts +0 -35
  73. package/dist/components/file.d.ts +0 -30
  74. package/dist/components/file.js +0 -52
  75. package/dist/events/convention.d.ts +0 -25
  76. package/dist/events/convention.js +0 -97
  77. package/dist/events/types.d.ts +0 -7
  78. package/dist/events/types.js +0 -48
  79. package/dist/messages/convention.d.ts +0 -89
  80. package/dist/messages/convention.js +0 -347
  81. package/dist/presence/convention.d.ts +0 -26
  82. 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 `src/messages/`.
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
- /** What a command file gives to `command()`: what the command is and does. */
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 `src/messages/`. One or the other.
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 default export of a command file. */
323
- export interface CommandFile<Options extends CommandOptions = CommandOptions, Where extends CommandWhere = CommandWhere> {
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 as the default export of a
329
- * file of `src/commands/`: the path of the file is the name of the command
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 default command({
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>): CommandFile<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 isCommandFile(value: unknown): value is CommandFile;
359
+ export declare function isCommandDeclaration(value: unknown): value is CommandDeclaration;
346
360
  export {};
@@ -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 as the default export of a
6
- * file of `src/commands/`: the path of the file is the name of the command
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 default command({
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 isCommandFile(value) {
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 { Convention } from '../loader/loader.js';
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 file, checked: what the rest of the feature works with. */
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 `src/commands/mod/ban.ts`: `/mod ban`. */
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 of `src/messages/` do: `description`
33
- * and the descriptions of the options are empty until `project.ts`
34
- * fills them in from the default language.
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
- * `src/commands/`: each file is a slash command, named after its path.
60
- * One folder makes a command with subcommands (`mod/ban.ts` is `/mod ban`),
61
- * two make a group (`mod/roles/add.ts` is `/mod roles add`): Discord allows
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 const commandsConvention: Convention<LoadedCommand>;
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
- // `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 };