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
@@ -1,5 +1,5 @@
1
1
  // What the bot answers by itself, when it can't run what was asked: in
2
- // English here, and in the languages of `src/messages/` that translate
2
+ // English here, and in the language files that translate
3
3
  // them (the `framework` of a language file).
4
4
  import { fill, pickLocale, templateOf } from './translate.js';
5
5
  /** The phrases of the framework, in English. */
@@ -18,7 +18,7 @@ export interface PluralTemplate {
18
18
  readonly params: readonly string[];
19
19
  }
20
20
  export type Template = TextTemplate | PluralTemplate;
21
- /** The languages of `src/messages/`, checked and assembled: what `t` reads. */
21
+ /** The languages of the project, checked and assembled: what `t` reads. */
22
22
  export interface LoadedMessages {
23
23
  readonly default: Locale;
24
24
  /** The texts of each language declared, compiled. */
@@ -31,7 +31,7 @@ export interface LoadedMessages {
31
31
  * needed for that.
32
32
  */
33
33
  readonly commands: ReadonlyMap<Locale, Readonly<Record<string, unknown>>>;
34
- /** The file of each language: `src/messages/fr.ts`. */
34
+ /** The file each language is declared in. */
35
35
  readonly files: ReadonlyMap<Locale, string>;
36
36
  }
37
37
  /** The forms a plural can have, in the order of the docs. */
@@ -82,7 +82,7 @@ export interface Audience {
82
82
  * @see https://docs.discord.com/developers/resources/guild#guild-object-guild-structure
83
83
  */
84
84
  export declare function audienceLocale({ person, guild, ephemeral, }: Audience): string | null;
85
- /** A `t` for a project without a language in `src/messages/`: it says so when called. */
85
+ /** A `t` for a project without a language: it says so when called. */
86
86
  export declare function missingTranslator(locale: string | null): Translator<MessageTexts>;
87
87
  /**
88
88
  * `t` for what the bot sends, in the language of who will read it. Without
@@ -92,7 +92,7 @@ export declare function missingTranslator(locale: string | null): Translator<Mes
92
92
  export declare function translatorOf(ctx: Pick<Context, 'messages'>, audience: Audience): Translator<MessageTexts>;
93
93
  /**
94
94
  * What a handler receives about translations: `t` in the language asked
95
- * when the project has a language in `src/messages/`, nothing otherwise. Spread it
95
+ * when the project has a language, nothing otherwise. Spread it
96
96
  * into the context of any handler.
97
97
  */
98
98
  export declare function translation(ctx: Pick<Context, 'messages'>, locale: string | null): {
@@ -176,7 +176,7 @@ export function translatorFor(messages, locale) {
176
176
  return fill(key, own, params, picked);
177
177
  const template = base.get(key);
178
178
  if (!template) {
179
- throw new TypeError(`There is no message "${key}" in src/messages/. It has: ${[...base.keys()].join(', ')}.`);
179
+ throw new TypeError(`There is no message "${key}" in your languages. They have: ${[...base.keys()].join(', ')}.`);
180
180
  }
181
181
  return fill(key, template, params, messages.default);
182
182
  });
@@ -208,14 +208,14 @@ export function audienceLocale({ person, guild, ephemeral, }) {
208
208
  return guild.features.includes('COMMUNITY') ? guild.preferredLocale : null;
209
209
  }
210
210
  const missing = new Map();
211
- /** A `t` for a project without a language in `src/messages/`: it says so when called. */
211
+ /** A `t` for a project without a language: it says so when called. */
212
212
  export function missingTranslator(locale) {
213
213
  const shown = locale ?? 'en-US';
214
214
  const known = missing.get(shown);
215
215
  if (known)
216
216
  return known;
217
217
  const t = (() => {
218
- throw new TypeError('This project has no language in src/messages/: add one, like src/messages/en-US.ts, to use t().');
218
+ throw new TypeError("This project has no language: declare one with language({ locale: 'en-US', texts: { ... } }) to use t().");
219
219
  });
220
220
  Object.defineProperties(t, {
221
221
  locale: { value: shown, enumerable: true },
@@ -241,7 +241,7 @@ export function translatorOf(ctx, audience) {
241
241
  }
242
242
  /**
243
243
  * What a handler receives about translations: `t` in the language asked
244
- * when the project has a language in `src/messages/`, nothing otherwise. Spread it
244
+ * when the project has a language, nothing otherwise. Spread it
245
245
  * into the context of any handler.
246
246
  */
247
247
  export function translation(ctx, locale) {
@@ -0,0 +1,19 @@
1
+ import type { Declaration } from '../loader/loader.js';
2
+ import { type RawGatewayPresenceUpdate } from '../discord/types/gateway-events.js';
3
+ /** The presence, checked: what the gateway sends. */
4
+ export interface LoadedPresence {
5
+ /** The Update Presence payload (also sent with Identify). */
6
+ readonly raw: RawGatewayPresenceUpdate;
7
+ /** What tells one presence from another: the payload, as text. */
8
+ readonly key: string;
9
+ }
10
+ /**
11
+ * What a bot without a presence shows: online, doing nothing. Sent when
12
+ * the declaration is removed while the bot runs.
13
+ */
14
+ export declare const DEFAULT_PRESENCE: RawGatewayPresenceUpdate;
15
+ /**
16
+ * The presence of the bot, declared once anywhere in `src/` with
17
+ * `presence()` and exported.
18
+ */
19
+ export declare const presenceDeclaration: Declaration<LoadedPresence>;
@@ -1,9 +1,8 @@
1
- // `src/presence.ts`: the one file that says what the bot shows under its
2
- // name. What it declares is checked when it loads and turned into what the
3
- // gateway sends, so a wrong status or type is explained before the bot
4
- // connects.
1
+ // What a file declares with `presence()`: what the bot shows under its
2
+ // name. It is checked when it loads and turned into what the gateway
3
+ // sends, so a wrong status or type is explained before the bot connects.
5
4
  import { ActivityType, } from '../discord/types/gateway-events.js';
6
- import { isPresenceFile } from './presence.js';
5
+ import { isPresenceDeclaration } from './presence.js';
7
6
  const fail = message => {
8
7
  throw new TypeError(message);
9
8
  };
@@ -36,8 +35,8 @@ const STREAM_URL = /^https:\/\/(?:www\.)?(?:twitch\.tv|youtube\.com)\//;
36
35
  const list = (words) => words.map(word => `'${word}'`).join(', ');
37
36
  const EXAMPLE = `presence({ status: 'online', activity: { type: 'watching', name: 'over the server' } })`;
38
37
  /**
39
- * What a bot without a presence file shows: online, doing nothing. Sent
40
- * when the file is removed while the bot runs.
38
+ * What a bot without a presence shows: online, doing nothing. Sent when
39
+ * the declaration is removed while the bot runs.
41
40
  */
42
41
  export const DEFAULT_PRESENCE = Object.freeze({
43
42
  since: null,
@@ -65,30 +64,16 @@ function toRaw(config) {
65
64
  };
66
65
  }
67
66
  /**
68
- * `src/presence.ts`: the status of the bot and what it is doing. The file
69
- * exports by default what `presence()` returns:
70
- *
71
- * ```ts
72
- * // src/presence.ts
73
- * import { presence } from 'chapterjs';
74
- *
75
- * export default presence({ activity: { type: 'playing', name: '/help' } });
76
- * ```
67
+ * The presence of the bot, declared once anywhere in `src/` with
68
+ * `presence()` and exported.
77
69
  */
78
- export const presenceConvention = {
79
- folder: 'presence',
80
- single: true,
70
+ export const presenceDeclaration = {
81
71
  one: 'presence',
82
72
  many: 'presences',
83
- read(exports) {
84
- const file = exports.default;
85
- const example = `import { presence } from 'chapterjs'; export default ${EXAMPLE}`;
86
- if (!isPresenceFile(file)) {
87
- return fail('default' in exports
88
- ? `The default export of this file must be what presence() returns: ${example}`
89
- : `This file has no default export. It should look like: ${example}`);
90
- }
91
- const config = file.config;
73
+ is: isPresenceDeclaration,
74
+ list: false,
75
+ read(value) {
76
+ const config = value.config;
92
77
  if (!isRecord(config)) {
93
78
  return fail(`presence() needs an object: ${EXAMPLE}`);
94
79
  }
@@ -26,22 +26,22 @@ export interface StreamingActivity {
26
26
  }
27
27
  /** What the bot is doing. */
28
28
  export type Activity = PlainActivity | StreamingActivity;
29
- /** What `src/presence.ts` gives to `presence()`. */
29
+ /** What a file gives to `presence()`. */
30
30
  export interface PresenceConfig {
31
31
  /** The status of the bot. `'online'` by default. */
32
32
  status?: PresenceStatusName;
33
33
  /** What the bot is doing. Nothing by default. */
34
34
  activity?: Activity;
35
35
  }
36
- /** What `presence()` returns: the default export of `src/presence.ts`. */
37
- export interface PresenceFile {
36
+ /** What `presence()` returns: a declaration the framework finds in the exports of a file. */
37
+ export interface PresenceDeclaration {
38
38
  /** What the file gave to `presence()`, not checked yet. */
39
39
  readonly config: unknown;
40
40
  }
41
41
  /**
42
42
  * Declares the presence of the bot: its status and what it is doing, shown
43
- * under its name. Export the result as the default export of
44
- * `src/presence.ts`; the bot has it as soon as it is connected.
43
+ * under its name. Export the result from any file of `src/`, once; the bot
44
+ * has it as soon as it is connected.
45
45
  *
46
46
  * ```ts
47
47
  * import { presence } from 'chapterjs';
@@ -52,6 +52,6 @@ export interface PresenceFile {
52
52
  * });
53
53
  * ```
54
54
  */
55
- export declare function presence(config: PresenceConfig): PresenceFile;
55
+ export declare function presence(config: PresenceConfig): PresenceDeclaration;
56
56
  /** Whether a value was made by `presence()`. */
57
- export declare function isPresenceFile(value: unknown): value is PresenceFile;
57
+ export declare function isPresenceDeclaration(value: unknown): value is PresenceDeclaration;
@@ -1,11 +1,11 @@
1
- // `presence()`, as `src/presence.ts` imports it from 'chapterjs': what the
2
- // bot shows under its name, its status and what it is doing.
1
+ // `presence()`, as user files import it from 'chapterjs': what the bot
2
+ // shows under its name, its status and what it is doing.
3
3
  // https://docs.discord.com/developers/events/gateway-events#update-presence
4
4
  const BRAND = Symbol.for('chapterjs.presence');
5
5
  /**
6
6
  * Declares the presence of the bot: its status and what it is doing, shown
7
- * under its name. Export the result as the default export of
8
- * `src/presence.ts`; the bot has it as soon as it is connected.
7
+ * under its name. Export the result from any file of `src/`, once; the bot
8
+ * has it as soon as it is connected.
9
9
  *
10
10
  * ```ts
11
11
  * import { presence } from 'chapterjs';
@@ -20,7 +20,7 @@ export function presence(config) {
20
20
  return Object.freeze({ [BRAND]: true, config });
21
21
  }
22
22
  /** Whether a value was made by `presence()`. */
23
- export function isPresenceFile(value) {
23
+ export function isPresenceDeclaration(value) {
24
24
  return (typeof value === 'object' &&
25
25
  value !== null &&
26
26
  value[BRAND] === true);
@@ -18,7 +18,7 @@ export interface Context {
18
18
  userId: Snowflake;
19
19
  applicationId: Snowflake;
20
20
  } | null;
21
- /** The texts of `src/messages/`; `null` without a language. */
21
+ /** The texts of the language files; `null` without a language. */
22
22
  messages: LoadedMessages | null;
23
23
  /** Where the bot is in voice; `null` outside a running bot. */
24
24
  voice: VoiceManager | null;
@@ -94,7 +94,7 @@ export declare class CommandInteraction extends Interaction {
94
94
  });
95
95
  /**
96
96
  * Opens a form, as the answer: `showModal(report({ userId }))`, with a
97
- * form of `src/components/modals/`. It must be the first answer, and the
97
+ * form declared with `modal()`. It must be the first answer, and the
98
98
  * person has as long as they want to fill it in.
99
99
  * @see https://docs.discord.com/developers/interactions/receiving-and-responding#interaction-response-object-modal
100
100
  */
@@ -128,7 +128,7 @@ export declare class ComponentInteraction extends Interaction {
128
128
  deferUpdate(): Promise<void>;
129
129
  /**
130
130
  * Opens a form, as the answer: `showModal(report({ userId }))`, with a
131
- * form of `src/components/modals/`. It must be the first answer.
131
+ * form declared with `modal()`. It must be the first answer.
132
132
  * @see https://docs.discord.com/developers/interactions/receiving-and-responding#interaction-response-object-modal
133
133
  */
134
134
  showModal(form: ModalComponent): Promise<void>;
@@ -75,7 +75,7 @@ export class Interaction extends IdStructure {
75
75
  openModal = (interaction, form) => {
76
76
  const rendered = renderedOf(form, 'The form given to showModal()');
77
77
  if (rendered.kind !== 'modal') {
78
- throw new TypeError(`showModal() opens a form of src/components/modals/, got a ${rendered.kind}.`);
78
+ throw new TypeError(`showModal() opens a form declared with modal(), got a ${rendered.kind}.`);
79
79
  }
80
80
  return interaction.#run(async () => {
81
81
  if (interaction.#state !== 'waiting') {
@@ -313,7 +313,7 @@ export class CommandInteraction extends Interaction {
313
313
  }
314
314
  /**
315
315
  * Opens a form, as the answer: `showModal(report({ userId }))`, with a
316
- * form of `src/components/modals/`. It must be the first answer, and the
316
+ * form declared with `modal()`. It must be the first answer, and the
317
317
  * person has as long as they want to fill it in.
318
318
  * @see https://docs.discord.com/developers/interactions/receiving-and-responding#interaction-response-object-modal
319
319
  */
@@ -353,7 +353,7 @@ export class ComponentInteraction extends Interaction {
353
353
  }
354
354
  /**
355
355
  * Opens a form, as the answer: `showModal(report({ userId }))`, with a
356
- * form of `src/components/modals/`. It must be the first answer.
356
+ * form declared with `modal()`. It must be the first answer.
357
357
  * @see https://docs.discord.com/developers/interactions/receiving-and-responding#interaction-response-object-modal
358
358
  */
359
359
  showModal(form) {
@@ -1,5 +1,5 @@
1
1
  import { type AssetFile } from '../assets/asset.js';
2
- import { type EmbedFile } from '../components/embed.js';
2
+ import { type EmbedDeclaration } from '../components/embed.js';
3
3
  import type { MessageComponent } from '../components/instance.js';
4
4
  import type { Snowflake } from '../discord/types/common.js';
5
5
  import { type CreateMessageJSONParams, type RawAllowedMentions, type RawEmbed } from '../discord/types/message.js';
@@ -39,11 +39,11 @@ export interface FileInput {
39
39
  export interface MessageOptions {
40
40
  /** The text of the message (up to 2000 characters). */
41
41
  content?: string;
42
- /** Up to 10 embeds: written here, or files of `src/components/embeds/`. */
43
- embeds?: (Embed | EmbedFile<[]>)[];
42
+ /** Up to 10 embeds: written here, or declared with `embed()`. */
43
+ embeds?: (Embed | EmbedDeclaration<[]>)[];
44
44
  /**
45
45
  * Buttons, select menus and layout components, made by ChapterJS: the
46
- * files of `src/components/` and `row()`, `text()`, `container()`...
46
+ * components declared with `button()`, `select()`... and `row()`, `text()`, `container()`...
47
47
  */
48
48
  components?: MessageComponent[];
49
49
  /** Up to 10 files to attach: their content, or `asset()` for a file of `public/`. */
@@ -2,7 +2,7 @@
2
2
  // expects. Used by everything that sends a message: channels, replies,
3
3
  // webhooks, and later interactions.
4
4
  import { isAssetFile, readAsset } from '../assets/asset.js';
5
- import { isEmbedFile } from '../components/embed.js';
5
+ import { isEmbedDeclaration, } from '../components/embed.js';
6
6
  import { renderComponents } from '../components/render.js';
7
7
  import { Limits } from '../discord/api.js';
8
8
  import { MessageFlags, } from '../discord/types/message.js';
@@ -41,16 +41,16 @@ export function checkEmbed(embed, name) {
41
41
  });
42
42
  return total;
43
43
  }
44
- /** The embeds of a message, with the files of `src/components/embeds/` called. */
44
+ /** The embeds of a message, with the ones declared with `embed()` called. */
45
45
  function resolveEmbeds(embeds) {
46
46
  if (!Array.isArray(embeds)) {
47
47
  throw new TypeError(`The embeds of a message are a list, got ${typeof embeds}.`);
48
48
  }
49
49
  return embeds.map((embed, index) => {
50
- if (isEmbedFile(embed))
50
+ if (isEmbedDeclaration(embed))
51
51
  return embed();
52
52
  if (typeof embed === 'function') {
53
- throw new TypeError(`Embed ${index + 1} of the message is a function: an embed is an object like { title: '...' }, or a file of src/components/embeds/.`);
53
+ throw new TypeError(`Embed ${index + 1} of the message is a function: an embed is an object like { title: '...' }, or one declared with embed().`);
54
54
  }
55
55
  return embed;
56
56
  });
@@ -0,0 +1,16 @@
1
+ import type { Declaration } from '../loader/loader.js';
2
+ import { type Schedule } from './schedule.js';
3
+ import { type TaskContext } from './task.js';
4
+ /** A task, checked: what the scheduler works with. */
5
+ export interface LoadedTask {
6
+ /** The name of the task: the name of the export that declares it. */
7
+ readonly name: string;
8
+ readonly schedule: Schedule;
9
+ readonly onStart: boolean;
10
+ readonly run: (context: TaskContext) => unknown;
11
+ }
12
+ /**
13
+ * A task, declared anywhere in `src/` with `task()` and exported: the name
14
+ * of the export is the name of the task.
15
+ */
16
+ export declare const taskDeclaration: Declaration<LoadedTask>;
@@ -1,49 +1,27 @@
1
- // `src/tasks/`: one file per task, named after it. What the file declares
2
- // is checked when it loads, so a wrong duration or cron is explained
3
- // before the bot waits for it.
1
+ // What a file declares with `task()`, checked when it loads, so a wrong
2
+ // duration or cron is explained before the bot waits for it. The name of
3
+ // the export is the name of the task.
4
4
  import { parseCron, parseEvery } from './schedule.js';
5
- import { isTaskFile } from './task.js';
5
+ import { isTaskDeclaration, } from './task.js';
6
6
  const fail = (message) => {
7
7
  throw new TypeError(message);
8
8
  };
9
9
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
10
10
  const KEYS = ['every', 'cron', 'timezone', 'onStart', 'run'];
11
+ const EXAMPLE = `task({ every: '10m', run() { ... } })`;
11
12
  /**
12
- * `src/tasks/`: each file is a task, named after its path. It exports by
13
- * default what `task()` returns:
14
- *
15
- * ```ts
16
- * // src/tasks/report.ts
17
- * import { task } from 'chapterjs';
18
- *
19
- * export default task({
20
- * cron: '0 9 * * 1',
21
- * async run({ guilds }) { ... },
22
- * });
23
- * ```
13
+ * A task, declared anywhere in `src/` with `task()` and exported: the name
14
+ * of the export is the name of the task.
24
15
  */
25
- export const tasksConvention = {
26
- folder: 'tasks',
16
+ export const taskDeclaration = {
27
17
  one: 'task',
28
18
  many: 'tasks',
29
- check(path) {
30
- for (const part of path.replace(/\.[^./]+$/, '').split('/')) {
31
- if (!/^[A-Za-z0-9_-]+$/.test(part)) {
32
- fail(`"${part}" can't be in the name of a task: use letters, digits, - and _ only. The path of the file is the name of the task.`);
33
- }
34
- }
35
- },
36
- read(exports, path) {
37
- const file = exports.default;
38
- const example = `import { task } from 'chapterjs'; export default task({ every: '10m', run() { ... } })`;
39
- if (!isTaskFile(file)) {
40
- return fail('default' in exports
41
- ? `The default export of this file must be what task() returns: ${example}`
42
- : `This file has no default export. It should look like: ${example}`);
43
- }
44
- const config = file.config;
19
+ is: isTaskDeclaration,
20
+ list: false,
21
+ read(value, { name }) {
22
+ const config = value.config;
45
23
  if (!isRecord(config)) {
46
- return fail(`task() needs an object: task({ every: '10m', run() { ... } })`);
24
+ return fail(`task() needs an object: ${EXAMPLE}`);
47
25
  }
48
26
  for (const key of Object.keys(config)) {
49
27
  if (!KEYS.includes(key)) {
@@ -69,7 +47,7 @@ export const tasksConvention = {
69
47
  fail(`This task has no "run": the function to run each time, like async run({ guilds }) { ... }`);
70
48
  }
71
49
  return Object.freeze({
72
- name: path.replace(/\.[^./]+$/, ''),
50
+ name,
73
51
  schedule,
74
52
  onStart: config.onStart === true,
75
53
  run: config.run,
@@ -1,8 +1,10 @@
1
1
  import type { TaskContext } from './task.js';
2
- import type { LoadedTask } from './convention.js';
2
+ import type { LoadedTask } from './declaration.js';
3
3
  /** A task, with the file it comes from (to report its errors). */
4
4
  export interface TaskEntry {
5
5
  file: string;
6
+ /** The export it is: its name is the name of the task. */
7
+ export: string;
6
8
  task: LoadedTask;
7
9
  }
8
10
  /** What runs the tasks. */
@@ -17,12 +17,12 @@ export class TimerScheduler {
17
17
  const started = [];
18
18
  for (const entry of entries) {
19
19
  const key = scheduleKey(entry.task.schedule);
20
- const previous = this.#tasks.get(entry.file);
20
+ const previous = this.#tasks.get(entry.task.name);
21
21
  if (previous && previous.key === key) {
22
22
  // Same timing: the next time stays, only the code changes.
23
23
  previous.entry = entry;
24
- kept.set(entry.file, previous);
25
- this.#tasks.delete(entry.file);
24
+ kept.set(entry.task.name, previous);
25
+ this.#tasks.delete(entry.task.name);
26
26
  continue;
27
27
  }
28
28
  const scheduled = {
@@ -32,7 +32,7 @@ export class TimerScheduler {
32
32
  timer: null,
33
33
  running: false,
34
34
  };
35
- kept.set(entry.file, scheduled);
35
+ kept.set(entry.task.name, scheduled);
36
36
  started.push(scheduled);
37
37
  }
38
38
  // What is left was removed, or changed timing: it starts over.
@@ -47,22 +47,21 @@ export interface CronTaskConfig extends TaskConfigBase {
47
47
  timezone?: string;
48
48
  every?: never;
49
49
  }
50
- /** What a task file gives to `task()`: when it runs, and what it does. */
50
+ /** What a file gives to `task()`: when it runs, and what it does. */
51
51
  export type TaskConfig = EveryTaskConfig | CronTaskConfig;
52
- /** What `task()` returns: the default export of a task file. */
53
- export interface TaskFile {
52
+ /** What `task()` returns: a declaration the framework finds in the exports of a file. */
53
+ export interface TaskDeclaration {
54
54
  /** What the file gave to `task()`, not checked yet. */
55
55
  readonly config: unknown;
56
56
  }
57
57
  /**
58
- * Declares a task: something the bot does by itself. Export the result as
59
- * the default export of a file of `src/tasks/`: the name of the file is
60
- * the name of the task.
58
+ * Declares a task: something the bot does by itself. Export the result
59
+ * from any file of `src/`: the name of the export is the name of the task.
61
60
  *
62
61
  * ```ts
63
62
  * import { task } from 'chapterjs';
64
63
  *
65
- * export default task({
64
+ * export const watch = task({
66
65
  * every: '10m',
67
66
  * async run({ guilds }) {
68
67
  * console.log(`Watching ${guilds.size} servers`);
@@ -70,7 +69,7 @@ export interface TaskFile {
70
69
  * });
71
70
  * ```
72
71
  */
73
- export declare function task(config: TaskConfig): TaskFile;
72
+ export declare function task(config: TaskConfig): TaskDeclaration;
74
73
  /** Whether a value was made by `task()`. */
75
- export declare function isTaskFile(value: unknown): value is TaskFile;
74
+ export declare function isTaskDeclaration(value: unknown): value is TaskDeclaration;
76
75
  export {};
@@ -1,15 +1,14 @@
1
- // `task()`, as the files of `src/tasks/` import it from 'chapterjs': what
2
- // the bot does by itself, at intervals or at given times.
1
+ // `task()`, as user files import it from 'chapterjs': what the bot does by
2
+ // itself, at intervals or at given times.
3
3
  const BRAND = Symbol.for('chapterjs.task');
4
4
  /**
5
- * Declares a task: something the bot does by itself. Export the result as
6
- * the default export of a file of `src/tasks/`: the name of the file is
7
- * the name of the task.
5
+ * Declares a task: something the bot does by itself. Export the result
6
+ * from any file of `src/`: the name of the export is the name of the task.
8
7
  *
9
8
  * ```ts
10
9
  * import { task } from 'chapterjs';
11
10
  *
12
- * export default task({
11
+ * export const watch = task({
13
12
  * every: '10m',
14
13
  * async run({ guilds }) {
15
14
  * console.log(`Watching ${guilds.size} servers`);
@@ -21,7 +20,7 @@ export function task(config) {
21
20
  return Object.freeze({ [BRAND]: true, config });
22
21
  }
23
22
  /** Whether a value was made by `task()`. */
24
- export function isTaskFile(value) {
23
+ export function isTaskDeclaration(value) {
25
24
  return (typeof value === 'object' &&
26
25
  value !== null &&
27
26
  value[BRAND] === true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterjs",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "The Discord framework",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,35 +0,0 @@
1
- import type { Convention } from '../loader/loader.js';
2
- import { type LoadedButton } from './button.js';
3
- import { isEmbedFile } from './embed.js';
4
- import { type LoadedModal } from './modal.js';
5
- import { type LoadedSelect } from './select.js';
6
- /** An embed file, checked (when it is static). */
7
- export interface LoadedEmbed {
8
- readonly kind: 'embed';
9
- readonly path: string;
10
- }
11
- /** A component file, checked: what the router works with. */
12
- export type LoadedComponent = LoadedButton | LoadedSelect | LoadedModal | LoadedEmbed;
13
- /**
14
- * `src/components/`: one folder per kind of component (`buttons/`,
15
- * `selects/`, `modals/`, `embeds/`), and in it one file per component,
16
- * whose path is its id. The file exports by default what `button()`,
17
- * `select()`, `modal()` or `embed()` returns.
18
- */
19
- export declare const componentsConvention: Convention<LoadedComponent>;
20
- /** The files that are the same component (through `(group)` folders): the later one is left out. */
21
- export declare function findDuplicates<T extends {
22
- file: string;
23
- component: {
24
- path: string;
25
- };
26
- }>(entries: readonly T[]): {
27
- valid: T[];
28
- conflicts: {
29
- file: string;
30
- message: string;
31
- }[];
32
- };
33
- /** Whether a file holds an embed: nothing to route. */
34
- export declare const isEmbedLoaded: (component: LoadedComponent) => component is LoadedEmbed;
35
- export { isEmbedFile };
@@ -1,30 +0,0 @@
1
- import { type PieceKind, type Rendered } from './instance.js';
2
- /** The kinds of files of `src/components/`, which are its folders. */
3
- export type ComponentKind = 'button' | 'select' | 'modal' | 'embed';
4
- export declare const COMPONENT_FOLDERS: Record<string, ComponentKind>;
5
- /** A file as the loader knows it: what it declared, and once loaded, how to make instances. */
6
- export interface ComponentFileState {
7
- readonly kind: ComponentKind;
8
- readonly config: unknown;
9
- /** The name of the file for messages, before it is bound: `button()`. */
10
- readonly made: string;
11
- bound?: {
12
- /** The path of the file inside `src/components/`, without extension. */
13
- path: string;
14
- /** Makes an instance from what the file was called with. */
15
- render: (args: readonly unknown[]) => Rendered | object;
16
- };
17
- }
18
- /** The state of a component file, if the value is one. */
19
- export declare function fileStateOf(value: unknown): ComponentFileState | undefined;
20
- /**
21
- * Makes a component file. `asPiece` says whether the file itself stands
22
- * for its instance when it carries no data (a button, a menu, a modal: not
23
- * an embed, which is only a function).
24
- */
25
- export declare function createFile(kind: ComponentKind, config: unknown, { asPiece, pieceKind }: {
26
- asPiece: boolean;
27
- pieceKind?: PieceKind;
28
- }): (...args: unknown[]) => unknown;
29
- /** Binds a file to its path and to what makes its instances. */
30
- export declare function bindFile(file: unknown, bound: NonNullable<ComponentFileState['bound']>): void;