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.
- package/dist/cli/build.js +9 -10
- package/dist/cli/dev.js +29 -16
- package/dist/cli/project.d.ts +38 -25
- package/dist/cli/project.js +199 -134
- package/dist/cli/update-check.d.ts +42 -0
- package/dist/cli/update-check.js +125 -0
- package/dist/commands/command.d.ts +25 -11
- package/dist/commands/command.js +5 -5
- package/dist/commands/{convention.d.ts → declaration.d.ts} +24 -11
- package/dist/commands/{convention.js → declaration.js} +61 -33
- package/dist/commands/tree.d.ts +8 -4
- package/dist/commands/tree.js +16 -6
- package/dist/components/button.d.ts +13 -13
- package/dist/components/button.js +9 -11
- package/dist/components/component.d.ts +1 -1
- package/dist/components/component.js +1 -1
- package/dist/components/custom-id.d.ts +4 -4
- package/dist/components/custom-id.js +12 -12
- package/dist/components/declaration.d.ts +42 -0
- package/dist/components/{convention.js → declaration.js} +76 -73
- package/dist/components/declared.d.ts +29 -0
- package/dist/components/declared.js +49 -0
- package/dist/components/embed.d.ts +13 -12
- package/dist/components/embed.js +6 -6
- package/dist/components/instance.d.ts +3 -3
- package/dist/components/instance.js +2 -2
- package/dist/components/modal.d.ts +11 -10
- package/dist/components/modal.js +7 -8
- package/dist/components/router.d.ts +3 -2
- package/dist/components/router.js +14 -19
- package/dist/components/select.d.ts +14 -12
- package/dist/components/select.js +4 -4
- package/dist/events/declaration.d.ts +16 -0
- package/dist/events/declaration.js +80 -0
- package/dist/events/event.d.ts +42 -5
- package/dist/events/event.js +25 -11
- package/dist/events/registry.d.ts +1 -10
- package/dist/events/router.d.ts +3 -2
- package/dist/index.d.ts +11 -9
- package/dist/index.js +2 -0
- package/dist/loader/generated.d.ts +5 -32
- package/dist/loader/generated.js +21 -55
- package/dist/loader/loader.d.ts +57 -44
- package/dist/loader/loader.js +73 -89
- package/dist/main.d.ts +0 -1
- package/dist/main.js +2 -3
- package/dist/messages/commands.d.ts +3 -3
- package/dist/messages/commands.js +29 -10
- package/dist/messages/declaration.d.ts +89 -0
- package/dist/messages/declaration.js +455 -0
- package/dist/messages/messages.d.ts +65 -43
- package/dist/messages/messages.js +11 -10
- package/dist/messages/phrases.js +1 -1
- package/dist/messages/translate.d.ts +4 -4
- package/dist/messages/translate.js +4 -4
- package/dist/presence/declaration.d.ts +19 -0
- package/dist/presence/{convention.js → declaration.js} +13 -28
- package/dist/presence/presence.d.ts +7 -7
- package/dist/presence/presence.js +5 -5
- package/dist/structures/context.d.ts +1 -1
- package/dist/structures/interaction.d.ts +2 -2
- package/dist/structures/interaction.js +3 -3
- package/dist/structures/payload.d.ts +4 -4
- package/dist/structures/payload.js +4 -4
- package/dist/tasks/declaration.d.ts +16 -0
- package/dist/tasks/{convention.js → declaration.js} +14 -36
- package/dist/tasks/scheduler.d.ts +3 -1
- package/dist/tasks/scheduler.js +4 -4
- package/dist/tasks/task.d.ts +8 -9
- package/dist/tasks/task.js +6 -7
- package/package.json +1 -1
- package/dist/components/convention.d.ts +0 -35
- package/dist/components/file.d.ts +0 -30
- package/dist/components/file.js +0 -52
- package/dist/events/convention.d.ts +0 -25
- package/dist/events/convention.js +0 -97
- package/dist/events/types.d.ts +0 -7
- package/dist/events/types.js +0 -48
- package/dist/messages/convention.d.ts +0 -89
- package/dist/messages/convention.js +0 -347
- package/dist/presence/convention.d.ts +0 -26
- package/dist/tasks/convention.d.ts +0 -26
package/dist/cli/project.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
15
|
+
import { componentDeclaration, findDuplicates, } from '../components/declaration.js';
|
|
17
16
|
import { ComponentRouter } from '../components/router.js';
|
|
18
|
-
import {
|
|
19
|
-
import { DEFAULT_PRESENCE,
|
|
20
|
-
import { assembleMessages, commandsDeclarations,
|
|
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 {
|
|
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 {
|
|
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/`):
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the
|
|
42
|
-
*
|
|
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,
|
|
46
|
-
const [files,
|
|
45
|
+
export async function writeTypes(cwd, info) {
|
|
46
|
+
const [files, found, names] = await Promise.all([
|
|
47
47
|
listPublic(cwd),
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
info ? null : scanLanguages(cwd),
|
|
49
|
+
info ? null : scanCommands(cwd),
|
|
50
50
|
]);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 = (
|
|
87
|
-
const where = locate(error, cwd);
|
|
88
|
-
log.error(`${where ? `${where.file}:${where.line}` :
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
commands = new Map(valid.map(entry => [entry
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
components = new Map(unique.valid.map(entry => [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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
210
|
-
const [first, ...extra] =
|
|
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 (
|
|
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
|
-
|
|
229
|
-
|
|
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(({
|
|
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
|
|
238
|
-
const restored = assembled.failed.filter(
|
|
239
|
-
const
|
|
240
|
-
|
|
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
|
|
244
|
-
|
|
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
|
|
264
|
-
for (const { file, path } of unknownCommands(messages,
|
|
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${
|
|
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 [
|
|
272
|
-
const { command, failed } = translateCommand(
|
|
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(
|
|
338
|
+
commands.set(key, { ...entry, command });
|
|
276
339
|
else
|
|
277
|
-
commands.delete(
|
|
340
|
+
commands.delete(key);
|
|
278
341
|
}
|
|
279
342
|
commandRouter.set(commands.values());
|
|
280
343
|
return [
|
|
281
|
-
...
|
|
282
|
-
...
|
|
283
|
-
...
|
|
284
|
-
...
|
|
285
|
-
...
|
|
286
|
-
...
|
|
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
|
|
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
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const CHANGELOG_URL = "https://www.chapterjs.org/changelog";
|
|
2
|
+
export interface UpdateCheckOptions {
|
|
3
|
+
/** The folder of the project. */
|
|
4
|
+
cwd: string;
|
|
5
|
+
/** The version of the framework that runs. */
|
|
6
|
+
version: string;
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
/** Only tests change these. */
|
|
9
|
+
registryUrl?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
/** A version newer than the one that runs. */
|
|
14
|
+
export interface Update {
|
|
15
|
+
current: string;
|
|
16
|
+
latest: string;
|
|
17
|
+
/** What to type to install it, with the package manager of the project. */
|
|
18
|
+
command: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The command that updates `chapterjs` in a project: with the package
|
|
22
|
+
* manager its lockfile belongs to, else the one that started the command
|
|
23
|
+
* (`pnpm dev` sets `npm_config_user_agent`), else npm, which every
|
|
24
|
+
* machine with Node has.
|
|
25
|
+
*/
|
|
26
|
+
export declare function updateCommand(cwd: string, env: Record<string, string | undefined>): string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether `latest` is a newer version than `current`. Only the three
|
|
29
|
+
* numbers count: `1.0.0-beta.2` and `1.0.0` are not told apart, so a
|
|
30
|
+
* prerelease never says to install the final version it precedes.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isNewer(latest: string, current: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Whether a newer `chapterjs` is published. The registry is asked at most
|
|
35
|
+
* once a day (`.chapterjs/cache/update.json` keeps the answer), with a
|
|
36
|
+
* short timeout, and never throws: without network, or when the registry
|
|
37
|
+
* does not answer, there is nothing to say. Resolves with the update to
|
|
38
|
+
* install, or `null` when the bot runs the latest version.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkForUpdate(options: UpdateCheckOptions): Promise<Update | null>;
|
|
41
|
+
/** The line that tells the developer about an update. */
|
|
42
|
+
export declare function describeUpdate(update: Update): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Whether a newer chapterjs is published, and the command that installs it:
|
|
2
|
+
// told where the developer starts their bot, so nobody has to watch npm.
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
/** Where the package is published. */
|
|
7
|
+
const NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
8
|
+
/** Asked once a day: a version is not published more often. */
|
|
9
|
+
const CHECK_EVERY = 24 * 60 * 60 * 1000;
|
|
10
|
+
/** A registry that does not answer must never hold the bot. */
|
|
11
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
12
|
+
export const CHANGELOG_URL = 'https://www.chapterjs.org/changelog';
|
|
13
|
+
const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
14
|
+
/** The lockfiles each package manager writes next to `package.json`. */
|
|
15
|
+
const LOCKFILES = {
|
|
16
|
+
pnpm: ['pnpm-lock.yaml'],
|
|
17
|
+
npm: ['package-lock.json', 'npm-shrinkwrap.json'],
|
|
18
|
+
yarn: ['yarn.lock'],
|
|
19
|
+
bun: ['bun.lock', 'bun.lockb'],
|
|
20
|
+
};
|
|
21
|
+
/** What installs the latest version with each package manager. */
|
|
22
|
+
const COMMANDS = {
|
|
23
|
+
pnpm: 'pnpm update chapterjs --latest',
|
|
24
|
+
npm: 'npm install chapterjs@latest',
|
|
25
|
+
yarn: 'yarn add chapterjs@latest',
|
|
26
|
+
bun: 'bun add chapterjs@latest',
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The command that updates `chapterjs` in a project: with the package
|
|
30
|
+
* manager its lockfile belongs to, else the one that started the command
|
|
31
|
+
* (`pnpm dev` sets `npm_config_user_agent`), else npm, which every
|
|
32
|
+
* machine with Node has.
|
|
33
|
+
*/
|
|
34
|
+
export function updateCommand(cwd, env) {
|
|
35
|
+
const fromLockfile = PACKAGE_MANAGERS.find(pm => LOCKFILES[pm].some(name => existsSync(join(cwd, name))));
|
|
36
|
+
const agent = env.npm_config_user_agent?.split('/')[0];
|
|
37
|
+
const fromAgent = PACKAGE_MANAGERS.find(pm => pm === agent);
|
|
38
|
+
return COMMANDS[fromLockfile ?? fromAgent ?? 'npm'];
|
|
39
|
+
}
|
|
40
|
+
/** `1.2.3`, with or without a prerelease suffix, as numbers. */
|
|
41
|
+
function parseVersion(version) {
|
|
42
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
43
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether `latest` is a newer version than `current`. Only the three
|
|
47
|
+
* numbers count: `1.0.0-beta.2` and `1.0.0` are not told apart, so a
|
|
48
|
+
* prerelease never says to install the final version it precedes.
|
|
49
|
+
*/
|
|
50
|
+
export function isNewer(latest, current) {
|
|
51
|
+
const a = parseVersion(latest);
|
|
52
|
+
const b = parseVersion(current);
|
|
53
|
+
if (!a || !b)
|
|
54
|
+
return false;
|
|
55
|
+
for (let i = 0; i < 3; i++) {
|
|
56
|
+
if (a[i] !== b[i])
|
|
57
|
+
return a[i] > b[i];
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/** The version the registry publishes as `latest`, or `null`. */
|
|
62
|
+
async function fetchLatest(registryUrl, timeout) {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(`${registryUrl}/chapterjs/latest`, {
|
|
65
|
+
headers: { accept: 'application/json' },
|
|
66
|
+
signal: AbortSignal.timeout(timeout),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
return null;
|
|
70
|
+
const { version } = (await response.json());
|
|
71
|
+
return typeof version === 'string' && parseVersion(version)
|
|
72
|
+
? version
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether a newer `chapterjs` is published. The registry is asked at most
|
|
81
|
+
* once a day (`.chapterjs/cache/update.json` keeps the answer), with a
|
|
82
|
+
* short timeout, and never throws: without network, or when the registry
|
|
83
|
+
* does not answer, there is nothing to say. Resolves with the update to
|
|
84
|
+
* install, or `null` when the bot runs the latest version.
|
|
85
|
+
*/
|
|
86
|
+
export async function checkForUpdate(options) {
|
|
87
|
+
const { cwd, version, env } = options;
|
|
88
|
+
const now = options.now ?? Date.now;
|
|
89
|
+
const file = join(cwd, '.chapterjs', 'cache', 'update.json');
|
|
90
|
+
const cached = await readFile(file, 'utf8')
|
|
91
|
+
.then((text) => {
|
|
92
|
+
const value = JSON.parse(text);
|
|
93
|
+
return value &&
|
|
94
|
+
typeof value.checked === 'number' &&
|
|
95
|
+
typeof value.latest === 'string'
|
|
96
|
+
? { checked: value.checked, latest: value.latest }
|
|
97
|
+
: null;
|
|
98
|
+
})
|
|
99
|
+
.catch(() => null);
|
|
100
|
+
let latest;
|
|
101
|
+
if (cached && now() - cached.checked < CHECK_EVERY) {
|
|
102
|
+
latest = cached.latest;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
latest = await fetchLatest(options.registryUrl ?? env.CHAPTERJS_REGISTRY_URL ?? NPM_REGISTRY, options.timeout ?? DEFAULT_TIMEOUT);
|
|
106
|
+
if (latest) {
|
|
107
|
+
// A folder or a disk that can't be written changes nothing: the
|
|
108
|
+
// registry is asked again next time.
|
|
109
|
+
await mkdir(dirname(file), { recursive: true })
|
|
110
|
+
.then(() => writeFile(file, JSON.stringify({ checked: now(), latest })))
|
|
111
|
+
.catch(() => { });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Yesterday's answer is better than none.
|
|
115
|
+
latest = cached?.latest ?? null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!latest || !isNewer(latest, version))
|
|
119
|
+
return null;
|
|
120
|
+
return { current: version, latest, command: updateCommand(cwd, env) };
|
|
121
|
+
}
|
|
122
|
+
/** The line that tells the developer about an update. */
|
|
123
|
+
export function describeUpdate(update) {
|
|
124
|
+
return `A new version of chapterjs is out: ${update.latest} (you have ${update.current}). To update, run:\n${update.command}\nWhat changed: ${CHANGELOG_URL}`;
|
|
125
|
+
}
|