chapterjs 0.1.1 → 0.1.4
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/assets/asset.d.ts +46 -0
- package/dist/assets/asset.js +86 -0
- package/dist/assets/public.d.ts +19 -0
- package/dist/assets/public.js +120 -0
- package/dist/cache/cache.d.ts +62 -0
- package/dist/cache/cache.js +80 -0
- package/dist/cache/store.d.ts +45 -0
- package/dist/cache/store.js +53 -0
- package/dist/cli/build.d.ts +41 -0
- package/dist/cli/build.js +206 -0
- package/dist/cli/cli.js +108 -25
- package/dist/cli/cluster.d.ts +70 -0
- package/dist/cli/cluster.js +213 -0
- package/dist/cli/dev.d.ts +18 -1
- package/dist/cli/dev.js +252 -198
- package/dist/cli/env.d.ts +31 -0
- package/dist/cli/env.js +93 -0
- package/dist/cli/log.d.ts +13 -0
- package/dist/cli/log.js +17 -0
- package/dist/cli/node-version.d.ts +8 -0
- package/dist/cli/node-version.js +13 -0
- package/dist/cli/preflight.d.ts +36 -0
- package/dist/cli/preflight.js +122 -0
- package/dist/cli/project.d.ts +122 -0
- package/dist/cli/project.js +452 -0
- package/dist/cli/start.d.ts +25 -0
- package/dist/cli/start.js +225 -0
- package/dist/commands/autocomplete.d.ts +13 -0
- package/dist/commands/autocomplete.js +159 -0
- package/dist/commands/command.d.ts +346 -0
- package/dist/commands/command.js +28 -0
- package/dist/commands/convention.d.ts +64 -0
- package/dist/commands/convention.js +464 -0
- package/dist/commands/register.d.ts +18 -0
- package/dist/commands/register.js +42 -0
- package/dist/commands/router.d.ts +20 -0
- package/dist/commands/router.js +228 -0
- package/dist/commands/tree.d.ts +43 -0
- package/dist/commands/tree.js +206 -0
- package/dist/commands/twins.d.ts +15 -0
- package/dist/commands/twins.js +78 -0
- package/dist/components/button.d.ts +79 -0
- package/dist/components/button.js +108 -0
- package/dist/components/component.d.ts +140 -0
- package/dist/components/component.js +39 -0
- package/dist/components/convention.d.ts +35 -0
- package/dist/components/convention.js +339 -0
- package/dist/components/custom-id.d.ts +72 -0
- package/dist/components/custom-id.js +128 -0
- package/dist/components/embed.d.ts +28 -0
- package/dist/components/embed.js +11 -0
- package/dist/components/file.d.ts +30 -0
- package/dist/components/file.js +52 -0
- package/dist/components/instance.d.ts +100 -0
- package/dist/components/instance.js +86 -0
- package/dist/components/layout.d.ts +103 -0
- package/dist/components/layout.js +279 -0
- package/dist/components/modal.d.ts +244 -0
- package/dist/components/modal.js +399 -0
- package/dist/components/options.d.ts +41 -0
- package/dist/components/options.js +101 -0
- package/dist/components/render.d.ts +12 -0
- package/dist/components/render.js +58 -0
- package/dist/components/router.d.ts +18 -0
- package/dist/components/router.js +256 -0
- package/dist/components/select.d.ts +144 -0
- package/dist/components/select.js +179 -0
- package/dist/core/bot.d.ts +99 -0
- package/dist/core/bot.js +174 -0
- package/dist/core/memory.d.ts +66 -0
- package/dist/core/memory.js +115 -0
- package/dist/discord/api.d.ts +112 -0
- package/dist/discord/api.js +115 -0
- package/dist/discord/cdn.d.ts +67 -0
- package/dist/discord/cdn.js +100 -0
- package/dist/discord/codes.d.ts +171 -0
- package/dist/discord/codes.js +421 -0
- package/dist/discord/endpoint.d.ts +56 -0
- package/dist/discord/endpoint.js +53 -0
- package/dist/discord/endpoints.d.ts +1529 -0
- package/dist/discord/endpoints.js +1139 -0
- package/dist/discord/formatting.d.ts +67 -0
- package/dist/discord/formatting.js +87 -0
- package/dist/discord/index.d.ts +10 -0
- package/dist/discord/index.js +13 -0
- package/dist/discord/intents.d.ts +48 -0
- package/dist/discord/intents.js +128 -0
- package/dist/discord/permissions.d.ts +94 -0
- package/dist/discord/permissions.js +185 -0
- package/dist/discord/snowflake.d.ts +21 -0
- package/dist/discord/snowflake.js +36 -0
- package/dist/discord/types/application-command.d.ts +332 -0
- package/dist/discord/types/application-command.js +48 -0
- package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
- package/dist/discord/types/application-role-connection-metadata.js +22 -0
- package/dist/discord/types/application.d.ts +208 -0
- package/dist/discord/types/application.js +48 -0
- package/dist/discord/types/audit-log.d.ts +255 -0
- package/dist/discord/types/audit-log.js +144 -0
- package/dist/discord/types/auto-moderation.d.ts +167 -0
- package/dist/discord/types/auto-moderation.js +52 -0
- package/dist/discord/types/channel.d.ts +557 -0
- package/dist/discord/types/channel.js +88 -0
- package/dist/discord/types/common.d.ts +54 -0
- package/dist/discord/types/common.js +38 -0
- package/dist/discord/types/component.d.ts +775 -0
- package/dist/discord/types/component.js +76 -0
- package/dist/discord/types/emoji.d.ts +74 -0
- package/dist/discord/types/emoji.js +1 -0
- package/dist/discord/types/entitlement.d.ts +84 -0
- package/dist/discord/types/entitlement.js +22 -0
- package/dist/discord/types/gateway-events.d.ts +1148 -0
- package/dist/discord/types/gateway-events.js +54 -0
- package/dist/discord/types/gateway.d.ts +38 -0
- package/dist/discord/types/gateway.js +1 -0
- package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
- package/dist/discord/types/guild-scheduled-event.js +68 -0
- package/dist/discord/types/guild-template.d.ts +51 -0
- package/dist/discord/types/guild-template.js +1 -0
- package/dist/discord/types/guild.d.ts +910 -0
- package/dist/discord/types/guild.js +153 -0
- package/dist/discord/types/index.d.ts +31 -0
- package/dist/discord/types/index.js +31 -0
- package/dist/discord/types/interaction.d.ts +321 -0
- package/dist/discord/types/interaction.js +47 -0
- package/dist/discord/types/invite.d.ts +158 -0
- package/dist/discord/types/invite.js +39 -0
- package/dist/discord/types/message.d.ts +834 -0
- package/dist/discord/types/message.js +159 -0
- package/dist/discord/types/oauth2.d.ts +22 -0
- package/dist/discord/types/oauth2.js +1 -0
- package/dist/discord/types/permissions.d.ts +183 -0
- package/dist/discord/types/permissions.js +118 -0
- package/dist/discord/types/poll.d.ts +106 -0
- package/dist/discord/types/poll.js +8 -0
- package/dist/discord/types/sku.d.ts +47 -0
- package/dist/discord/types/sku.js +26 -0
- package/dist/discord/types/soundboard.d.ts +71 -0
- package/dist/discord/types/soundboard.js +1 -0
- package/dist/discord/types/stage-instance.d.ts +58 -0
- package/dist/discord/types/stage-instance.js +10 -0
- package/dist/discord/types/sticker.d.ts +115 -0
- package/dist/discord/types/sticker.js +20 -0
- package/dist/discord/types/subscription.d.ts +54 -0
- package/dist/discord/types/subscription.js +12 -0
- package/dist/discord/types/team.d.ts +55 -0
- package/dist/discord/types/team.js +21 -0
- package/dist/discord/types/user.d.ts +273 -0
- package/dist/discord/types/user.js +83 -0
- package/dist/discord/types/voice.d.ts +72 -0
- package/dist/discord/types/voice.js +1 -0
- package/dist/discord/types/webhook.d.ts +184 -0
- package/dist/discord/types/webhook.js +12 -0
- package/dist/events/convention.d.ts +25 -0
- package/dist/events/convention.js +97 -0
- package/dist/events/event.d.ts +8 -0
- package/dist/events/event.js +20 -0
- package/dist/events/registry.d.ts +706 -0
- package/dist/events/registry.js +839 -0
- package/dist/events/router.d.ts +45 -0
- package/dist/events/router.js +133 -0
- package/dist/events/types.d.ts +7 -0
- package/dist/events/types.js +48 -0
- package/dist/gateway/errors.d.ts +18 -0
- package/dist/gateway/errors.js +47 -0
- package/dist/gateway/gateway.d.ts +64 -0
- package/dist/gateway/gateway.js +117 -0
- package/dist/gateway/identify-queue.d.ts +25 -0
- package/dist/gateway/identify-queue.js +29 -0
- package/dist/gateway/shard.d.ts +94 -0
- package/dist/gateway/shard.js +414 -0
- package/dist/gateway/state.d.ts +11 -0
- package/dist/gateway/state.js +184 -0
- package/dist/index.d.ts +55 -18
- package/dist/index.js +38 -12
- package/dist/interactions/cooldown.d.ts +33 -0
- package/dist/interactions/cooldown.js +74 -0
- package/dist/interactions/dispatch.d.ts +91 -0
- package/dist/interactions/dispatch.js +110 -0
- package/dist/loader/generated.d.ts +37 -0
- package/dist/loader/generated.js +114 -0
- package/dist/loader/hot.d.ts +13 -0
- package/dist/loader/hot.js +85 -0
- package/dist/loader/loader.d.ts +72 -0
- package/dist/loader/loader.js +135 -0
- package/dist/loader/locate.d.ts +10 -0
- package/dist/loader/locate.js +36 -0
- package/dist/loader/watch.d.ts +17 -0
- package/dist/loader/watch.js +66 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +6 -0
- package/dist/messages/commands.d.ts +23 -0
- package/dist/messages/commands.js +123 -0
- package/dist/messages/convention.d.ts +89 -0
- package/dist/messages/convention.js +347 -0
- package/dist/messages/messages.d.ts +219 -0
- package/dist/messages/messages.js +30 -0
- package/dist/messages/phrases.d.ts +23 -0
- package/dist/messages/phrases.js +44 -0
- package/dist/messages/translate.d.ts +100 -0
- package/dist/messages/translate.js +249 -0
- package/dist/presence/convention.d.ts +26 -0
- package/dist/presence/convention.js +133 -0
- package/dist/presence/presence.d.ts +57 -0
- package/dist/presence/presence.js +27 -0
- package/dist/rest/errors.d.ts +55 -0
- package/dist/rest/errors.js +92 -0
- package/dist/rest/rate-limiter.d.ts +46 -0
- package/dist/rest/rate-limiter.js +153 -0
- package/dist/rest/rest.d.ts +63 -0
- package/dist/rest/rest.js +194 -0
- package/dist/structures/base.d.ts +48 -0
- package/dist/structures/base.js +84 -0
- package/dist/structures/channel.d.ts +380 -0
- package/dist/structures/channel.js +627 -0
- package/dist/structures/context.d.ts +30 -0
- package/dist/structures/context.js +7 -0
- package/dist/structures/emoji.d.ts +46 -0
- package/dist/structures/emoji.js +65 -0
- package/dist/structures/entities.d.ts +62 -0
- package/dist/structures/entities.js +226 -0
- package/dist/structures/guild.d.ts +348 -0
- package/dist/structures/guild.js +466 -0
- package/dist/structures/interaction.d.ts +197 -0
- package/dist/structures/interaction.js +396 -0
- package/dist/structures/invite.d.ts +38 -0
- package/dist/structures/invite.js +57 -0
- package/dist/structures/known.d.ts +28 -0
- package/dist/structures/known.js +58 -0
- package/dist/structures/member.d.ts +140 -0
- package/dist/structures/member.js +260 -0
- package/dist/structures/message.d.ts +197 -0
- package/dist/structures/message.js +268 -0
- package/dist/structures/payload.d.ts +91 -0
- package/dist/structures/payload.js +184 -0
- package/dist/structures/role.d.ts +69 -0
- package/dist/structures/role.js +112 -0
- package/dist/structures/user.d.ts +59 -0
- package/dist/structures/user.js +94 -0
- package/dist/structures/webhook.d.ts +46 -0
- package/dist/structures/webhook.js +79 -0
- package/dist/tasks/convention.d.ts +26 -0
- package/dist/tasks/convention.js +78 -0
- package/dist/tasks/schedule.d.ts +54 -0
- package/dist/tasks/schedule.js +236 -0
- package/dist/tasks/scheduler.d.ts +33 -0
- package/dist/tasks/scheduler.js +107 -0
- package/dist/tasks/task.d.ts +76 -0
- package/dist/tasks/task.js +28 -0
- package/dist/util/case.d.ts +13 -0
- package/dist/util/case.js +54 -0
- package/dist/util/duration.d.ts +8 -0
- package/dist/util/duration.js +29 -0
- package/dist/util/websocket.d.ts +9 -0
- package/dist/util/websocket.js +31 -0
- package/dist/voice/connection.d.ts +90 -0
- package/dist/voice/connection.js +494 -0
- package/dist/voice/dave.d.ts +109 -0
- package/dist/voice/dave.js +245 -0
- package/dist/voice/opus.d.ts +19 -0
- package/dist/voice/opus.js +273 -0
- package/dist/voice/session.d.ts +55 -0
- package/dist/voice/session.js +405 -0
- package/dist/voice/source.d.ts +22 -0
- package/dist/voice/source.js +195 -0
- package/dist/voice/transport.d.ts +43 -0
- package/dist/voice/transport.js +140 -0
- package/dist/voice/usage.d.ts +4 -0
- package/dist/voice/usage.js +33 -0
- package/package.json +26 -7
- package/vendor/dave/LICENSE +231 -0
- package/vendor/dave/libdave.d.mts +99 -0
- package/vendor/dave/libdave.mjs +2 -0
- package/vendor/dave/libdave.wasm +0 -0
- package/dist/components.d.ts +0 -97
- package/dist/components.js +0 -260
- package/dist/context.d.ts +0 -72
- package/dist/context.js +0 -273
- package/dist/discord/client.d.ts +0 -35
- package/dist/discord/client.js +0 -66
- package/dist/discord/constants.d.ts +0 -49
- package/dist/discord/constants.js +0 -61
- package/dist/discord/events.d.ts +0 -129
- package/dist/discord/events.js +0 -102
- package/dist/discord/gateway.d.ts +0 -61
- package/dist/discord/gateway.js +0 -320
- package/dist/discord/payloads.d.ts +0 -275
- package/dist/discord/payloads.js +0 -3
- package/dist/discord/presence.d.ts +0 -42
- package/dist/discord/presence.js +0 -51
- package/dist/discord/rest.d.ts +0 -49
- package/dist/discord/rest.js +0 -271
- package/dist/discord/types.d.ts +0 -334
- package/dist/discord/types.js +0 -4
- package/dist/events.d.ts +0 -94
- package/dist/events.js +0 -255
- package/dist/presence.d.ts +0 -92
- package/dist/presence.js +0 -250
- package/dist/structures/Channel.d.ts +0 -110
- package/dist/structures/Channel.js +0 -171
- package/dist/structures/Guild.d.ts +0 -70
- package/dist/structures/Guild.js +0 -105
- package/dist/structures/Interaction.d.ts +0 -113
- package/dist/structures/Interaction.js +0 -274
- package/dist/structures/Member.d.ts +0 -70
- package/dist/structures/Member.js +0 -124
- package/dist/structures/Message.d.ts +0 -87
- package/dist/structures/Message.js +0 -182
- package/dist/structures/Reaction.d.ts +0 -38
- package/dist/structures/Reaction.js +0 -64
- package/dist/structures/Role.d.ts +0 -43
- package/dist/structures/Role.js +0 -77
- package/dist/structures/User.d.ts +0 -41
- package/dist/structures/User.js +0 -79
- package/dist/structures/utils.d.ts +0 -72
- package/dist/structures/utils.js +0 -59
- package/dist/utils/errors.d.ts +0 -5
- package/dist/utils/errors.js +0 -28
- package/dist/utils/modules.d.ts +0 -39
- package/dist/utils/modules.js +0 -156
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// `button()`, as the files of `src/components/buttons/` import it from
|
|
2
|
+
// 'chapterjs': what the button looks like, what it carries, and what to do
|
|
3
|
+
// when it is clicked.
|
|
4
|
+
// https://docs.discord.com/developers/components/reference#button
|
|
5
|
+
import { Limits } from '../discord/api.js';
|
|
6
|
+
import { ButtonStyle, ComponentType, } from '../discord/types/component.js';
|
|
7
|
+
import { isDynamic, resolveText } from './component.js';
|
|
8
|
+
import { checkData, encodeCustomId, } from './custom-id.js';
|
|
9
|
+
import { createFile } from './file.js';
|
|
10
|
+
import { emojiOf } from './instance.js';
|
|
11
|
+
export const BUTTON_STYLES = {
|
|
12
|
+
primary: ButtonStyle.Primary,
|
|
13
|
+
secondary: ButtonStyle.Secondary,
|
|
14
|
+
success: ButtonStyle.Success,
|
|
15
|
+
danger: ButtonStyle.Danger,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Declares a button. Export the result as the default export of a file of
|
|
19
|
+
* `src/components/buttons/`: the path of the file is what tells the
|
|
20
|
+
* button apart, so you never write an id.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { button } from 'chapterjs';
|
|
24
|
+
*
|
|
25
|
+
* export default button({
|
|
26
|
+
* label: 'Confirm',
|
|
27
|
+
* style: 'success',
|
|
28
|
+
* async run({ interaction }) {
|
|
29
|
+
* await interaction.update({ content: 'Confirmed!', components: [] });
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function button(config) {
|
|
35
|
+
return createFile('button', config, {
|
|
36
|
+
asPiece: true,
|
|
37
|
+
pieceKind: 'button',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const fail = (message) => {
|
|
41
|
+
throw new TypeError(message);
|
|
42
|
+
};
|
|
43
|
+
/** Checks what a message gives to change the look of a button. */
|
|
44
|
+
export function readLook(what, look) {
|
|
45
|
+
if (look === undefined)
|
|
46
|
+
return {};
|
|
47
|
+
if (typeof look !== 'object' || look === null || Array.isArray(look)) {
|
|
48
|
+
fail(`The look of ${what} is an object like { disabled: true }.`);
|
|
49
|
+
}
|
|
50
|
+
const given = look;
|
|
51
|
+
for (const key of Object.keys(given)) {
|
|
52
|
+
if (!['label', 'emoji', 'style', 'disabled'].includes(key)) {
|
|
53
|
+
fail(`"${key}" is not something the look of ${what} has. It can have: label, emoji, style, disabled.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (given.label !== undefined && !isDynamic(given.label)) {
|
|
57
|
+
if (typeof given.label !== 'string' ||
|
|
58
|
+
given.label.trim() === '' ||
|
|
59
|
+
given.label.length > Limits.ButtonLabel) {
|
|
60
|
+
fail(`The label of ${what} is a text of 1 to ${Limits.ButtonLabel} characters.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (given.style !== undefined &&
|
|
64
|
+
!Object.hasOwn(BUTTON_STYLES, given.style)) {
|
|
65
|
+
fail(`The style of ${what} is 'primary', 'secondary', 'success' or 'danger', got ${JSON.stringify(given.style)}.`);
|
|
66
|
+
}
|
|
67
|
+
if (given.disabled !== undefined && typeof given.disabled !== 'boolean') {
|
|
68
|
+
fail(`"disabled" of ${what} is true or false.`);
|
|
69
|
+
}
|
|
70
|
+
if (given.emoji !== undefined)
|
|
71
|
+
emojiOf(given.emoji, what);
|
|
72
|
+
return given;
|
|
73
|
+
}
|
|
74
|
+
/** The button as Discord takes it, for the data and the look given. */
|
|
75
|
+
export function renderButton(loaded, args) {
|
|
76
|
+
const hasData = Object.keys(loaded.data).length > 0;
|
|
77
|
+
const name = loaded.path.split('/').pop();
|
|
78
|
+
if (args.length > (hasData ? 2 : 1)) {
|
|
79
|
+
fail(`${name} takes ${hasData ? 'its data, then a look' : 'a look'} at most: ${hasData ? `${name}({ ... }, { disabled: true })` : `${name}({ disabled: true })`}.`);
|
|
80
|
+
}
|
|
81
|
+
const values = checkData(name, loaded.data, hasData ? args[0] : undefined);
|
|
82
|
+
const look = {
|
|
83
|
+
...loaded.look,
|
|
84
|
+
...readLook(name, hasData ? args[1] : args[0]),
|
|
85
|
+
};
|
|
86
|
+
const base = {
|
|
87
|
+
type: ComponentType.Button,
|
|
88
|
+
style: BUTTON_STYLES[look.style],
|
|
89
|
+
custom_id: encodeCustomId(loaded.path, loaded.data, values),
|
|
90
|
+
};
|
|
91
|
+
if (look.emoji !== undefined)
|
|
92
|
+
base.emoji = emojiOf(look.emoji, name);
|
|
93
|
+
if (look.disabled)
|
|
94
|
+
base.disabled = true;
|
|
95
|
+
const { label } = look;
|
|
96
|
+
if (label === undefined)
|
|
97
|
+
return { kind: 'button', raw: base };
|
|
98
|
+
if (!isDynamic(label))
|
|
99
|
+
return { kind: 'button', raw: { ...base, label } };
|
|
100
|
+
// The label is computed when the message is sent, for who will read it.
|
|
101
|
+
return {
|
|
102
|
+
kind: 'button',
|
|
103
|
+
raw: t => ({
|
|
104
|
+
...base,
|
|
105
|
+
label: resolveText(`The label of ${name}`, label, { t, data: values }, { max: Limits.ButtonLabel }),
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { DMChannel, GuildTextBasedChannel } from '../structures/channel.js';
|
|
2
|
+
import type { Guild } from '../structures/guild.js';
|
|
3
|
+
import type { Interaction } from '../structures/interaction.js';
|
|
4
|
+
import type { GuildMember } from '../structures/member.js';
|
|
5
|
+
import type { Message } from '../structures/message.js';
|
|
6
|
+
import type { User } from '../structures/user.js';
|
|
7
|
+
import type { DataShape, DataValuesOf } from './custom-id.js';
|
|
8
|
+
import type { Translator } from '../messages/messages.js';
|
|
9
|
+
/**
|
|
10
|
+
* What a text of a component is computed from when it is a function: `t`
|
|
11
|
+
* in the language of who will read the message, and the data the
|
|
12
|
+
* component carries.
|
|
13
|
+
*/
|
|
14
|
+
export interface TextContext<Data extends DataShape = DataShape> {
|
|
15
|
+
/** The texts of `src/messages/`, in the language of who will read the message. */
|
|
16
|
+
t: Translator;
|
|
17
|
+
/** What the component carries, as declared in `data`. */
|
|
18
|
+
data: [DataShape] extends [Data] ? Record<string, string | number | boolean> : DataValuesOf<Data>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A text shown by a component: written as is, or computed when the
|
|
22
|
+
* message is sent, from `t` and the data of the component.
|
|
23
|
+
*/
|
|
24
|
+
export type DynamicText<Data extends DataShape = DataShape> = string | ((context: TextContext<Data>) => string);
|
|
25
|
+
/** Whether a text is computed when the message is sent. */
|
|
26
|
+
export declare const isDynamic: (value: unknown) => value is (context: TextContext) => string;
|
|
27
|
+
/**
|
|
28
|
+
* Checks a text of a component, now when it is written, or when the
|
|
29
|
+
* message is sent when it is a function: `resolve` is then what to check.
|
|
30
|
+
* Returns the text, or `undefined` when there is none and none is needed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkDynamicText(what: string, value: unknown, { min, max, required, }: {
|
|
33
|
+
min?: number;
|
|
34
|
+
max: number;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
}): void;
|
|
37
|
+
/** A text given by a file or computed, within the limits of Discord. */
|
|
38
|
+
export declare function checkTextValue(what: string, value: unknown, { min, max }: {
|
|
39
|
+
min?: number;
|
|
40
|
+
max: number;
|
|
41
|
+
}): string;
|
|
42
|
+
/** The text of a component when the message is sent. */
|
|
43
|
+
export declare function resolveText(what: string, value: DynamicText | undefined, context: TextContext, limits: {
|
|
44
|
+
min?: number;
|
|
45
|
+
max: number;
|
|
46
|
+
}): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Where a component can be used: in messages of servers (`'guild'`), in
|
|
49
|
+
* private messages with the bot (`'dm'`), or in both. A component is where
|
|
50
|
+
* the message that holds it is: this says where your code expects it.
|
|
51
|
+
*/
|
|
52
|
+
export type ComponentWhere = 'guild' | 'dm' | 'both';
|
|
53
|
+
/**
|
|
54
|
+
* Who may use a component: `'everyone'` who sees the message (the
|
|
55
|
+
* default), or only the `'author'`, the person who used the command the
|
|
56
|
+
* message answers. Anyone else gets a private refusal.
|
|
57
|
+
*/
|
|
58
|
+
export type ComponentWho = 'everyone' | 'author';
|
|
59
|
+
/** What every interactive component declares, besides what it looks like. */
|
|
60
|
+
export interface InteractiveConfig<Data extends DataShape, Where extends ComponentWhere> {
|
|
61
|
+
/**
|
|
62
|
+
* What the component carries from the message that shows it to the code
|
|
63
|
+
* that runs when it is used: a name and a kind for each value
|
|
64
|
+
* (`{ userId: 'string', page: 'number' }`). Given when the component is
|
|
65
|
+
* put in a message, received typed in `run`. Discord keeps it in the
|
|
66
|
+
* component itself, so it survives restarts.
|
|
67
|
+
*/
|
|
68
|
+
data?: Data;
|
|
69
|
+
/**
|
|
70
|
+
* Where the component can be used: `'guild'` (in servers, the default),
|
|
71
|
+
* `'dm'` (in private messages with the bot) or `'both'`. What `run`
|
|
72
|
+
* receives follows it.
|
|
73
|
+
*/
|
|
74
|
+
where?: Where;
|
|
75
|
+
/**
|
|
76
|
+
* Who may use it: `'everyone'` (the default) or only the `'author'` of
|
|
77
|
+
* the command the message answers.
|
|
78
|
+
*/
|
|
79
|
+
who?: ComponentWho;
|
|
80
|
+
/**
|
|
81
|
+
* Make the answers of the component only visible to who used it. On a
|
|
82
|
+
* message only the person sees (an ephemeral answer), they already are.
|
|
83
|
+
*/
|
|
84
|
+
ephemeral?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** A server, as an interaction used in one sees it. */
|
|
87
|
+
export type InGuild<I extends Interaction> = I & {
|
|
88
|
+
readonly guildId: string;
|
|
89
|
+
readonly guild: Guild;
|
|
90
|
+
readonly member: GuildMember;
|
|
91
|
+
readonly channel: GuildTextBasedChannel;
|
|
92
|
+
};
|
|
93
|
+
/** A private message, for something that also works in servers. */
|
|
94
|
+
export type InPrivate<I extends Interaction> = I & {
|
|
95
|
+
readonly guildId: null;
|
|
96
|
+
readonly guild: null;
|
|
97
|
+
readonly member: null;
|
|
98
|
+
readonly channel: DMChannel;
|
|
99
|
+
};
|
|
100
|
+
/** A private message, for something that only works there: no server at all. */
|
|
101
|
+
export type InDm<I extends Interaction> = Omit<I, 'guildId' | 'guild' | 'member' | 'channel'> & {
|
|
102
|
+
readonly channel: DMChannel;
|
|
103
|
+
};
|
|
104
|
+
/** What `run` receives about the place, in a server. */
|
|
105
|
+
export interface PlaceInGuild<I extends Interaction> {
|
|
106
|
+
interaction: InGuild<I>;
|
|
107
|
+
/** The server it was used in. */
|
|
108
|
+
guild: Guild;
|
|
109
|
+
/** Who used it, as a member of the server. */
|
|
110
|
+
member: GuildMember;
|
|
111
|
+
/** The channel it was used in. */
|
|
112
|
+
channel: GuildTextBasedChannel;
|
|
113
|
+
}
|
|
114
|
+
/** What `run` receives about the place, in a private message, when it also works in servers. */
|
|
115
|
+
export interface PlaceInPrivate<I extends Interaction> {
|
|
116
|
+
interaction: InPrivate<I>;
|
|
117
|
+
/** No server: it was used in a private message. */
|
|
118
|
+
guild: null;
|
|
119
|
+
/** No member: the person is `user`. */
|
|
120
|
+
member: null;
|
|
121
|
+
/** The private conversation it was used in. */
|
|
122
|
+
channel: DMChannel;
|
|
123
|
+
}
|
|
124
|
+
/** What `run` receives about the place, when it only works in private messages. */
|
|
125
|
+
export interface PlaceInDm<I extends Interaction> {
|
|
126
|
+
interaction: InDm<I>;
|
|
127
|
+
/** The private conversation it was used in. */
|
|
128
|
+
channel: DMChannel;
|
|
129
|
+
}
|
|
130
|
+
/** What `run` receives about where it was used, following `where`. */
|
|
131
|
+
export type PlaceOf<Where extends ComponentWhere, I extends Interaction> = Where extends 'guild' ? PlaceInGuild<I> : Where extends 'dm' ? PlaceInDm<I> : PlaceInGuild<I> | PlaceInPrivate<I>;
|
|
132
|
+
/** What every interactive component gives to its `run`, besides the place. */
|
|
133
|
+
export interface InteractiveContext<Data extends DataShape> {
|
|
134
|
+
/** Who used it. */
|
|
135
|
+
user: User;
|
|
136
|
+
/** The message the component is on. */
|
|
137
|
+
message: Message;
|
|
138
|
+
/** What the component carries, as declared in `data`. */
|
|
139
|
+
data: DataValuesOf<Data>;
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// What every interactive component of `src/components/` shares: where it
|
|
2
|
+
// works, who may use it, what it carries, and what its `run` receives
|
|
3
|
+
// about the place it was used in.
|
|
4
|
+
/** Whether a text is computed when the message is sent. */
|
|
5
|
+
export const isDynamic = (value) => typeof value === 'function';
|
|
6
|
+
/**
|
|
7
|
+
* Checks a text of a component, now when it is written, or when the
|
|
8
|
+
* message is sent when it is a function: `resolve` is then what to check.
|
|
9
|
+
* Returns the text, or `undefined` when there is none and none is needed.
|
|
10
|
+
*/
|
|
11
|
+
export function checkDynamicText(what, value, { min = 1, max, required = false, }) {
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
if (required)
|
|
14
|
+
throw new TypeError(`${what} is missing.`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (isDynamic(value))
|
|
18
|
+
return;
|
|
19
|
+
checkTextValue(what, value, { min, max });
|
|
20
|
+
}
|
|
21
|
+
/** A text given by a file or computed, within the limits of Discord. */
|
|
22
|
+
export function checkTextValue(what, value, { min = 1, max }) {
|
|
23
|
+
if (typeof value !== 'string') {
|
|
24
|
+
throw new TypeError(`${what} is a text${min === 1 ? ` of 1 to ${max} characters` : ''}, got ${typeof value}.`);
|
|
25
|
+
}
|
|
26
|
+
if (value.length < min ||
|
|
27
|
+
value.length > max ||
|
|
28
|
+
(min > 0 && value.trim() === '')) {
|
|
29
|
+
throw new RangeError(`${what} is ${value.length} characters long: Discord accepts between ${min} and ${max}.`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/** The text of a component when the message is sent. */
|
|
34
|
+
export function resolveText(what, value, context, limits) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
const text = isDynamic(value) ? value(context) : value;
|
|
38
|
+
return checkTextValue(isDynamic(value) ? `${what}, as its function returned it,` : what, text, limits);
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Convention } from '../loader/loader.js';
|
|
2
|
+
import { type LoadedButton } from './button.js';
|
|
3
|
+
import { isEmbedFile } from './embed.js';
|
|
4
|
+
import { type LoadedModal } from './modal.js';
|
|
5
|
+
import { type LoadedSelect } from './select.js';
|
|
6
|
+
/** An embed file, checked (when it is static). */
|
|
7
|
+
export interface LoadedEmbed {
|
|
8
|
+
readonly kind: 'embed';
|
|
9
|
+
readonly path: string;
|
|
10
|
+
}
|
|
11
|
+
/** A component file, checked: what the router works with. */
|
|
12
|
+
export type LoadedComponent = LoadedButton | LoadedSelect | LoadedModal | LoadedEmbed;
|
|
13
|
+
/**
|
|
14
|
+
* `src/components/`: one folder per kind of component (`buttons/`,
|
|
15
|
+
* `selects/`, `modals/`, `embeds/`), and in it one file per component,
|
|
16
|
+
* whose path is its id. The file exports by default what `button()`,
|
|
17
|
+
* `select()`, `modal()` or `embed()` returns.
|
|
18
|
+
*/
|
|
19
|
+
export declare const componentsConvention: Convention<LoadedComponent>;
|
|
20
|
+
/** The files that are the same component (through `(group)` folders): the later one is left out. */
|
|
21
|
+
export declare function findDuplicates<T extends {
|
|
22
|
+
file: string;
|
|
23
|
+
component: {
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
}>(entries: readonly T[]): {
|
|
27
|
+
valid: T[];
|
|
28
|
+
conflicts: {
|
|
29
|
+
file: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}[];
|
|
32
|
+
};
|
|
33
|
+
/** Whether a file holds an embed: nothing to route. */
|
|
34
|
+
export declare const isEmbedLoaded: (component: LoadedComponent) => component is LoadedEmbed;
|
|
35
|
+
export { isEmbedFile };
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// `src/components/`: the folder is the kind of component, the path of the
|
|
2
|
+
// file is its id. Everything a file declares is checked when it loads,
|
|
3
|
+
// against the limits of Discord, so a mistake is explained before Discord
|
|
4
|
+
// refuses it; the file is then bound to its path, which is what makes its
|
|
5
|
+
// instances.
|
|
6
|
+
import { Limits } from '../discord/api.js';
|
|
7
|
+
import { checkEmbed } from '../structures/payload.js';
|
|
8
|
+
import { isDynamic } from './component.js';
|
|
9
|
+
import { readLook, renderButton, } from './button.js';
|
|
10
|
+
import { DATA_KINDS } from './custom-id.js';
|
|
11
|
+
import { isEmbedFile } from './embed.js';
|
|
12
|
+
import { bindFile, COMPONENT_FOLDERS, fileStateOf, } from './file.js';
|
|
13
|
+
import { readField, renderModal } from './modal.js';
|
|
14
|
+
import { readOptions } from './options.js';
|
|
15
|
+
import { readChannelTypes, readPlaceholder, readRange, renderSelect, SELECT_TYPES, } from './select.js';
|
|
16
|
+
const fail = (message) => {
|
|
17
|
+
throw new TypeError(message);
|
|
18
|
+
};
|
|
19
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
/** The names of the folders and files: what a path and an id can hold. */
|
|
21
|
+
const NAME = /^[A-Za-z0-9_-]+$/;
|
|
22
|
+
const FOLDERS = Object.keys(COMPONENT_FOLDERS);
|
|
23
|
+
function checkKeys(what, value, allowed) {
|
|
24
|
+
for (const key of Object.keys(value)) {
|
|
25
|
+
if (!allowed.includes(key)) {
|
|
26
|
+
fail(`"${key}" is not something ${what} has. It can have: ${allowed.join(', ')}.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function checkBoolean(what, value) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return false;
|
|
33
|
+
if (typeof value !== 'boolean')
|
|
34
|
+
return fail(`${what} is true or false, got ${JSON.stringify(value) ?? typeof value}.`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
const PLACES = ['guild', 'dm', 'both'];
|
|
38
|
+
function checkWhere(what, value) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return 'guild';
|
|
41
|
+
if (!PLACES.includes(value)) {
|
|
42
|
+
return fail(`"where" says where ${what} can be used: 'guild' (in servers, which is the default), 'dm' (in private messages with the bot) or 'both'. Got ${JSON.stringify(value) ?? typeof value}.`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function checkWho(what, value) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return 'everyone';
|
|
49
|
+
if (value !== 'everyone' && value !== 'author') {
|
|
50
|
+
return fail(`"who" says who may use ${what}: 'everyone' (the default) or 'author' (only who used the command the message answers). Got ${JSON.stringify(value) ?? typeof value}.`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function checkDataShape(what, value) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return Object.freeze({});
|
|
57
|
+
if (!isRecord(value)) {
|
|
58
|
+
return fail(`"data" of ${what} is an object from a name to a kind: data: { userId: 'string', page: 'number' }.`);
|
|
59
|
+
}
|
|
60
|
+
const kinds = DATA_KINDS.map(one => `'${one}'`).join(', ');
|
|
61
|
+
for (const [name, field] of Object.entries(value)) {
|
|
62
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
63
|
+
fail(`"${name}" can't be the name of a data of ${what}: it is how your code reads it (data.${name}), so use letters, digits and _ only.`);
|
|
64
|
+
}
|
|
65
|
+
// A kind, or a kind with a default value.
|
|
66
|
+
if (isRecord(field)) {
|
|
67
|
+
for (const key of Object.keys(field)) {
|
|
68
|
+
if (key !== 'type' && key !== 'default') {
|
|
69
|
+
fail(`"${key}" is not something the data "${name}" of ${what} has. It can have: type, default.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!DATA_KINDS.includes(field.type)) {
|
|
73
|
+
fail(`The data "${name}" of ${what} has the type ${JSON.stringify(field.type) ?? typeof field.type}, which does not exist. Kinds are: ${kinds}.`);
|
|
74
|
+
}
|
|
75
|
+
if (field.default !== undefined &&
|
|
76
|
+
(typeof field.default !== field.type ||
|
|
77
|
+
(field.type === 'number' && !Number.isFinite(field.default)))) {
|
|
78
|
+
fail(`The default value of the data "${name}" of ${what} must be a ${field.type}, got ${JSON.stringify(field.default) ?? typeof field.default}.`);
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!DATA_KINDS.includes(field)) {
|
|
83
|
+
fail(`The data "${name}" of ${what} has the kind ${JSON.stringify(field) ?? typeof field}, which does not exist. Kinds are: ${kinds}, or { type: 'number', default: 1 } for a value with a default.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze({ ...value });
|
|
87
|
+
}
|
|
88
|
+
function checkRun(what, value, example) {
|
|
89
|
+
if (typeof value !== 'function') {
|
|
90
|
+
fail(`${what} has no "run": the function to run when someone uses it, like ${example}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function readButton(path, config) {
|
|
94
|
+
if (!isRecord(config)) {
|
|
95
|
+
return fail(`button() needs an object: button({ label: '...', run({ interaction }) { ... } })`);
|
|
96
|
+
}
|
|
97
|
+
const what = 'this button';
|
|
98
|
+
checkKeys('a button', config, [
|
|
99
|
+
'label',
|
|
100
|
+
'emoji',
|
|
101
|
+
'style',
|
|
102
|
+
'disabled',
|
|
103
|
+
'data',
|
|
104
|
+
'where',
|
|
105
|
+
'who',
|
|
106
|
+
'ephemeral',
|
|
107
|
+
'run',
|
|
108
|
+
]);
|
|
109
|
+
const look = readLook(what, {
|
|
110
|
+
...(config.label !== undefined ? { label: config.label } : {}),
|
|
111
|
+
...(config.emoji !== undefined ? { emoji: config.emoji } : {}),
|
|
112
|
+
...(config.style !== undefined ? { style: config.style } : {}),
|
|
113
|
+
...(config.disabled !== undefined ? { disabled: config.disabled } : {}),
|
|
114
|
+
});
|
|
115
|
+
if (look.label === undefined && look.emoji === undefined) {
|
|
116
|
+
fail('A button needs a label or an emoji.');
|
|
117
|
+
}
|
|
118
|
+
checkRun(what, config.run, `async run({ interaction }) { await interaction.update('Done!'); }`);
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
kind: 'button',
|
|
121
|
+
path,
|
|
122
|
+
look: {
|
|
123
|
+
style: look.style ?? 'primary',
|
|
124
|
+
disabled: look.disabled ?? false,
|
|
125
|
+
...(look.label !== undefined ? { label: look.label } : {}),
|
|
126
|
+
...(look.emoji !== undefined ? { emoji: look.emoji } : {}),
|
|
127
|
+
},
|
|
128
|
+
data: checkDataShape(what, config.data),
|
|
129
|
+
where: checkWhere(what, config.where),
|
|
130
|
+
who: checkWho(what, config.who),
|
|
131
|
+
ephemeral: checkBoolean('"ephemeral"', config.ephemeral),
|
|
132
|
+
run: config.run,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function readSelect(path, config) {
|
|
136
|
+
if (!isRecord(config)) {
|
|
137
|
+
return fail(`select() needs an object: select({ options: ['a', 'b'], run({ interaction, value }) { ... } })`);
|
|
138
|
+
}
|
|
139
|
+
const what = 'this select menu';
|
|
140
|
+
const type = (config.type ?? 'string');
|
|
141
|
+
if (!Object.hasOwn(SELECT_TYPES, type)) {
|
|
142
|
+
fail(`The type of ${what} is ${JSON.stringify(config.type)}, which does not exist. Types are: ${Object.keys(SELECT_TYPES)
|
|
143
|
+
.map(one => `'${one}'`)
|
|
144
|
+
.join(', ')}.`);
|
|
145
|
+
}
|
|
146
|
+
checkKeys('a select menu', config, [
|
|
147
|
+
'type',
|
|
148
|
+
'options',
|
|
149
|
+
'placeholder',
|
|
150
|
+
'min',
|
|
151
|
+
'max',
|
|
152
|
+
'disabled',
|
|
153
|
+
'channelTypes',
|
|
154
|
+
'data',
|
|
155
|
+
'where',
|
|
156
|
+
'who',
|
|
157
|
+
'ephemeral',
|
|
158
|
+
'run',
|
|
159
|
+
]);
|
|
160
|
+
if (type !== 'string' && config.options !== undefined) {
|
|
161
|
+
fail(`${what} lists ${type}s: Discord fills it, so it takes no "options".`);
|
|
162
|
+
}
|
|
163
|
+
if (type !== 'channel' && config.channelTypes !== undefined) {
|
|
164
|
+
fail(`"channelTypes" is only for a menu of channels (type: 'channel').`);
|
|
165
|
+
}
|
|
166
|
+
const options = type === 'string' ? readOptions(what, config.options) : [];
|
|
167
|
+
const placeholder = readPlaceholder(what, config.placeholder);
|
|
168
|
+
checkRun(what, config.run, `async run({ interaction, values }) { ... }`);
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
kind: 'select',
|
|
171
|
+
path,
|
|
172
|
+
type: type,
|
|
173
|
+
options: Object.freeze(options),
|
|
174
|
+
channelTypes: readChannelTypes(what, config.channelTypes),
|
|
175
|
+
look: {
|
|
176
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
177
|
+
...(checkBoolean('"disabled"', config.disabled)
|
|
178
|
+
? { disabled: true }
|
|
179
|
+
: {}),
|
|
180
|
+
},
|
|
181
|
+
...readRange(what, config, Limits.SelectValues, { min: 1, max: 1 }),
|
|
182
|
+
data: checkDataShape(what, config.data),
|
|
183
|
+
where: checkWhere(what, config.where),
|
|
184
|
+
who: checkWho(what, config.who),
|
|
185
|
+
ephemeral: checkBoolean('"ephemeral"', config.ephemeral),
|
|
186
|
+
run: config.run,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function readModal(path, config) {
|
|
190
|
+
if (!isRecord(config)) {
|
|
191
|
+
return fail(`modal() needs an object: modal({ title: '...', fields: { ... }, run({ interaction, fields }) { ... } })`);
|
|
192
|
+
}
|
|
193
|
+
const what = 'this form';
|
|
194
|
+
checkKeys('a form', config, [
|
|
195
|
+
'title',
|
|
196
|
+
'fields',
|
|
197
|
+
'data',
|
|
198
|
+
'where',
|
|
199
|
+
'ephemeral',
|
|
200
|
+
'run',
|
|
201
|
+
]);
|
|
202
|
+
if (!isDynamic(config.title) &&
|
|
203
|
+
(typeof config.title !== 'string' ||
|
|
204
|
+
config.title.trim() === '' ||
|
|
205
|
+
config.title.length > Limits.ModalTitle)) {
|
|
206
|
+
fail(`The title of ${what} is a text of 1 to ${Limits.ModalTitle} characters.`);
|
|
207
|
+
}
|
|
208
|
+
if (!isRecord(config.fields)) {
|
|
209
|
+
fail(`"fields" of ${what} is an object whose keys are the names of the fields: fields: { reason: { type: 'text', label: 'Reason' } }`);
|
|
210
|
+
}
|
|
211
|
+
const fields = Object.entries(config.fields).map(([name, field]) => readField(name, field));
|
|
212
|
+
const inputs = fields.filter(field => field.field.type !== 'note');
|
|
213
|
+
if (fields.length === 0 || fields.length > Limits.ModalFields) {
|
|
214
|
+
fail(`${what} has ${fields.length} fields: Discord accepts between 1 and ${Limits.ModalFields}.`);
|
|
215
|
+
}
|
|
216
|
+
if (inputs.length === 0) {
|
|
217
|
+
fail(`${what} has nothing to fill in: add a field like { type: 'text', label: '...' }.`);
|
|
218
|
+
}
|
|
219
|
+
checkRun(what, config.run, `async run({ interaction, fields }) { ... }`);
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
kind: 'modal',
|
|
222
|
+
path,
|
|
223
|
+
title: config.title,
|
|
224
|
+
fields: Object.freeze(fields),
|
|
225
|
+
data: checkDataShape(what, config.data),
|
|
226
|
+
where: checkWhere(what, config.where),
|
|
227
|
+
ephemeral: checkBoolean('"ephemeral"', config.ephemeral),
|
|
228
|
+
run: config.run,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** A file of `src/components/`, by the kind its folder says. */
|
|
232
|
+
function readOne(kind, path, exports) {
|
|
233
|
+
const file = exports.default;
|
|
234
|
+
const state = fileStateOf(file);
|
|
235
|
+
const example = `import { ${kind} } from 'chapterjs'; export default ${kind}({ ... })`;
|
|
236
|
+
if (!state) {
|
|
237
|
+
return fail('default' in exports
|
|
238
|
+
? `The default export of this file must be what ${kind}() returns: ${example}`
|
|
239
|
+
: `This file has no default export. It should look like: ${example}`);
|
|
240
|
+
}
|
|
241
|
+
if (state.kind !== kind) {
|
|
242
|
+
const folder = FOLDERS.find(one => COMPONENT_FOLDERS[one] === state.kind);
|
|
243
|
+
return fail(`This file exports a ${state.kind}, but it is in src/components/${FOLDERS.find(one => COMPONENT_FOLDERS[one] === kind)}/. Move it to src/components/${folder}/.`);
|
|
244
|
+
}
|
|
245
|
+
switch (kind) {
|
|
246
|
+
case 'button': {
|
|
247
|
+
const loaded = readButton(path, state.config);
|
|
248
|
+
bindFile(file, { path, render: args => renderButton(loaded, args) });
|
|
249
|
+
return loaded;
|
|
250
|
+
}
|
|
251
|
+
case 'select': {
|
|
252
|
+
const loaded = readSelect(path, state.config);
|
|
253
|
+
bindFile(file, { path, render: args => renderSelect(loaded, args) });
|
|
254
|
+
return loaded;
|
|
255
|
+
}
|
|
256
|
+
case 'modal': {
|
|
257
|
+
const loaded = readModal(path, state.config);
|
|
258
|
+
bindFile(file, { path, render: args => renderModal(loaded, args) });
|
|
259
|
+
return loaded;
|
|
260
|
+
}
|
|
261
|
+
default: {
|
|
262
|
+
const { config } = state;
|
|
263
|
+
if (typeof config !== 'function') {
|
|
264
|
+
if (!isRecord(config)) {
|
|
265
|
+
fail(`embed() takes the embed itself ({ title: '...' }) or a function that returns one.`);
|
|
266
|
+
}
|
|
267
|
+
checkEmbed(config, 'this embed');
|
|
268
|
+
}
|
|
269
|
+
bindFile(file, {
|
|
270
|
+
path,
|
|
271
|
+
render: args => {
|
|
272
|
+
if (typeof config === 'function')
|
|
273
|
+
return config(...args);
|
|
274
|
+
if (args.length > 0)
|
|
275
|
+
fail(`${path.split('/').pop()} always looks the same: it takes no arguments.`);
|
|
276
|
+
return config;
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
return Object.freeze({ kind: 'embed', path });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* `src/components/`: one folder per kind of component (`buttons/`,
|
|
285
|
+
* `selects/`, `modals/`, `embeds/`), and in it one file per component,
|
|
286
|
+
* whose path is its id. The file exports by default what `button()`,
|
|
287
|
+
* `select()`, `modal()` or `embed()` returns.
|
|
288
|
+
*/
|
|
289
|
+
export const componentsConvention = {
|
|
290
|
+
folder: 'components',
|
|
291
|
+
one: 'component',
|
|
292
|
+
many: 'components',
|
|
293
|
+
check(path) {
|
|
294
|
+
const parts = path.replace(/\.[^./]+$/, '').split('/');
|
|
295
|
+
if (parts.length < 2) {
|
|
296
|
+
fail(`This file is directly in src/components/. Put it in the folder of its kind: ${FOLDERS.map(one => `src/components/${one}/`).join(', ')}.`);
|
|
297
|
+
}
|
|
298
|
+
const folder = parts[0];
|
|
299
|
+
if (!Object.hasOwn(COMPONENT_FOLDERS, folder)) {
|
|
300
|
+
fail(`The folder src/components/${folder} is not a kind of component. Kinds are: ${FOLDERS.join(', ')}.`);
|
|
301
|
+
}
|
|
302
|
+
for (const part of parts) {
|
|
303
|
+
if (!NAME.test(part)) {
|
|
304
|
+
fail(`"${part}" can't be in the path of a component: use letters, digits, - and _ only. The path of the file is the id of the component.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (path.replace(/\.[^./]+$/, '').length > Limits.CustomId - 20) {
|
|
308
|
+
fail(`The path of this file is ${path.length} characters long: Discord gives ${Limits.CustomId} characters to the id of a component and its data together. Shorten the names.`);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
read(exports, path) {
|
|
312
|
+
const id = path.replace(/\.[^./]+$/, '');
|
|
313
|
+
const kind = COMPONENT_FOLDERS[id.split('/')[0]];
|
|
314
|
+
return readOne(kind, id, exports);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
/** The files that are the same component (through `(group)` folders): the later one is left out. */
|
|
318
|
+
export function findDuplicates(entries) {
|
|
319
|
+
const seen = new Map();
|
|
320
|
+
const conflicts = [];
|
|
321
|
+
const valid = [...entries]
|
|
322
|
+
.sort((a, b) => (a.file < b.file ? -1 : 1))
|
|
323
|
+
.filter(entry => {
|
|
324
|
+
const first = seen.get(entry.component.path);
|
|
325
|
+
if (!first) {
|
|
326
|
+
seen.set(entry.component.path, entry);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
conflicts.push({
|
|
330
|
+
file: entry.file,
|
|
331
|
+
message: `${entry.component.path} is already ${first.file}: two files can't be the same component. A folder in parentheses only groups files, it is not part of the id.`,
|
|
332
|
+
});
|
|
333
|
+
return false;
|
|
334
|
+
});
|
|
335
|
+
return { valid, conflicts };
|
|
336
|
+
}
|
|
337
|
+
/** Whether a file holds an embed: nothing to route. */
|
|
338
|
+
export const isEmbedLoaded = (component) => component.kind === 'embed';
|
|
339
|
+
export { isEmbedFile };
|