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