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
@@ -6,64 +6,66 @@ import { stat } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
7
  import { setPublicDir } from '../assets/asset.js';
8
8
  import { listPublic, publicDeclarations } from '../assets/public.js';
9
- import { eventTypedFolders } from '../events/types.js';
10
9
  import { MissingForEvent } from '../events/registry.js';
11
10
  import { sourcesJoinVoice } from '../voice/usage.js';
12
11
  import { writeGenerated } from '../loader/generated.js';
13
- import { commandsConvention } from '../commands/convention.js';
12
+ import { commandDeclaration, scanCommands } from '../commands/declaration.js';
14
13
  import { CommandRouter } from '../commands/router.js';
15
14
  import { findConflicts } from '../commands/tree.js';
16
- import { componentsConvention, findDuplicates, } from '../components/convention.js';
15
+ import { componentDeclaration, findDuplicates, } from '../components/declaration.js';
17
16
  import { ComponentRouter } from '../components/router.js';
18
- import { tasksConvention } from '../tasks/convention.js';
19
- import { DEFAULT_PRESENCE, presenceConvention, } from '../presence/convention.js';
20
- import { assembleMessages, commandsDeclarations, languagesConvention, languageTypedFolders, messagesDeclarations, } from '../messages/convention.js';
17
+ import { taskDeclaration } from '../tasks/declaration.js';
18
+ import { DEFAULT_PRESENCE, presenceDeclaration, } from '../presence/declaration.js';
19
+ import { assembleMessages, commandsDeclarations, languageDeclaration, languageDefaultsDeclaration, messagesDeclarations, scanLanguages, } from '../messages/declaration.js';
21
20
  import { translation } from '../messages/translate.js';
22
21
  import { translateCommand, unknownCommands } from '../messages/commands.js';
23
22
  import { TimerScheduler, } from '../tasks/scheduler.js';
24
23
  import { createBot } from '../core/bot.js';
25
24
  import { watchMemory } from '../core/memory.js';
26
25
  import { GatewayIntent } from '../discord/intents.js';
27
- import { eventsConvention } from '../events/convention.js';
26
+ import { eventDeclaration } from '../events/declaration.js';
28
27
  import { EventRouter, intentsFor, intentsOf, limitsFor, } from '../events/router.js';
29
28
  import { GatewayFatalError, SessionLimitError } from '../gateway/errors.js';
30
- import { listFolder, loadBuilt, loadFolder, } from '../loader/loader.js';
29
+ import { loadSources, readDeclarations, siteName, } from '../loader/loader.js';
31
30
  import { locate, messageOf } from '../loader/locate.js';
32
31
  import { DiscordApiError, DiscordUnavailableError, InvalidTokenError, } from '../rest/errors.js';
33
32
  import { PreflightFailure } from './preflight.js';
34
33
  /**
35
- * Writes the types of a project (`.chapterjs/`): one project per typed
36
- * folder (every event, every language file), and what every file gets,
37
- * like the files of `public/`. With the commands and the languages as
38
- * they loaded, a language file only offers the ones without `description`
39
- * in their file, `t` is typed from the default language and `default:
40
- * true` is refused in every file when another one says it (`defaults`,
41
- * the files that do); without them (`sync`, which runs nothing),
42
- * every command is offered and the first language file types `t`.
34
+ * Writes the types of a project (`.chapterjs/`): what the project adds to
35
+ * 'chapterjs', like the files of `public/`, its languages and its
36
+ * commands. With the project as it loaded (`info`), a language only
37
+ * offers the commands without `description`, `t` is typed from the
38
+ * default language and `default: true` is refused in every language that
39
+ * says it when another one does; without it (`sync`, which runs nothing),
40
+ * the commands and the languages are found by reading the files, every
41
+ * command is offered, and the first language or the one that says
42
+ * `default: true` types `t`.
43
43
  * @returns how many files were written
44
44
  */
45
- export async function writeTypes(cwd, commands, messages, defaults = []) {
46
- const [files, languageFiles, commandFiles] = await Promise.all([
45
+ export async function writeTypes(cwd, info) {
46
+ const [files, found, names] = await Promise.all([
47
47
  listPublic(cwd),
48
- listFolder(cwd, languagesConvention),
49
- listFolder(cwd, commandsConvention),
48
+ info ? null : scanLanguages(cwd),
49
+ info ? null : scanCommands(cwd),
50
50
  ]);
51
- return writeGenerated(cwd, [...eventTypedFolders(), ...languageTypedFolders(languageFiles, defaults)], publicDeclarations(files) +
52
- messagesDeclarations(languageFiles, messages ? messages.files.get(messages.default) : undefined),
53
- // Only language files use it, and they are in the main project.
54
- commandsDeclarations(commandFiles.map(({ file }) => ({
55
- file,
56
- described: commands?.get(file)?.command.described ?? false,
57
- }))));
51
+ const languages = info ? info.languages : found.languages;
52
+ const defaultLocale = info ? info.defaultLocale : found.defaultLocale;
53
+ const commands = info
54
+ ? info.commands
55
+ : names.map(name => ({ name, described: false }));
56
+ return writeGenerated(cwd, publicDeclarations(files) +
57
+ messagesDeclarations(languages, defaultLocale) +
58
+ languageDefaultsDeclaration(info?.languageDefaults ?? []) +
59
+ commandsDeclarations(commands));
58
60
  }
59
- /** The conventional folders of a project: one per feature. */
60
- export const CONVENTIONS = [
61
- eventsConvention,
62
- commandsConvention,
63
- componentsConvention,
64
- tasksConvention,
65
- presenceConvention,
66
- languagesConvention,
61
+ /** The kinds of declarations a project is made of: one per feature. */
62
+ export const DECLARATIONS = [
63
+ eventDeclaration,
64
+ commandDeclaration,
65
+ componentDeclaration,
66
+ taskDeclaration,
67
+ presenceDeclaration,
68
+ languageDeclaration,
67
69
  ];
68
70
  /** Whether the folder is a project: it has a `src` folder. */
69
71
  export async function hasSources(cwd, log) {
@@ -79,13 +81,45 @@ const PRIVATE_INTENTS = GatewayIntent.DirectMessages |
79
81
  GatewayIntent.DirectMessageTyping |
80
82
  GatewayIntent.DirectMessagePolls;
81
83
  const megabytes = (bytes) => `${Math.round(bytes / 1024 / 1024)} MB`;
84
+ /** What tells one declaration from another: its file and its export. */
85
+ const keyOf = (site) => `${site.file}#${site.export}`;
86
+ /**
87
+ * Two declarations with the same name, where the name is the export's
88
+ * (a task): the later one is left out, with a message saying so.
89
+ */
90
+ function sameNames(entries, nameOf, what) {
91
+ const seen = new Map();
92
+ const conflicts = [];
93
+ const valid = [...entries]
94
+ .sort((a, b) => a.file === b.file
95
+ ? a.export < b.export
96
+ ? -1
97
+ : 1
98
+ : a.file < b.file
99
+ ? -1
100
+ : 1)
101
+ .filter(entry => {
102
+ const first = seen.get(nameOf(entry));
103
+ if (!first) {
104
+ seen.set(nameOf(entry), entry);
105
+ return true;
106
+ }
107
+ conflicts.push({
108
+ file: entry.file,
109
+ export: entry.export,
110
+ message: `There is already a ${what} named ${nameOf(entry)}, in ${first.file}: the name of the export is the name of the ${what}, so rename one of them.`,
111
+ });
112
+ return false;
113
+ });
114
+ return { valid, conflicts };
115
+ }
82
116
  export function createProject(options) {
83
117
  const { cwd, log } = options;
84
118
  // Where `asset()` reads the files of the project from.
85
119
  setPublicDir(cwd);
86
- const report = ({ file, error }) => {
87
- const where = locate(error, cwd);
88
- log.error(`${where ? `${where.file}:${where.line}` : file} ${messageOf(error)}`);
120
+ const report = (failure) => {
121
+ const where = locate(failure.error, cwd);
122
+ log.error(`${where ? `${where.file}:${where.line}` : siteName(failure)} ${messageOf(failure.error)}`);
89
123
  };
90
124
  const router = new EventRouter((file, error) => report({ file, error }), (event, error) => {
91
125
  const missing = error instanceof MissingForEvent ? error : null;
@@ -109,11 +143,13 @@ export function createProject(options) {
109
143
  let components = new Map();
110
144
  let tasks = new Map();
111
145
  let presence = null;
146
+ /** The files the presence was declared in, to keep it when they break. */
147
+ let presenceFiles = new Set();
112
148
  // Joining voice needs the bot's own voice state, sent with an intent.
113
149
  let voice = options.joinsVoice ?? false;
114
150
  const neededIntents = () => intentsFor(events.values()) | (voice ? GatewayIntent.GuildVoiceStates : 0);
115
151
  let messages = null;
116
- /** The last version of each language file that loaded. */
152
+ /** The last version of each language that loaded. */
117
153
  let languages = new Map();
118
154
  let languageDefaults = [];
119
155
  /** The bot running the files now, to give it what a reload changes. */
@@ -141,107 +177,134 @@ export function createProject(options) {
141
177
  get messages() {
142
178
  return messages;
143
179
  },
144
- get languageDefaults() {
145
- return languageDefaults;
146
- },
180
+ typesInfo: () => ({
181
+ languages: [...languages.values()].map(entry => ({
182
+ file: entry.file,
183
+ export: entry.export,
184
+ name: entry.name,
185
+ locale: entry.language.locale,
186
+ })),
187
+ ...(messages ? { defaultLocale: messages.default } : {}),
188
+ languageDefaults,
189
+ commands: [...commands.values()].map(({ command }) => ({
190
+ name: command.path.join(' '),
191
+ described: command.described,
192
+ })),
193
+ }),
147
194
  report,
148
195
  async load() {
149
196
  const { built } = options;
150
197
  if (options.joinsVoice === undefined) {
151
198
  voice = await sourcesJoinVoice(cwd);
152
199
  }
153
- const [eventFiles, commandFiles, componentFiles, taskFiles, presenceFiles, messageFiles,] = await Promise.all(built
154
- ? [
155
- loadBuilt(eventsConvention, built),
156
- loadBuilt(commandsConvention, built),
157
- loadBuilt(componentsConvention, built),
158
- loadBuilt(tasksConvention, built),
159
- loadBuilt(presenceConvention, built),
160
- loadBuilt(languagesConvention, built),
161
- ]
162
- : [
163
- loadFolder(cwd, eventsConvention),
164
- loadFolder(cwd, commandsConvention),
165
- loadFolder(cwd, componentsConvention),
166
- loadFolder(cwd, tasksConvention),
167
- loadFolder(cwd, presenceConvention),
168
- loadFolder(cwd, languagesConvention),
169
- ]);
170
- const nextEvents = new Map();
171
- for (const { file, value } of eventFiles.loaded) {
172
- nextEvents.set(file, { file, event: value });
173
- }
174
- for (const { file } of eventFiles.failed) {
175
- const previous = events.get(file);
176
- if (previous)
177
- nextEvents.set(file, previous);
178
- }
179
- events = nextEvents;
200
+ // Every file of src/ runs (or ran, in a build); what each exports is
201
+ // then read, kind by kind.
202
+ const sources = built ? { modules: built, failed: [] } : await loadSources(cwd);
203
+ const read = (declaration) => readDeclarations(sources.modules, declaration);
204
+ const [eventItems, commandItems, componentItems, taskItems, presenceItems, languageItems,] = [
205
+ read(eventDeclaration),
206
+ read(commandDeclaration),
207
+ read(componentDeclaration),
208
+ read(taskDeclaration),
209
+ read(presenceDeclaration),
210
+ read(languageDeclaration),
211
+ ];
212
+ const broken = new Set(sources.failed.map(({ file }) => file));
213
+ /**
214
+ * The next version of a kind: what loaded now, plus the last good
215
+ * version of what failed (an export that can't be read, or a file
216
+ * that can't run at all).
217
+ */
218
+ const next = (previous, items, make) => {
219
+ const result = new Map();
220
+ for (const item of items.loaded)
221
+ result.set(keyOf(item), make(item));
222
+ const failedKeys = new Set(items.failed.map(({ file, export: exported }) => keyOf({ file, export: exported ?? 'default' })));
223
+ for (const [key, entry] of previous) {
224
+ if (failedKeys.has(key) || broken.has(entry.file)) {
225
+ result.set(key, entry);
226
+ }
227
+ }
228
+ return result;
229
+ };
230
+ events = next(events, eventItems, item => ({
231
+ file: item.file,
232
+ export: item.export,
233
+ event: item.value,
234
+ }));
180
235
  router.set(events.values());
181
- const entries = commandFiles.loaded.map(({ file, value }) => ({ file, command: value }));
182
- for (const { file } of commandFiles.failed) {
183
- const previous = commands.get(file);
184
- if (previous)
185
- entries.push(previous);
186
- }
187
- const { valid, conflicts } = findConflicts(entries);
188
- commands = new Map(valid.map(entry => [entry.file, entry]));
189
- const pieces = componentFiles.loaded.map(({ file, value }) => ({ file, component: value }));
190
- for (const { file } of componentFiles.failed) {
191
- const previous = components.get(file);
192
- if (previous)
193
- pieces.push(previous);
194
- }
195
- const unique = findDuplicates(pieces);
196
- components = new Map(unique.valid.map(entry => [entry.file, entry]));
236
+ const { valid, conflicts } = findConflicts([
237
+ ...next(commands, commandItems, item => ({
238
+ file: item.file,
239
+ export: item.export,
240
+ command: item.value,
241
+ })).values(),
242
+ ]);
243
+ commands = new Map(valid.map(entry => [keyOf(entry), entry]));
244
+ const unique = findDuplicates([
245
+ ...next(components, componentItems, item => ({
246
+ file: item.file,
247
+ export: item.export,
248
+ component: item.value,
249
+ })).values(),
250
+ ]);
251
+ components = new Map(unique.valid.map(entry => [keyOf(entry), entry]));
197
252
  componentRouter.set(components.values());
198
- const nextTasks = new Map();
199
- for (const { file, value } of taskFiles.loaded) {
200
- nextTasks.set(file, { file, task: value });
201
- }
202
- for (const { file } of taskFiles.failed) {
203
- const previous = tasks.get(file);
204
- if (previous)
205
- nextTasks.set(file, previous);
206
- }
207
- tasks = nextTasks;
253
+ const sameTask = sameNames([
254
+ ...next(tasks, taskItems, item => ({
255
+ file: item.file,
256
+ export: item.export,
257
+ task: item.value,
258
+ })).values(),
259
+ ], entry => entry.task.name, 'task');
260
+ tasks = new Map(sameTask.valid.map(entry => [keyOf(entry), entry]));
208
261
  scheduler.set(tasks.values());
209
- // One presence file. A second one (another extension) is left out.
210
- const [first, ...extra] = presenceFiles.loaded;
262
+ // One presence. A second one is left out.
263
+ const [first, ...extra] = presenceItems.loaded;
211
264
  if (first)
212
265
  presence = first.value;
213
- else if (presenceFiles.failed.length === 0)
266
+ else if (presenceItems.failed.length === 0 &&
267
+ ![...broken].some(file => presenceFiles.has(file))) {
214
268
  presence = null;
215
- const twice = extra.map(({ file }) => ({
216
- file,
217
- error: new TypeError(`There are two presence files: ${first.file} is used, keep only one.`),
218
- }));
219
- const nextLanguages = new Map();
220
- for (const { file, value } of messageFiles.loaded) {
221
- nextLanguages.set(file, { file, language: value });
222
- }
223
- for (const { file } of messageFiles.failed) {
224
- const previous = languages.get(file);
225
- if (previous)
226
- nextLanguages.set(file, previous);
227
269
  }
228
- // What the files say now, before a file is put back to its last good
229
- // version: two files saying default: true are both underlined.
270
+ presenceFiles = new Set(presenceItems.loaded.map(({ file }) => file));
271
+ const twice = extra.map(item => ({
272
+ file: item.file,
273
+ export: item.export,
274
+ error: new TypeError(`There are two presences: the one of ${siteName(first)} is used, keep only one.`),
275
+ }));
276
+ const nextLanguages = next(languages, languageItems, item => ({
277
+ file: item.file,
278
+ export: item.export,
279
+ name: item.name,
280
+ language: item.value,
281
+ }));
282
+ // What the files say now, before a declaration is put back to its
283
+ // last good version: two languages saying default: true are both
284
+ // underlined.
230
285
  languageDefaults = [...nextLanguages.values()]
231
286
  .filter(({ language }) => language.isDefault)
232
- .map(({ file }) => file);
287
+ .map(({ language }) => language.locale);
233
288
  const previousLanguages = languages;
234
289
  languages = nextLanguages;
235
290
  let assembled = assembleMessages([...languages.values()]);
236
291
  // A language that no longer matches the others keeps its last version
237
- // that did, like a file that no longer loads.
238
- const restored = assembled.failed.filter(({ file }) => {
239
- const previous = previousLanguages.get(file);
240
- return previous !== undefined && previous !== languages.get(file);
292
+ // that did, like a declaration that no longer loads.
293
+ const restored = assembled.failed.filter(failure => {
294
+ const key = keyOf({
295
+ file: failure.file,
296
+ export: failure.export ?? 'default',
297
+ });
298
+ const previous = previousLanguages.get(key);
299
+ return previous !== undefined && previous !== languages.get(key);
241
300
  });
242
301
  if (restored.length > 0) {
243
- for (const { file } of restored) {
244
- languages.set(file, previousLanguages.get(file));
302
+ for (const failure of restored) {
303
+ const key = keyOf({
304
+ file: failure.file,
305
+ export: failure.export ?? 'default',
306
+ });
307
+ languages.set(key, previousLanguages.get(key));
245
308
  }
246
309
  const again = assembleMessages([...languages.values()]);
247
310
  assembled = {
@@ -260,34 +323,36 @@ export function createProject(options) {
260
323
  // be described is left out.
261
324
  const badTexts = [...assembled.failed];
262
325
  if (messages) {
263
- const paths = new Set([...commands.values()].map(entry => entry.command.path.join('/')));
264
- for (const { file, path } of unknownCommands(messages, paths)) {
326
+ const names = new Set([...commands.values()].map(entry => entry.command.path.join(' ')));
327
+ for (const { file, path } of unknownCommands(messages, names)) {
265
328
  badTexts.push({
266
329
  file,
267
- error: new TypeError(`"commands" translates "${path}", which is not a command of this project${paths.size > 0 ? ` (its commands are: ${[...paths].join(', ')})` : ''}. The key is the path of the command file, like 'ping' or 'mod/ban'.`),
330
+ error: new TypeError(`"commands" translates "${path}", which is not a command of this project${names.size > 0 ? ` (its commands are: ${[...names].join(', ')})` : ''}. The key is the name of the command, like 'ping' or 'mod ban'.`),
268
331
  });
269
332
  }
270
333
  }
271
- for (const [file, entry] of commands) {
272
- const { command, failed } = translateCommand(file, entry.command, messages);
334
+ for (const [key, entry] of commands) {
335
+ const { command, failed } = translateCommand(siteName(entry), entry.command, messages);
273
336
  badTexts.push(...failed);
274
337
  if (command)
275
- commands.set(file, { ...entry, command });
338
+ commands.set(key, { ...entry, command });
276
339
  else
277
- commands.delete(file);
340
+ commands.delete(key);
278
341
  }
279
342
  commandRouter.set(commands.values());
280
343
  return [
281
- ...eventFiles.failed,
282
- ...commandFiles.failed,
283
- ...componentFiles.failed,
284
- ...taskFiles.failed,
285
- ...presenceFiles.failed,
286
- ...messageFiles.failed,
344
+ ...sources.failed,
345
+ ...eventItems.failed,
346
+ ...commandItems.failed,
347
+ ...componentItems.failed,
348
+ ...taskItems.failed,
349
+ ...presenceItems.failed,
350
+ ...languageItems.failed,
287
351
  ...badTexts,
288
352
  ...twice,
289
- ...[...conflicts, ...unique.conflicts].map(({ file, message }) => ({
353
+ ...[...conflicts, ...unique.conflicts, ...sameTask.conflicts].map(({ file, export: exported, message }) => ({
290
354
  file,
355
+ export: exported,
291
356
  error: new TypeError(message),
292
357
  })),
293
358
  ];
@@ -299,7 +364,7 @@ export function createProject(options) {
299
364
  tasks.size === 0 &&
300
365
  presence === null &&
301
366
  messages === null) {
302
- return 'Nothing to run yet: add a file in src/commands/ or in a folder like src/events/messageCreate/';
367
+ return "Nothing to run yet: export a command or an event from a file of src/, like export default command({ name: 'ping', ... })";
303
368
  }
304
369
  return `${[
305
370
  ...(commands.size > 0
@@ -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[]>;