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,15 +1,15 @@
1
- // `src/components/`: the folder is the kind of component, the path of the
2
- // file is its id. Everything a file declares is checked when it loads,
3
- // against the limits of Discord, so a mistake is explained before Discord
4
- // refuses it; the file is then bound to its path, which is what makes its
1
+ // What a file declares with `button()`, `select()`, `modal()` and
2
+ // `embed()`, checked when it loads against the limits of Discord, so a
3
+ // mistake is explained before Discord refuses it; the component is then
4
+ // bound to the name of its export, which is its id and what makes its
5
5
  // instances.
6
6
  import { Limits } from '../discord/api.js';
7
7
  import { checkEmbed } from '../structures/payload.js';
8
8
  import { isDynamic } from './component.js';
9
9
  import { readLook, renderButton, } from './button.js';
10
10
  import { DATA_KINDS } from './custom-id.js';
11
- import { isEmbedFile } from './embed.js';
12
- import { bindFile, COMPONENT_FOLDERS, fileStateOf, } from './file.js';
11
+ import { isEmbedDeclaration } from './embed.js';
12
+ import { bindComponent, componentStateOf, } from './declared.js';
13
13
  import { readField, renderModal } from './modal.js';
14
14
  import { readOptions } from './options.js';
15
15
  import { readChannelTypes, readPlaceholder, readRange, renderSelect, SELECT_TYPES, } from './select.js';
@@ -17,9 +17,12 @@ const fail = (message) => {
17
17
  throw new TypeError(message);
18
18
  };
19
19
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
20
- /** The names of the folders and files: what a path and an id can hold. */
21
- const NAME = /^[A-Za-z0-9_-]+$/;
22
- const FOLDERS = Object.keys(COMPONENT_FOLDERS);
20
+ /**
21
+ * What the name of a component can hold: the name of an export (or of a
22
+ * file, for a default export), as long as a custom_id can carry it beside
23
+ * the data: no `:` (what separates the data), and room left for the data.
24
+ */
25
+ const NAME = /^[^:\s]+$/;
23
26
  function checkKeys(what, value, allowed) {
24
27
  for (const key of Object.keys(value)) {
25
28
  if (!allowed.includes(key)) {
@@ -90,7 +93,7 @@ function checkRun(what, value, example) {
90
93
  fail(`${what} has no "run": the function to run when someone uses it, like ${example}`);
91
94
  }
92
95
  }
93
- function readButton(path, config) {
96
+ function readButton(name, config) {
94
97
  if (!isRecord(config)) {
95
98
  return fail(`button() needs an object: button({ label: '...', run({ interaction }) { ... } })`);
96
99
  }
@@ -118,7 +121,7 @@ function readButton(path, config) {
118
121
  checkRun(what, config.run, `async run({ interaction }) { await interaction.update('Done!'); }`);
119
122
  return Object.freeze({
120
123
  kind: 'button',
121
- path,
124
+ name,
122
125
  look: {
123
126
  style: look.style ?? 'primary',
124
127
  disabled: look.disabled ?? false,
@@ -132,7 +135,7 @@ function readButton(path, config) {
132
135
  run: config.run,
133
136
  });
134
137
  }
135
- function readSelect(path, config) {
138
+ function readSelect(name, config) {
136
139
  if (!isRecord(config)) {
137
140
  return fail(`select() needs an object: select({ options: ['a', 'b'], run({ interaction, value }) { ... } })`);
138
141
  }
@@ -168,7 +171,7 @@ function readSelect(path, config) {
168
171
  checkRun(what, config.run, `async run({ interaction, values }) { ... }`);
169
172
  return Object.freeze({
170
173
  kind: 'select',
171
- path,
174
+ name,
172
175
  type: type,
173
176
  options: Object.freeze(options),
174
177
  channelTypes: readChannelTypes(what, config.channelTypes),
@@ -186,7 +189,7 @@ function readSelect(path, config) {
186
189
  run: config.run,
187
190
  });
188
191
  }
189
- function readModal(path, config) {
192
+ function readModal(name, config) {
190
193
  if (!isRecord(config)) {
191
194
  return fail(`modal() needs an object: modal({ title: '...', fields: { ... }, run({ interaction, fields }) { ... } })`);
192
195
  }
@@ -219,7 +222,7 @@ function readModal(path, config) {
219
222
  checkRun(what, config.run, `async run({ interaction, fields }) { ... }`);
220
223
  return Object.freeze({
221
224
  kind: 'modal',
222
- path,
225
+ name,
223
226
  title: config.title,
224
227
  fields: Object.freeze(fields),
225
228
  data: checkDataShape(what, config.data),
@@ -228,34 +231,38 @@ function readModal(path, config) {
228
231
  run: config.run,
229
232
  });
230
233
  }
231
- /** A file of `src/components/`, by the kind its folder says. */
232
- function readOne(kind, path, exports) {
233
- const file = exports.default;
234
- const state = fileStateOf(file);
235
- const example = `import { ${kind} } from 'chapterjs'; export default ${kind}({ ... })`;
236
- if (!state) {
237
- return fail('default' in exports
238
- ? `The default export of this file must be what ${kind}() returns: ${example}`
239
- : `This file has no default export. It should look like: ${example}`);
234
+ /** One component, by the kind it was declared with. */
235
+ function readOne(state, declared, { name, file, export: exported }) {
236
+ const what = `the ${WHAT[state.kind]} ${name}`;
237
+ if (!NAME.test(name)) {
238
+ fail(`"${name}" can't be the name of a ${WHAT[state.kind]}: the name of the export is its id, and an id has no space and no ":". Rename the export.`);
240
239
  }
241
- if (state.kind !== kind) {
242
- const folder = FOLDERS.find(one => COMPONENT_FOLDERS[one] === state.kind);
243
- return fail(`This file exports a ${state.kind}, but it is in src/components/${FOLDERS.find(one => COMPONENT_FOLDERS[one] === kind)}/. Move it to src/components/${folder}/.`);
240
+ if (name.length > Limits.CustomId - 20) {
241
+ fail(`The name of ${what} is ${name.length} characters long: Discord gives ${Limits.CustomId} characters to the id of a component and its data together. Rename the export${exported === 'default' ? ` or the file ${file}` : ''} with something shorter.`);
244
242
  }
245
- switch (kind) {
243
+ switch (state.kind) {
246
244
  case 'button': {
247
- const loaded = readButton(path, state.config);
248
- bindFile(file, { path, render: args => renderButton(loaded, args) });
245
+ const loaded = readButton(name, state.config);
246
+ bindComponent(declared, {
247
+ name,
248
+ render: args => renderButton(loaded, args),
249
+ });
249
250
  return loaded;
250
251
  }
251
252
  case 'select': {
252
- const loaded = readSelect(path, state.config);
253
- bindFile(file, { path, render: args => renderSelect(loaded, args) });
253
+ const loaded = readSelect(name, state.config);
254
+ bindComponent(declared, {
255
+ name,
256
+ render: args => renderSelect(loaded, args),
257
+ });
254
258
  return loaded;
255
259
  }
256
260
  case 'modal': {
257
- const loaded = readModal(path, state.config);
258
- bindFile(file, { path, render: args => renderModal(loaded, args) });
261
+ const loaded = readModal(name, state.config);
262
+ bindComponent(declared, {
263
+ name,
264
+ render: args => renderModal(loaded, args),
265
+ });
259
266
  return loaded;
260
267
  }
261
268
  default: {
@@ -266,74 +273,70 @@ function readOne(kind, path, exports) {
266
273
  }
267
274
  checkEmbed(config, 'this embed');
268
275
  }
269
- bindFile(file, {
270
- path,
276
+ bindComponent(declared, {
277
+ name,
271
278
  render: args => {
272
279
  if (typeof config === 'function')
273
280
  return config(...args);
274
281
  if (args.length > 0)
275
- fail(`${path.split('/').pop()} always looks the same: it takes no arguments.`);
282
+ fail(`${name} always looks the same: it takes no arguments.`);
276
283
  return config;
277
284
  },
278
285
  });
279
- return Object.freeze({ kind: 'embed', path });
286
+ return Object.freeze({ kind: 'embed', name });
280
287
  }
281
288
  }
282
289
  }
290
+ /** What each kind is called in messages. */
291
+ export const WHAT = {
292
+ button: 'button',
293
+ select: 'select menu',
294
+ modal: 'form',
295
+ embed: 'embed',
296
+ };
283
297
  /**
284
- * `src/components/`: one folder per kind of component (`buttons/`,
285
- * `selects/`, `modals/`, `embeds/`), and in it one file per component,
286
- * whose path is its id. The file exports by default what `button()`,
287
- * `select()`, `modal()` or `embed()` returns.
298
+ * A component (a button, a select menu, a form or an embed), declared
299
+ * anywhere in `src/` with `button()`, `select()`, `modal()` or `embed()`,
300
+ * and exported: the name of the export is its id, so a file may declare
301
+ * several, each under its own export.
288
302
  */
289
- export const componentsConvention = {
290
- folder: 'components',
303
+ export const componentDeclaration = {
291
304
  one: 'component',
292
305
  many: 'components',
293
- check(path) {
294
- const parts = path.replace(/\.[^./]+$/, '').split('/');
295
- if (parts.length < 2) {
296
- fail(`This file is directly in src/components/. Put it in the folder of its kind: ${FOLDERS.map(one => `src/components/${one}/`).join(', ')}.`);
297
- }
298
- const folder = parts[0];
299
- if (!Object.hasOwn(COMPONENT_FOLDERS, folder)) {
300
- fail(`The folder src/components/${folder} is not a kind of component. Kinds are: ${FOLDERS.join(', ')}.`);
301
- }
302
- for (const part of parts) {
303
- if (!NAME.test(part)) {
304
- fail(`"${part}" can't be in the path of a component: use letters, digits, - and _ only. The path of the file is the id of the component.`);
305
- }
306
- }
307
- if (path.replace(/\.[^./]+$/, '').length > Limits.CustomId - 20) {
308
- fail(`The path of this file is ${path.length} characters long: Discord gives ${Limits.CustomId} characters to the id of a component and its data together. Shorten the names.`);
309
- }
310
- },
311
- read(exports, path) {
312
- const id = path.replace(/\.[^./]+$/, '');
313
- const kind = COMPONENT_FOLDERS[id.split('/')[0]];
314
- return readOne(kind, id, exports);
306
+ is: value => componentStateOf(value) !== undefined,
307
+ list: false,
308
+ read(value, site) {
309
+ return readOne(componentStateOf(value), value, site);
315
310
  },
316
311
  };
317
- /** The files that are the same component (through `(group)` folders): the later one is left out. */
312
+ /** Two components of the same kind with the same name: the later one is left out. */
318
313
  export function findDuplicates(entries) {
319
314
  const seen = new Map();
320
315
  const conflicts = [];
321
316
  const valid = [...entries]
322
- .sort((a, b) => (a.file < b.file ? -1 : 1))
317
+ .sort((a, b) => a.file === b.file
318
+ ? a.export < b.export
319
+ ? -1
320
+ : 1
321
+ : a.file < b.file
322
+ ? -1
323
+ : 1)
323
324
  .filter(entry => {
324
- const first = seen.get(entry.component.path);
325
+ const key = `${entry.component.kind}:${entry.component.name}`;
326
+ const first = seen.get(key);
325
327
  if (!first) {
326
- seen.set(entry.component.path, entry);
328
+ seen.set(key, entry);
327
329
  return true;
328
330
  }
329
331
  conflicts.push({
330
332
  file: entry.file,
331
- message: `${entry.component.path} is already ${first.file}: two files can't be the same component. A folder in parentheses only groups files, it is not part of the id.`,
333
+ export: entry.export,
334
+ message: `There is already a ${WHAT[entry.component.kind]} named ${entry.component.name}, in ${first.file}: the name of the export is what tells a ${WHAT[entry.component.kind]} apart, so rename one of them.`,
332
335
  });
333
336
  return false;
334
337
  });
335
338
  return { valid, conflicts };
336
339
  }
337
- /** Whether a file holds an embed: nothing to route. */
340
+ /** Whether a declaration is an embed: nothing to route. */
338
341
  export const isEmbedLoaded = (component) => component.kind === 'embed';
339
- export { isEmbedFile };
342
+ export { isEmbedDeclaration };
@@ -0,0 +1,29 @@
1
+ import { type PieceKind, type Rendered } from './instance.js';
2
+ /** The kinds of components a file declares. */
3
+ export type ComponentKind = 'button' | 'select' | 'modal' | 'embed';
4
+ /** A declared component as the loader knows it: what it declared, and once loaded, how to make instances. */
5
+ export interface ComponentState {
6
+ readonly kind: ComponentKind;
7
+ readonly config: unknown;
8
+ /** What made it, for messages: `button()`. */
9
+ readonly made: string;
10
+ bound?: {
11
+ /** The name of the export that declares it: its id. */
12
+ name: string;
13
+ /** Makes an instance from what the component was called with. */
14
+ render: (args: readonly unknown[]) => Rendered | object;
15
+ };
16
+ }
17
+ /** The state of a declared component, if the value is one. */
18
+ export declare function componentStateOf(value: unknown): ComponentState | undefined;
19
+ /**
20
+ * Makes a declared component. `asPiece` says whether it stands for its
21
+ * instance by itself when it carries no data (a button, a menu, a modal:
22
+ * not an embed, which is only a function).
23
+ */
24
+ export declare function createComponent(kind: ComponentKind, config: unknown, { asPiece, pieceKind }: {
25
+ asPiece: boolean;
26
+ pieceKind?: PieceKind;
27
+ }): (...args: unknown[]) => unknown;
28
+ /** Binds a declared component to its name and to what makes its instances. */
29
+ export declare function bindComponent(declared: unknown, bound: NonNullable<ComponentState['bound']>): void;
@@ -0,0 +1,49 @@
1
+ // What `button()`, `select()`, `modal()` and `embed()` return: a component,
2
+ // declared in a file of the project. It is a function (called with the
3
+ // data the component carries) that the loader recognises, checks, and binds
4
+ // to the name of its export: that name is the custom_id. Until then it can't
5
+ // make anything, which is how a component the framework never loaded is
6
+ // refused.
7
+ import { piece, pieceFunction, } from './instance.js';
8
+ const STATE = Symbol.for('chapterjs.component.declared');
9
+ /** The state of a declared component, if the value is one. */
10
+ export function componentStateOf(value) {
11
+ if (typeof value !== 'function')
12
+ return undefined;
13
+ return value[STATE];
14
+ }
15
+ /**
16
+ * Makes a declared component. `asPiece` says whether it stands for its
17
+ * instance by itself when it carries no data (a button, a menu, a modal:
18
+ * not an embed, which is only a function).
19
+ */
20
+ export function createComponent(kind, config, { asPiece, pieceKind }) {
21
+ const state = { kind, config, made: `${kind}()` };
22
+ const notBound = () => {
23
+ throw new Error(`This ${kind} was not loaded by ChapterJS: a ${kind} is exported from a file of src/ (export const name = ${kind}({ ... })), and used once the bot started.`);
24
+ };
25
+ const declared = (...args) => {
26
+ if (!state.bound)
27
+ return notBound();
28
+ const rendered = state.bound.render(args);
29
+ return pieceKind
30
+ ? piece(pieceKind, rendered.raw)
31
+ : rendered;
32
+ };
33
+ Object.defineProperty(declared, STATE, { value: state });
34
+ Object.defineProperty(declared, 'config', {
35
+ value: config,
36
+ enumerable: true,
37
+ });
38
+ if (asPiece) {
39
+ pieceFunction(declared, () => state.bound ? state.bound.render([]) : notBound());
40
+ }
41
+ return declared;
42
+ }
43
+ /** Binds a declared component to its name and to what makes its instances. */
44
+ export function bindComponent(declared, bound) {
45
+ const state = componentStateOf(declared);
46
+ if (!state)
47
+ throw new TypeError('Not a component.');
48
+ state.bound = bound;
49
+ }
@@ -1,28 +1,29 @@
1
1
  import type { Embed } from '../structures/payload.js';
2
2
  /**
3
- * What `embed()` returns: the default export of an embed file. Import it
4
- * where you send a message: `embeds: [welcome({ member })]`, or
5
- * `embeds: [rules]` for an embed that always looks the same.
3
+ * What `embed()` returns: a declaration the framework finds in the exports
4
+ * of a file. Import it where you send a message: `embeds: [welcome({
5
+ * member })]`, or `embeds: [rules]` for an embed that always looks the
6
+ * same.
6
7
  */
7
- export type EmbedFile<Args extends readonly unknown[] = []> = {
8
+ export type EmbedDeclaration<Args extends readonly unknown[] = []> = {
8
9
  /** What the file gave to `embed()`, not checked yet. */
9
10
  readonly config: unknown;
10
11
  } & ((...args: Args) => Embed);
11
12
  /**
12
- * Declares an embed. Export the result as the default export of a file of
13
- * `src/components/embeds/`. Give it the embed itself when it always looks
14
- * the same, or a function of what it shows:
13
+ * Declares an embed. Export the result from any file of `src/`. Give it
14
+ * the embed itself when it always looks the same, or a function of what it
15
+ * shows:
15
16
  *
16
17
  * ```ts
17
18
  * import { embed, type GuildMember } from 'chapterjs';
18
19
  *
19
- * export default embed((member: GuildMember) => ({
20
+ * export const welcome = embed((member: GuildMember) => ({
20
21
  * title: `Welcome ${member.displayName}!`,
21
22
  * color: 0x57f287,
22
23
  * }));
23
24
  * ```
24
25
  */
25
- export declare function embed(definition: Embed): EmbedFile<[]>;
26
- export declare function embed<const Args extends readonly unknown[]>(definition: (...args: Args) => Embed): EmbedFile<Args>;
27
- /** Whether a value is an embed file: a function to call for the embed. */
28
- export declare const isEmbedFile: (value: unknown) => value is EmbedFile<readonly unknown[]>;
26
+ export declare function embed(definition: Embed): EmbedDeclaration<[]>;
27
+ export declare function embed<const Args extends readonly unknown[]>(definition: (...args: Args) => Embed): EmbedDeclaration<Args>;
28
+ /** Whether a value is a declared embed: a function to call for the embed. */
29
+ export declare const isEmbedDeclaration: (value: unknown) => value is EmbedDeclaration<readonly unknown[]>;
@@ -1,11 +1,11 @@
1
- // `embed()`, as the files of `src/components/embeds/` import it from
2
- // 'chapterjs': an embed written once, used in every message that needs it.
1
+ // `embed()`, as user files import it from 'chapterjs': an embed written
2
+ // once, used in every message that needs it.
3
3
  // Static, or a function of what it shows.
4
- import { createFile, fileStateOf } from './file.js';
4
+ import { createComponent, componentStateOf } from './declared.js';
5
5
  export function embed(definition) {
6
- return createFile('embed', definition, {
6
+ return createComponent('embed', definition, {
7
7
  asPiece: false,
8
8
  });
9
9
  }
10
- /** Whether a value is an embed file: a function to call for the embed. */
11
- export const isEmbedFile = (value) => fileStateOf(value)?.kind === 'embed';
10
+ /** Whether a value is a declared embed: a function to call for the embed. */
11
+ export const isEmbedDeclaration = (value) => componentStateOf(value)?.kind === 'embed';
@@ -9,9 +9,9 @@ export interface Piece<Kind extends string> {
9
9
  /** What kind of piece it is. Only the framework makes pieces. */
10
10
  readonly [brand]: Kind;
11
11
  }
12
- /** A button: of a file of `src/components/buttons/`, `linkButton()` or `premiumButton()`. */
12
+ /** A button: declared with `button()`, or made with `linkButton()` or `premiumButton()`. */
13
13
  export type ButtonComponent = Piece<'button'>;
14
- /** A select menu of a file of `src/components/selects/`. */
14
+ /** A select menu declared with `select()`. */
15
15
  export type SelectComponent = Piece<'select'>;
16
16
  /** A row of up to 5 buttons, or of one menu: `row()`. */
17
17
  export type ActionRowComponent = Piece<'row'>;
@@ -29,7 +29,7 @@ export type FileComponent = Piece<'file'>;
29
29
  export type SeparatorComponent = Piece<'separator'>;
30
30
  /** A box around other pieces, with an optional color: `container()`. */
31
31
  export type ContainerComponent = Piece<'container'>;
32
- /** A form to open with `interaction.showModal()`: of a file of `src/components/modals/`. */
32
+ /** A form to open with `interaction.showModal()`: declared with `modal()`. */
33
33
  export type ModalComponent = Piece<'modal'>;
34
34
  /** What a container can hold. */
35
35
  export type ContainerChild = ButtonComponent | SelectComponent | ActionRowComponent | TextComponent | SectionComponent | GalleryComponent | FileComponent | SeparatorComponent;
@@ -1,5 +1,5 @@
1
1
  // What goes in the `components` of a message: pieces made by the framework
2
- // (a button of `src/components/buttons/`, a `row()`, a `container()`...),
2
+ // (a button declared with `button()`, a `row()`, a `container()`...),
3
3
  // never raw JSON. Each piece knows the Discord shape it is sent as; the
4
4
  // brand is what lets a message refuse anything else, so that no custom_id
5
5
  // is ever written by hand.
@@ -47,7 +47,7 @@ export function isPiece(value) {
47
47
  /** What a piece is and how it is sent. Throws on anything else. */
48
48
  export function renderedOf(value, where) {
49
49
  if (!isPiece(value)) {
50
- throw new TypeError(`${where} is not a component made by ChapterJS, got ${value === null ? 'null' : typeof value === 'object' && 'custom_id' in value ? 'an object with a custom_id' : typeof value}. Use the buttons, menus and modals of src/components/, and row(), text(), section(), container()... from 'chapterjs'. Components are never written as JSON.`);
50
+ throw new TypeError(`${where} is not a component made by ChapterJS, got ${value === null ? 'null' : typeof value === 'object' && 'custom_id' in value ? 'an object with a custom_id' : typeof value}. Use the buttons, menus and forms declared with button(), select() and modal(), and row(), text(), section(), container()... from 'chapterjs'. Components are never written as JSON.`);
51
51
  }
52
52
  const { kind, raw } = value;
53
53
  return { kind, raw };
@@ -186,24 +186,24 @@ export interface ModalInstanceOptions<Fields extends ModalFields = ModalFields>
186
186
  values?: ModalPrefill<Fields>;
187
187
  }
188
188
  /**
189
- * What `modal()` returns: the default export of a modal file. Import it
190
- * where someone uses a command or a component, and open it:
189
+ * What `modal()` returns: a declaration the framework finds in the exports
190
+ * of a file. Import it where someone uses a command or a component, and
191
+ * open it:
191
192
  * `interaction.showModal(report({ userId }))`, or
192
193
  * `interaction.showModal(feedback)` for a form without data.
193
194
  */
194
- export type ModalFile<Fields extends ModalFields = ModalFields, Data extends DataShape = {}> = {
195
+ export type ModalDeclaration<Fields extends ModalFields = ModalFields, Data extends DataShape = {}> = {
195
196
  /** What the file gave to `modal()`, not checked yet. */
196
197
  readonly config: unknown;
197
198
  } & ({} extends Data ? ((options?: ModalInstanceOptions<Fields>) => ModalComponent) & ModalComponent : {} extends DataInputOf<Data> ? ((data?: DataInputOf<Data>, options?: ModalInstanceOptions<Fields>) => ModalComponent) & ModalComponent : (data: DataInputOf<Data>, options?: ModalInstanceOptions<Fields>) => ModalComponent);
198
199
  /**
199
- * Declares a form. Export the result as the default export of a file of
200
- * `src/components/modals/`: the path of the file is what tells the form
201
- * apart, so you never write an id.
200
+ * Declares a form. Export the result from any file of `src/`: the name of
201
+ * the export is what tells the form apart, so you never write an id.
202
202
  *
203
203
  * ```ts
204
204
  * import { modal } from 'chapterjs';
205
205
  *
206
- * export default modal({
206
+ * export const feedback = modal({
207
207
  * title: 'Feedback',
208
208
  * fields: {
209
209
  * text: { type: 'text', label: 'What do you think?', style: 'paragraph' },
@@ -214,7 +214,7 @@ export type ModalFile<Fields extends ModalFields = ModalFields, Data extends Dat
214
214
  * });
215
215
  * ```
216
216
  */
217
- export declare function modal<const Fields extends ModalFields, const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ModalConfig<Fields, Data, Where>): ModalFile<Fields, Data>;
217
+ export declare function modal<const Fields extends ModalFields, const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ModalConfig<Fields, Data, Where>): ModalDeclaration<Fields, Data>;
218
218
  /** A field of a form, checked, with how it is sent. */
219
219
  export interface LoadedField {
220
220
  readonly name: string;
@@ -225,10 +225,11 @@ export interface LoadedField {
225
225
  readonly min: number;
226
226
  readonly max: number;
227
227
  }
228
- /** A modal file, checked. */
228
+ /** A form, checked. */
229
229
  export interface LoadedModal {
230
230
  readonly kind: 'modal';
231
- readonly path: string;
231
+ /** The name of the export that declares it: its id for Discord. */
232
+ readonly name: string;
232
233
  /** The title (45 characters at most): a text, or computed when the form opens. */
233
234
  readonly title: DynamicText;
234
235
  readonly fields: readonly LoadedField[];
@@ -5,18 +5,17 @@ import { Limits } from '../discord/api.js';
5
5
  import { ComponentType, TextInputStyle, } from '../discord/types/component.js';
6
6
  import { isDynamic, resolveText, } from './component.js';
7
7
  import { checkData, encodeCustomId, } from './custom-id.js';
8
- import { createFile } from './file.js';
8
+ import { createComponent } from './declared.js';
9
9
  import { hasDynamicOption, readOptions, resolveOptions, } from './options.js';
10
10
  import { readChannelTypes, readPlaceholder, readRange, SELECT_TYPES, } from './select.js';
11
11
  /**
12
- * Declares a form. Export the result as the default export of a file of
13
- * `src/components/modals/`: the path of the file is what tells the form
14
- * apart, so you never write an id.
12
+ * Declares a form. Export the result from any file of `src/`: the name of
13
+ * the export is what tells the form apart, so you never write an id.
15
14
  *
16
15
  * ```ts
17
16
  * import { modal } from 'chapterjs';
18
17
  *
19
- * export default modal({
18
+ * export const feedback = modal({
20
19
  * title: 'Feedback',
21
20
  * fields: {
22
21
  * text: { type: 'text', label: 'What do you think?', style: 'paragraph' },
@@ -28,7 +27,7 @@ import { readChannelTypes, readPlaceholder, readRange, SELECT_TYPES, } from './s
28
27
  * ```
29
28
  */
30
29
  export function modal(config) {
31
- return createFile('modal', config, {
30
+ return createComponent('modal', config, {
32
31
  asPiece: true,
33
32
  pieceKind: 'modal',
34
33
  });
@@ -374,14 +373,14 @@ function readPrefill(loaded, name, given) {
374
373
  /** The form as Discord takes it, for the data and the values given. */
375
374
  export function renderModal(loaded, args) {
376
375
  const hasData = Object.keys(loaded.data).length > 0;
377
- const name = loaded.path.split('/').pop();
376
+ const { name } = loaded;
378
377
  if (args.length > (hasData ? 2 : 1)) {
379
378
  fail(`${name} takes ${hasData ? 'its data, then options' : 'options'} at most.`);
380
379
  }
381
380
  const values = checkData(name, loaded.data, hasData ? args[0] : undefined);
382
381
  const prefill = readPrefill(loaded, name, hasData ? args[1] : args[0]);
383
382
  const make = (context) => ({
384
- custom_id: encodeCustomId(loaded.path, loaded.data, values),
383
+ custom_id: encodeCustomId(loaded.name, loaded.data, values),
385
384
  title: resolveText(`The title of ${name}`, loaded.title, context, {
386
385
  max: Limits.ModalTitle,
387
386
  }),
@@ -1,10 +1,11 @@
1
1
  import { type RawInteraction } from '../discord/types/interaction.js';
2
2
  import { type Reporter } from '../interactions/dispatch.js';
3
3
  import type { Context } from '../structures/context.js';
4
- import type { LoadedComponent } from './convention.js';
5
- /** A component, with the file it comes from (to report its errors). */
4
+ import type { LoadedComponent } from './declaration.js';
5
+ /** A component, with the file and the export it comes from (to report its errors). */
6
6
  export interface ComponentEntry {
7
7
  file: string;
8
+ export: string;
8
9
  component: LoadedComponent;
9
10
  }
10
11
  export type ComponentRouterOptions = Reporter;
@@ -1,6 +1,6 @@
1
1
  // Runs the component someone used: reads the id the framework wrote in it,
2
- // finds its file, turns what Discord sent into what `run` receives, and
3
- // makes sure the person always gets an answer.
2
+ // finds its declaration, turns what Discord sent into what `run` receives,
3
+ // and makes sure the person always gets an answer.
4
4
  import { ComponentType, } from '../discord/types/component.js';
5
5
  import { InteractionType, } from '../discord/types/interaction.js';
6
6
  import { MessageFlags } from '../discord/types/message.js';
@@ -12,11 +12,10 @@ import { toCamelCase } from '../util/case.js';
12
12
  import { decodeCustomId, readData } from './custom-id.js';
13
13
  import { SELECT_TYPES } from './select.js';
14
14
  /** What a component is called, for the person and the developer. */
15
- const WHAT = {
15
+ const WHAT_OF = {
16
16
  button: 'button',
17
17
  select: 'menu',
18
18
  modal: 'form',
19
- embed: 'embed',
20
19
  };
21
20
  export class ComponentRouter {
22
21
  #components = new Map();
@@ -29,7 +28,7 @@ export class ComponentRouter {
29
28
  const components = new Map();
30
29
  for (const entry of entries) {
31
30
  if (entry.component.kind !== 'embed') {
32
- components.set(entry.component.path, entry);
31
+ components.set(`${entry.component.kind}:${entry.component.name}`, entry);
33
32
  }
34
33
  }
35
34
  this.#components = components;
@@ -59,14 +58,17 @@ export class ComponentRouter {
59
58
  MessageFlags.Ephemeral) !==
60
59
  0;
61
60
  const ephemeralOf = (component) => isPrivate || ('ephemeral' in component && component.ephemeral);
62
- const { path, parts } = decodeCustomId(data.custom_id);
63
- const entry = this.#components.get(path);
64
- const what = isModal
65
- ? 'form'
61
+ const { name, parts } = decodeCustomId(data.custom_id);
62
+ // A button, a menu and a form may share a name: what was used says
63
+ // which one is meant.
64
+ const kind = isModal
65
+ ? 'modal'
66
66
  : data.component_type ===
67
67
  ComponentType.Button
68
68
  ? 'button'
69
- : 'menu';
69
+ : 'select';
70
+ const entry = this.#components.get(`${kind}:${name}`);
71
+ const what = WHAT_OF[kind];
70
72
  const interaction = isModal
71
73
  ? new ModalInteraction(ctx, rest, user, {
72
74
  ephemeral: entry ? ephemeralOf(entry.component) : isPrivate,
@@ -81,27 +83,20 @@ export class ComponentRouter {
81
83
  // Embeds are never in the map: what is left has a `run`.
82
84
  if (!entry ||
83
85
  entry.component.kind === 'embed' ||
84
- (isModal
85
- ? entry.component.kind !== 'modal'
86
- : entry.component.kind === 'modal') ||
87
- (entry.component.kind === 'button' &&
88
- data.component_type !==
89
- ComponentType.Button) ||
90
86
  (entry.component.kind === 'select' &&
91
87
  SELECT_TYPES[entry.component.type] !==
92
88
  data.component_type)) {
93
- // Sent before the file was renamed, removed or changed of kind.
89
+ // Sent before the export was renamed, removed or changed of kind.
94
90
  refuse(interaction, 'gone', { what });
95
91
  return;
96
92
  }
97
93
  const { component, file } = entry;
98
94
  const values = readData(component.data, parts);
99
95
  if (!values) {
100
- // The data of the file changed since the message was sent.
96
+ // The data of the component changed since the message was sent.
101
97
  refuse(interaction, 'outdated', { what });
102
98
  return;
103
99
  }
104
- const name = component.path;
105
100
  const placed = resolvePlace(ctx, raw, user, author.member, component.where, warning => this.#options.onWarning(file, `${name} ${warning}`));
106
101
  if (!placed.place) {
107
102
  refuse(interaction, placed.refusal, { what });