chapterjs 0.1.2 → 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.
Files changed (320) hide show
  1. package/dist/assets/asset.d.ts +46 -0
  2. package/dist/assets/asset.js +86 -0
  3. package/dist/assets/public.d.ts +19 -0
  4. package/dist/assets/public.js +120 -0
  5. package/dist/cache/cache.d.ts +62 -0
  6. package/dist/cache/cache.js +80 -0
  7. package/dist/cache/store.d.ts +45 -0
  8. package/dist/cache/store.js +53 -0
  9. package/dist/cli/build.d.ts +41 -0
  10. package/dist/cli/build.js +206 -0
  11. package/dist/cli/cli.js +108 -25
  12. package/dist/cli/cluster.d.ts +70 -0
  13. package/dist/cli/cluster.js +213 -0
  14. package/dist/cli/dev.d.ts +18 -1
  15. package/dist/cli/dev.js +252 -198
  16. package/dist/cli/env.d.ts +31 -0
  17. package/dist/cli/env.js +93 -0
  18. package/dist/cli/log.d.ts +13 -0
  19. package/dist/cli/log.js +17 -0
  20. package/dist/cli/node-version.d.ts +8 -0
  21. package/dist/cli/node-version.js +13 -0
  22. package/dist/cli/preflight.d.ts +36 -0
  23. package/dist/cli/preflight.js +122 -0
  24. package/dist/cli/project.d.ts +122 -0
  25. package/dist/cli/project.js +452 -0
  26. package/dist/cli/start.d.ts +25 -0
  27. package/dist/cli/start.js +225 -0
  28. package/dist/commands/autocomplete.d.ts +13 -0
  29. package/dist/commands/autocomplete.js +159 -0
  30. package/dist/commands/command.d.ts +346 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/convention.d.ts +64 -0
  33. package/dist/commands/convention.js +464 -0
  34. package/dist/commands/register.d.ts +18 -0
  35. package/dist/commands/register.js +42 -0
  36. package/dist/commands/router.d.ts +20 -0
  37. package/dist/commands/router.js +228 -0
  38. package/dist/commands/tree.d.ts +43 -0
  39. package/dist/commands/tree.js +206 -0
  40. package/dist/commands/twins.d.ts +15 -0
  41. package/dist/commands/twins.js +78 -0
  42. package/dist/components/button.d.ts +79 -0
  43. package/dist/components/button.js +108 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/convention.d.ts +35 -0
  47. package/dist/components/convention.js +339 -0
  48. package/dist/components/custom-id.d.ts +72 -0
  49. package/dist/components/custom-id.js +128 -0
  50. package/dist/components/embed.d.ts +28 -0
  51. package/dist/components/embed.js +11 -0
  52. package/dist/components/file.d.ts +30 -0
  53. package/dist/components/file.js +52 -0
  54. package/dist/components/instance.d.ts +100 -0
  55. package/dist/components/instance.js +86 -0
  56. package/dist/components/layout.d.ts +103 -0
  57. package/dist/components/layout.js +279 -0
  58. package/dist/components/modal.d.ts +244 -0
  59. package/dist/components/modal.js +399 -0
  60. package/dist/components/options.d.ts +41 -0
  61. package/dist/components/options.js +101 -0
  62. package/dist/components/render.d.ts +12 -0
  63. package/dist/components/render.js +58 -0
  64. package/dist/components/router.d.ts +18 -0
  65. package/dist/components/router.js +256 -0
  66. package/dist/components/select.d.ts +144 -0
  67. package/dist/components/select.js +179 -0
  68. package/dist/core/bot.d.ts +99 -0
  69. package/dist/core/bot.js +174 -0
  70. package/dist/core/memory.d.ts +66 -0
  71. package/dist/core/memory.js +115 -0
  72. package/dist/discord/api.d.ts +112 -0
  73. package/dist/discord/api.js +115 -0
  74. package/dist/discord/cdn.d.ts +67 -0
  75. package/dist/discord/cdn.js +100 -0
  76. package/dist/discord/codes.d.ts +171 -0
  77. package/dist/discord/codes.js +421 -0
  78. package/dist/discord/endpoint.d.ts +56 -0
  79. package/dist/discord/endpoint.js +53 -0
  80. package/dist/discord/endpoints.d.ts +1529 -0
  81. package/dist/discord/endpoints.js +1139 -0
  82. package/dist/discord/formatting.d.ts +67 -0
  83. package/dist/discord/formatting.js +87 -0
  84. package/dist/discord/index.d.ts +10 -0
  85. package/dist/discord/index.js +13 -0
  86. package/dist/discord/intents.d.ts +48 -0
  87. package/dist/discord/intents.js +128 -0
  88. package/dist/discord/permissions.d.ts +94 -0
  89. package/dist/discord/permissions.js +185 -0
  90. package/dist/discord/snowflake.d.ts +21 -0
  91. package/dist/discord/snowflake.js +36 -0
  92. package/dist/discord/types/application-command.d.ts +332 -0
  93. package/dist/discord/types/application-command.js +48 -0
  94. package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
  95. package/dist/discord/types/application-role-connection-metadata.js +22 -0
  96. package/dist/discord/types/application.d.ts +208 -0
  97. package/dist/discord/types/application.js +48 -0
  98. package/dist/discord/types/audit-log.d.ts +255 -0
  99. package/dist/discord/types/audit-log.js +144 -0
  100. package/dist/discord/types/auto-moderation.d.ts +167 -0
  101. package/dist/discord/types/auto-moderation.js +52 -0
  102. package/dist/discord/types/channel.d.ts +557 -0
  103. package/dist/discord/types/channel.js +88 -0
  104. package/dist/discord/types/common.d.ts +54 -0
  105. package/dist/discord/types/common.js +38 -0
  106. package/dist/discord/types/component.d.ts +775 -0
  107. package/dist/discord/types/component.js +76 -0
  108. package/dist/discord/types/emoji.d.ts +74 -0
  109. package/dist/discord/types/emoji.js +1 -0
  110. package/dist/discord/types/entitlement.d.ts +84 -0
  111. package/dist/discord/types/entitlement.js +22 -0
  112. package/dist/discord/types/gateway-events.d.ts +1148 -0
  113. package/dist/discord/types/gateway-events.js +54 -0
  114. package/dist/discord/types/gateway.d.ts +38 -0
  115. package/dist/discord/types/gateway.js +1 -0
  116. package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
  117. package/dist/discord/types/guild-scheduled-event.js +68 -0
  118. package/dist/discord/types/guild-template.d.ts +51 -0
  119. package/dist/discord/types/guild-template.js +1 -0
  120. package/dist/discord/types/guild.d.ts +910 -0
  121. package/dist/discord/types/guild.js +153 -0
  122. package/dist/discord/types/index.d.ts +31 -0
  123. package/dist/discord/types/index.js +31 -0
  124. package/dist/discord/types/interaction.d.ts +321 -0
  125. package/dist/discord/types/interaction.js +47 -0
  126. package/dist/discord/types/invite.d.ts +158 -0
  127. package/dist/discord/types/invite.js +39 -0
  128. package/dist/discord/types/message.d.ts +834 -0
  129. package/dist/discord/types/message.js +159 -0
  130. package/dist/discord/types/oauth2.d.ts +22 -0
  131. package/dist/discord/types/oauth2.js +1 -0
  132. package/dist/discord/types/permissions.d.ts +183 -0
  133. package/dist/discord/types/permissions.js +118 -0
  134. package/dist/discord/types/poll.d.ts +106 -0
  135. package/dist/discord/types/poll.js +8 -0
  136. package/dist/discord/types/sku.d.ts +47 -0
  137. package/dist/discord/types/sku.js +26 -0
  138. package/dist/discord/types/soundboard.d.ts +71 -0
  139. package/dist/discord/types/soundboard.js +1 -0
  140. package/dist/discord/types/stage-instance.d.ts +58 -0
  141. package/dist/discord/types/stage-instance.js +10 -0
  142. package/dist/discord/types/sticker.d.ts +115 -0
  143. package/dist/discord/types/sticker.js +20 -0
  144. package/dist/discord/types/subscription.d.ts +54 -0
  145. package/dist/discord/types/subscription.js +12 -0
  146. package/dist/discord/types/team.d.ts +55 -0
  147. package/dist/discord/types/team.js +21 -0
  148. package/dist/discord/types/user.d.ts +273 -0
  149. package/dist/discord/types/user.js +83 -0
  150. package/dist/discord/types/voice.d.ts +72 -0
  151. package/dist/discord/types/voice.js +1 -0
  152. package/dist/discord/types/webhook.d.ts +184 -0
  153. package/dist/discord/types/webhook.js +12 -0
  154. package/dist/events/convention.d.ts +25 -0
  155. package/dist/events/convention.js +97 -0
  156. package/dist/events/event.d.ts +8 -0
  157. package/dist/events/event.js +20 -0
  158. package/dist/events/registry.d.ts +706 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +45 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/events/types.d.ts +7 -0
  163. package/dist/events/types.js +48 -0
  164. package/dist/gateway/errors.d.ts +18 -0
  165. package/dist/gateway/errors.js +47 -0
  166. package/dist/gateway/gateway.d.ts +64 -0
  167. package/dist/gateway/gateway.js +117 -0
  168. package/dist/gateway/identify-queue.d.ts +25 -0
  169. package/dist/gateway/identify-queue.js +29 -0
  170. package/dist/gateway/shard.d.ts +94 -0
  171. package/dist/gateway/shard.js +414 -0
  172. package/dist/gateway/state.d.ts +11 -0
  173. package/dist/gateway/state.js +184 -0
  174. package/dist/index.d.ts +55 -18
  175. package/dist/index.js +38 -12
  176. package/dist/interactions/cooldown.d.ts +33 -0
  177. package/dist/interactions/cooldown.js +74 -0
  178. package/dist/interactions/dispatch.d.ts +91 -0
  179. package/dist/interactions/dispatch.js +110 -0
  180. package/dist/loader/generated.d.ts +37 -0
  181. package/dist/loader/generated.js +114 -0
  182. package/dist/loader/hot.d.ts +13 -0
  183. package/dist/loader/hot.js +85 -0
  184. package/dist/loader/loader.d.ts +72 -0
  185. package/dist/loader/loader.js +135 -0
  186. package/dist/loader/locate.d.ts +10 -0
  187. package/dist/loader/locate.js +36 -0
  188. package/dist/loader/watch.d.ts +17 -0
  189. package/dist/loader/watch.js +66 -0
  190. package/dist/main.d.ts +3 -0
  191. package/dist/main.js +6 -0
  192. package/dist/messages/commands.d.ts +23 -0
  193. package/dist/messages/commands.js +123 -0
  194. package/dist/messages/convention.d.ts +89 -0
  195. package/dist/messages/convention.js +347 -0
  196. package/dist/messages/messages.d.ts +219 -0
  197. package/dist/messages/messages.js +30 -0
  198. package/dist/messages/phrases.d.ts +23 -0
  199. package/dist/messages/phrases.js +44 -0
  200. package/dist/messages/translate.d.ts +100 -0
  201. package/dist/messages/translate.js +249 -0
  202. package/dist/presence/convention.d.ts +26 -0
  203. package/dist/presence/convention.js +133 -0
  204. package/dist/presence/presence.d.ts +57 -0
  205. package/dist/presence/presence.js +27 -0
  206. package/dist/rest/errors.d.ts +55 -0
  207. package/dist/rest/errors.js +92 -0
  208. package/dist/rest/rate-limiter.d.ts +46 -0
  209. package/dist/rest/rate-limiter.js +153 -0
  210. package/dist/rest/rest.d.ts +63 -0
  211. package/dist/rest/rest.js +194 -0
  212. package/dist/structures/base.d.ts +48 -0
  213. package/dist/structures/base.js +84 -0
  214. package/dist/structures/channel.d.ts +380 -0
  215. package/dist/structures/channel.js +627 -0
  216. package/dist/structures/context.d.ts +30 -0
  217. package/dist/structures/context.js +7 -0
  218. package/dist/structures/emoji.d.ts +46 -0
  219. package/dist/structures/emoji.js +65 -0
  220. package/dist/structures/entities.d.ts +62 -0
  221. package/dist/structures/entities.js +226 -0
  222. package/dist/structures/guild.d.ts +348 -0
  223. package/dist/structures/guild.js +466 -0
  224. package/dist/structures/interaction.d.ts +197 -0
  225. package/dist/structures/interaction.js +396 -0
  226. package/dist/structures/invite.d.ts +38 -0
  227. package/dist/structures/invite.js +57 -0
  228. package/dist/structures/known.d.ts +28 -0
  229. package/dist/structures/known.js +58 -0
  230. package/dist/structures/member.d.ts +140 -0
  231. package/dist/structures/member.js +260 -0
  232. package/dist/structures/message.d.ts +197 -0
  233. package/dist/structures/message.js +268 -0
  234. package/dist/structures/payload.d.ts +91 -0
  235. package/dist/structures/payload.js +184 -0
  236. package/dist/structures/role.d.ts +69 -0
  237. package/dist/structures/role.js +112 -0
  238. package/dist/structures/user.d.ts +59 -0
  239. package/dist/structures/user.js +94 -0
  240. package/dist/structures/webhook.d.ts +46 -0
  241. package/dist/structures/webhook.js +79 -0
  242. package/dist/tasks/convention.d.ts +26 -0
  243. package/dist/tasks/convention.js +78 -0
  244. package/dist/tasks/schedule.d.ts +54 -0
  245. package/dist/tasks/schedule.js +236 -0
  246. package/dist/tasks/scheduler.d.ts +33 -0
  247. package/dist/tasks/scheduler.js +107 -0
  248. package/dist/tasks/task.d.ts +76 -0
  249. package/dist/tasks/task.js +28 -0
  250. package/dist/util/case.d.ts +13 -0
  251. package/dist/util/case.js +54 -0
  252. package/dist/util/duration.d.ts +8 -0
  253. package/dist/util/duration.js +29 -0
  254. package/dist/util/websocket.d.ts +9 -0
  255. package/dist/util/websocket.js +31 -0
  256. package/dist/voice/connection.d.ts +90 -0
  257. package/dist/voice/connection.js +494 -0
  258. package/dist/voice/dave.d.ts +109 -0
  259. package/dist/voice/dave.js +245 -0
  260. package/dist/voice/opus.d.ts +19 -0
  261. package/dist/voice/opus.js +273 -0
  262. package/dist/voice/session.d.ts +55 -0
  263. package/dist/voice/session.js +405 -0
  264. package/dist/voice/source.d.ts +22 -0
  265. package/dist/voice/source.js +195 -0
  266. package/dist/voice/transport.d.ts +43 -0
  267. package/dist/voice/transport.js +140 -0
  268. package/dist/voice/usage.d.ts +4 -0
  269. package/dist/voice/usage.js +33 -0
  270. package/package.json +26 -7
  271. package/vendor/dave/LICENSE +231 -0
  272. package/vendor/dave/libdave.d.mts +99 -0
  273. package/vendor/dave/libdave.mjs +2 -0
  274. package/vendor/dave/libdave.wasm +0 -0
  275. package/dist/components.d.ts +0 -97
  276. package/dist/components.js +0 -260
  277. package/dist/context.d.ts +0 -72
  278. package/dist/context.js +0 -273
  279. package/dist/discord/client.d.ts +0 -35
  280. package/dist/discord/client.js +0 -66
  281. package/dist/discord/constants.d.ts +0 -49
  282. package/dist/discord/constants.js +0 -61
  283. package/dist/discord/events.d.ts +0 -129
  284. package/dist/discord/events.js +0 -102
  285. package/dist/discord/gateway.d.ts +0 -61
  286. package/dist/discord/gateway.js +0 -320
  287. package/dist/discord/payloads.d.ts +0 -275
  288. package/dist/discord/payloads.js +0 -3
  289. package/dist/discord/presence.d.ts +0 -42
  290. package/dist/discord/presence.js +0 -51
  291. package/dist/discord/rest.d.ts +0 -49
  292. package/dist/discord/rest.js +0 -271
  293. package/dist/discord/types.d.ts +0 -334
  294. package/dist/discord/types.js +0 -4
  295. package/dist/events.d.ts +0 -94
  296. package/dist/events.js +0 -255
  297. package/dist/presence.d.ts +0 -92
  298. package/dist/presence.js +0 -250
  299. package/dist/structures/Channel.d.ts +0 -110
  300. package/dist/structures/Channel.js +0 -171
  301. package/dist/structures/Guild.d.ts +0 -70
  302. package/dist/structures/Guild.js +0 -105
  303. package/dist/structures/Interaction.d.ts +0 -113
  304. package/dist/structures/Interaction.js +0 -274
  305. package/dist/structures/Member.d.ts +0 -70
  306. package/dist/structures/Member.js +0 -124
  307. package/dist/structures/Message.d.ts +0 -87
  308. package/dist/structures/Message.js +0 -182
  309. package/dist/structures/Reaction.d.ts +0 -38
  310. package/dist/structures/Reaction.js +0 -64
  311. package/dist/structures/Role.d.ts +0 -43
  312. package/dist/structures/Role.js +0 -77
  313. package/dist/structures/User.d.ts +0 -41
  314. package/dist/structures/User.js +0 -79
  315. package/dist/structures/utils.d.ts +0 -72
  316. package/dist/structures/utils.js +0 -59
  317. package/dist/utils/errors.d.ts +0 -5
  318. package/dist/utils/errors.js +0 -28
  319. package/dist/utils/modules.d.ts +0 -39
  320. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,206 @@
1
+ // `chapterjs build`: prepares the bot for production. Everything that can
2
+ // go wrong with the files of the project goes wrong here, before anything
3
+ // is online: types are checked, every file is run, then the whole bot is
4
+ // compiled into one JavaScript file in `.chapterjs/build/`. `chapterjs
5
+ // start` runs that, and only that: the bot in production is exactly what
6
+ // was built.
7
+ //
8
+ // The compiling is done by esbuild, the one dependency of the framework. It
9
+ // is only loaded here: running a bot never needs it.
10
+ import { spawn } from 'node:child_process';
11
+ import { createHash } from 'node:crypto';
12
+ import { mkdir, readdir, readFile, rm, stat, writeFile, } from 'node:fs/promises';
13
+ import { join, relative, sep } from 'node:path';
14
+ import { pathToFileURL } from 'node:url';
15
+ import { enableProjectLoader } from '../loader/hot.js';
16
+ import { listFolder } from '../loader/loader.js';
17
+ import { sourcesJoinVoice } from '../voice/usage.js';
18
+ import { CONVENTIONS, createProject, hasSources, writeTypes, } from './project.js';
19
+ /** Where the build of a project is, from its folder. */
20
+ export const buildDir = (cwd) => join(cwd, '.chapterjs', 'build');
21
+ /** The file the whole bot is compiled into, in the build. */
22
+ export const BUNDLE = 'bot.js';
23
+ /** One value for everything in a folder: it changes when a file does. */
24
+ export async function fingerprint(dir) {
25
+ const hash = createHash('sha256');
26
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
27
+ const files = entries
28
+ .filter(entry => entry.isFile())
29
+ .map(entry => join(entry.parentPath, entry.name))
30
+ .sort();
31
+ for (const file of files) {
32
+ hash.update(relative(dir, file).split(sep).join('/'));
33
+ hash.update('\0');
34
+ hash.update(await readFile(file));
35
+ hash.update('\0');
36
+ }
37
+ return hash.digest('hex');
38
+ }
39
+ /** The build of a project, or why there is none to run. */
40
+ export async function readBuild(cwd, version) {
41
+ let info;
42
+ try {
43
+ info = JSON.parse(await readFile(join(buildDir(cwd), 'build.json'), 'utf8'));
44
+ await stat(join(buildDir(cwd), BUNDLE));
45
+ }
46
+ catch {
47
+ return {
48
+ problem: 'There is no build of your bot here.\nRun "chapterjs build" first (the build script of your project), then start again.',
49
+ };
50
+ }
51
+ if (info.version !== version) {
52
+ return {
53
+ problem: `This build was made with chapterjs ${info.version}, and this is chapterjs ${version}.\nRun "chapterjs build" again.`,
54
+ };
55
+ }
56
+ // On a host that only received the build, there are no sources to compare.
57
+ const src = join(cwd, 'src');
58
+ const hasSrc = (await stat(src).catch(() => null))?.isDirectory() === true;
59
+ return {
60
+ info,
61
+ stale: hasSrc && (await fingerprint(src)) !== info.sources,
62
+ };
63
+ }
64
+ /** Runs the TypeScript of the project on itself. `null`: there is none. */
65
+ async function checkTypes(cwd) {
66
+ const tsc = join(cwd, 'node_modules', '.bin', 'tsc');
67
+ const [hasTsc, hasConfig] = await Promise.all([
68
+ stat(tsc).catch(() => null),
69
+ stat(join(cwd, 'tsconfig.json')).catch(() => null),
70
+ ]);
71
+ if (!hasTsc || !hasConfig)
72
+ return null;
73
+ return new Promise(resolve => {
74
+ const child = spawn(tsc, ['-b', '--pretty', 'false'], { cwd });
75
+ let output = '';
76
+ child.stdout.on('data', (chunk) => (output += chunk.toString()));
77
+ child.stderr.on('data', (chunk) => (output += chunk.toString()));
78
+ child.on('error', error => resolve({ ok: false, output: error.message }));
79
+ child.on('close', code => resolve({ ok: code === 0, output }));
80
+ });
81
+ }
82
+ /**
83
+ * Loads the bot a build compiled: every file of the project, already run.
84
+ * Errors that come out of it afterwards name the file and the line of the
85
+ * project, not of the compiled file: Node reads the map next to it.
86
+ */
87
+ export async function loadBundle(cwd) {
88
+ process.setSourceMapsEnabled(true);
89
+ const module = (await import(pathToFileURL(join(buildDir(cwd), BUNDLE)).href));
90
+ return module.files;
91
+ }
92
+ /** Resolves with the exit code of the process. */
93
+ export async function build(options) {
94
+ const { cwd, log } = options;
95
+ if (!(await hasSources(cwd, log)))
96
+ return 1;
97
+ const src = join(cwd, 'src');
98
+ const out = buildDir(cwd);
99
+ // Whatever happens next, the last build is not one to start any more.
100
+ await rm(out, { recursive: true, force: true });
101
+ const stopped = async (failures, why = "can't run") => {
102
+ log.error(`${failures === 1 ? 'This file' : `These ${failures} files`} ${why}, so your bot was not built. Fix ${failures === 1 ? 'it' : 'them'} and build again: chapterjs dev shows the same errors while you write.`);
103
+ await rm(out, { recursive: true, force: true });
104
+ return 1;
105
+ };
106
+ // 1. Every file must run, one by one: each says what is wrong with it.
107
+ enableProjectLoader(src, { reload: false });
108
+ const sources = createProject({ cwd, version: options.version, log });
109
+ const failures = await sources.load();
110
+ // 2. Types: what the editor underlines, for the whole project. Written
111
+ // once the files ran, so a language file only offers the commands they
112
+ // do not describe; written even when a file failed, so the editor
113
+ // follows.
114
+ await writeTypes(cwd, sources.commands, sources.messages, sources.languageDefaults);
115
+ if (failures.length > 0) {
116
+ for (const failure of failures)
117
+ sources.report(failure);
118
+ return stopped(failures.length);
119
+ }
120
+ const types = await checkTypes(cwd);
121
+ if (types === null) {
122
+ log.info('Types were not checked: TypeScript is not installed in this project (or it has no tsconfig.json).');
123
+ }
124
+ else if (!types.ok) {
125
+ log.error(`Your project has type errors, so it was not built:\n${types.output.trim()}`);
126
+ return 1;
127
+ }
128
+ else {
129
+ log.success('Types checked');
130
+ }
131
+ // 3. The whole bot in one file: every file of the conventional folders,
132
+ // with what it imports from the project. Packages stay where they are
133
+ // installed: the bot and the framework must share the same ones.
134
+ const found = (await Promise.all(CONVENTIONS.map(convention => listFolder(cwd, convention)))).flat();
135
+ const entry = [
136
+ ...found.map(({ path }, index) => `import * as m${index} from ${JSON.stringify(path)};`),
137
+ `export const files = [${found
138
+ .map(({ file }, index) => `{ file: ${JSON.stringify(file)}, exports: m${index} }`)
139
+ .join(', ')}];`,
140
+ ].join('\n');
141
+ await mkdir(out, { recursive: true });
142
+ // Loaded when it is needed: no other command pays for it.
143
+ const esbuild = await import('esbuild');
144
+ try {
145
+ await esbuild.build({
146
+ stdin: {
147
+ contents: entry,
148
+ resolveDir: cwd,
149
+ sourcefile: 'chapterjs-build.js',
150
+ loader: 'js',
151
+ },
152
+ absWorkingDir: cwd,
153
+ outfile: join(out, BUNDLE),
154
+ bundle: true,
155
+ platform: 'node',
156
+ format: 'esm',
157
+ target: 'node24',
158
+ packages: 'external',
159
+ // Compact, but with the names the developer wrote: an error must
160
+ // still read like their code. The map gives back files and lines.
161
+ minifyWhitespace: true,
162
+ minifySyntax: true,
163
+ minifyIdentifiers: false,
164
+ keepNames: true,
165
+ sourcemap: true,
166
+ sourcesContent: false,
167
+ logLevel: 'silent',
168
+ });
169
+ }
170
+ catch (error) {
171
+ const errors = error.errors ?? [];
172
+ if (errors.length === 0)
173
+ throw error;
174
+ for (const { text, location } of errors) {
175
+ log.error(`${location ? `${location.file}:${location.line} ` : ''}${text}`);
176
+ }
177
+ return stopped(errors.length, "can't be compiled");
178
+ }
179
+ // 4. What was compiled is run once, as production will run it.
180
+ const built = createProject({
181
+ cwd,
182
+ version: options.version,
183
+ log,
184
+ built: await loadBundle(cwd),
185
+ });
186
+ const broken = await built.load();
187
+ if (broken.length > 0) {
188
+ for (const failure of broken)
189
+ built.report(failure);
190
+ return stopped(broken.length, "can't run once compiled");
191
+ }
192
+ const info = {
193
+ version: options.version,
194
+ sources: await fingerprint(src),
195
+ commands: built.commands.size,
196
+ events: built.events.size,
197
+ voice: await sourcesJoinVoice(cwd),
198
+ };
199
+ await writeFile(join(out, 'build.json'), JSON.stringify(info, null, 2));
200
+ if (built.isEmpty())
201
+ log.info(built.summary());
202
+ const { size } = await stat(join(out, BUNDLE));
203
+ log.success(`Built in .chapterjs/build: ${built.isEmpty() ? 'nothing to run yet' : built.summary().replace(/ loaded$/, '')} (${Math.max(1, Math.round(size / 1024))} kB)`);
204
+ log.info('Run chapterjs start to put it online.');
205
+ return 0;
206
+ }
package/dist/cli/cli.js CHANGED
@@ -1,33 +1,116 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { build } from './build.js';
4
+ import { readAssignment } from './cluster.js';
2
5
  import { dev } from './dev.js';
3
- const HELP = `Usage: chapterjs <command>
6
+ import { writeTypes } from './project.js';
7
+ import { createLog } from './log.js';
8
+ import { nodeTooOld } from './node-version.js';
9
+ import { start } from './start.js';
10
+ const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
11
+ const USAGE = `Usage: chapterjs <command>
4
12
 
5
- dev Local development
6
- build Validate and bundle the project`;
7
- const [command, ...args] = process.argv.slice(2);
8
- // package.json engines: global WebSocket and running .ts files without flags
9
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
10
- if (major < 22 || (major === 22 && minor < 18)) {
11
- console.error(`✗ ChapterJS needs Node.js 22.18 or newer, you have ${process.version}. Install the latest LTS from https://nodejs.org`);
13
+ Commands:
14
+ dev Run your bot on your dev server and reload it when you save a file
15
+ build Check your bot and prepare it for production
16
+ start Run for everyone the bot that was built
17
+ sync Write the types of your project (dev does it too)
18
+
19
+ Options of start:
20
+ --processes <n> How many processes to use (by default: as many as the
21
+ size of your bot needs)`;
22
+ const colors = process.stdout.isTTY === true && !process.env.NO_COLOR;
23
+ const log = createLog(line => console.log(line), colors);
24
+ const tooOld = nodeTooOld();
25
+ if (tooOld) {
26
+ log.error(tooOld);
12
27
  process.exit(1);
13
28
  }
14
- try {
15
- switch (command) {
16
- case 'dev':
17
- await dev();
18
- break;
19
- case 'build':
20
- console.log('build');
21
- break;
22
- default:
23
- if (command && command !== 'help' && command !== '--help') {
24
- console.error(`✗ Unknown command "${command}".\n`);
25
- }
26
- console.log(HELP);
27
- process.exit(command && command !== 'help' && command !== '--help' ? 1 : 0);
29
+ const [command, ...rest] = process.argv.slice(2);
30
+ /**
31
+ * Ctrl+C and a stop asked by the system both end the session cleanly, so
32
+ * the bot goes offline at once. A second one does not wait.
33
+ */
34
+ function stopSignal() {
35
+ const controller = new AbortController();
36
+ for (const name of ['SIGINT', 'SIGTERM']) {
37
+ process.on(name, () => {
38
+ if (controller.signal.aborted)
39
+ process.exit(1);
40
+ controller.abort();
41
+ });
28
42
  }
43
+ return controller.signal;
29
44
  }
30
- catch (error) {
31
- console.error(error instanceof Error ? `✗ ${error.message}` : error);
32
- process.exit(1);
45
+ /** `--processes 4` or `--processes=4`; `null` when it is not a count. */
46
+ function readProcesses(args) {
47
+ const index = args.findIndex(arg => arg === '--processes' || arg.startsWith('--processes='));
48
+ if (index === -1)
49
+ return args.length === 0 ? undefined : null;
50
+ const arg = args[index];
51
+ const value = arg.includes('=')
52
+ ? arg.slice(arg.indexOf('=') + 1)
53
+ : args[index + 1];
54
+ const others = args.length - (arg.includes('=') ? 1 : 2);
55
+ const count = Number(value);
56
+ return others === 0 && Number.isInteger(count) && count >= 1 ? count : null;
57
+ }
58
+ if (command === 'dev') {
59
+ const controller = { signal: stopSignal() };
60
+ process.exitCode = await dev({
61
+ cwd: process.cwd(),
62
+ env: process.env,
63
+ version,
64
+ log,
65
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
66
+ signal: controller.signal,
67
+ });
68
+ // Nothing may keep the process alive once the bot is disconnected.
69
+ process.exit();
70
+ }
71
+ else if (command === 'start') {
72
+ const processes = readProcesses(rest);
73
+ if (processes === null) {
74
+ log.error(`chapterjs start takes one option: --processes followed by a number, like "chapterjs start --processes 4". Got "${rest.join(' ')}".`);
75
+ process.exitCode = 1;
76
+ }
77
+ else {
78
+ const assignment = readAssignment(process.env);
79
+ process.exitCode = await start({
80
+ cwd: process.cwd(),
81
+ env: { ...process.env, CHAPTERJS_COLORS: colors ? '1' : '0' },
82
+ version,
83
+ // A process started by another prints for it: colors are its choice.
84
+ log: assignment
85
+ ? createLog(line => console.log(line), process.env.CHAPTERJS_COLORS === '1')
86
+ : log,
87
+ write: line => console.log(line),
88
+ signal: stopSignal(),
89
+ processes,
90
+ assignment,
91
+ script: process.argv[1],
92
+ args: ['start'],
93
+ });
94
+ process.exit();
95
+ }
96
+ }
97
+ else if (command === 'sync') {
98
+ // Run after installing, so the editor knows the types before the first
99
+ // `chapterjs dev`.
100
+ await writeTypes(process.cwd());
101
+ log.success('Types written to .chapterjs/');
102
+ }
103
+ else if (command === '--version' || command === '-v') {
104
+ console.log(version);
105
+ }
106
+ else if (command === undefined || command === '--help' || command === '-h') {
107
+ console.log(USAGE);
108
+ }
109
+ else if (command === 'build') {
110
+ process.exitCode = await build({ cwd: process.cwd(), version, log });
111
+ process.exit();
112
+ }
113
+ else {
114
+ log.error(`"${command}" is not a command.\n${USAGE}`);
115
+ process.exitCode = 1;
33
116
  }
@@ -0,0 +1,70 @@
1
+ import { type IdentifyGate } from '../gateway/identify-queue.js';
2
+ /** How many shards one process runs when nothing else is asked. */
3
+ export declare const SHARDS_PER_PROCESS = 4;
4
+ /** The exit code of a process Discord refused for good: not started again. */
5
+ export declare const REFUSED = 78;
6
+ /** The share of one process, given to it when it starts. */
7
+ export interface Assignment {
8
+ /** Which process this is, from 0. */
9
+ index: number;
10
+ /** How many processes run the bot. */
11
+ processes: number;
12
+ /** The shards it runs, out of `count`. */
13
+ ids: number[];
14
+ count: number;
15
+ }
16
+ /** The name of the variable a process finds its share in. */
17
+ export declare const ASSIGNMENT_VARIABLE = "CHAPTERJS_PROCESS";
18
+ /**
19
+ * How many processes run a bot of `shards` shards: what was asked, or one
20
+ * for every few shards, never more than the machine has CPUs.
21
+ */
22
+ export declare function planProcesses(shards: number, asked?: number, cpus?: number): number;
23
+ /** The shards of each process: as even as it gets, in order. */
24
+ export declare function splitShards(count: number, processes: number): number[][];
25
+ /** Reads the share of this process, if it was started by another one. */
26
+ export declare function readAssignment(env: Record<string, string | undefined>): Assignment | null;
27
+ /**
28
+ * In a process started by another: asks that one when a shard may
29
+ * identify, and hears when to stop.
30
+ */
31
+ export declare function connectToPrimary(): {
32
+ gate: IdentifyGate;
33
+ ready(info: {
34
+ guilds: number;
35
+ user: string;
36
+ }): void;
37
+ refused(): void;
38
+ /** Resolves when this process is asked to stop, or is left alone. */
39
+ stopped: Promise<void>;
40
+ };
41
+ export interface ClusterOptions {
42
+ /** The file to run in each process, and its arguments. */
43
+ script: string;
44
+ args: readonly string[];
45
+ env: Record<string, string | undefined>;
46
+ shards: number;
47
+ processes: number;
48
+ maxConcurrency: number;
49
+ /** Aborted when the bot is asked to stop. */
50
+ signal: AbortSignal;
51
+ /** Shows a line printed by one of the processes. */
52
+ write: (line: string) => void;
53
+ /** Every process is connected, for the first time. */
54
+ onReady: (info: {
55
+ guilds: number;
56
+ user: string;
57
+ }) => void;
58
+ /** A process stopped by itself and is started again. */
59
+ onRestart: (index: number, why: string) => void;
60
+ /** Only tests change these. */
61
+ identifyInterval?: number | undefined;
62
+ restartDelay?: number;
63
+ stopTimeout?: number;
64
+ }
65
+ /**
66
+ * Starts the processes of the bot and looks after them until asked to
67
+ * stop. Resolves with the exit code: 0 when stopped, 1 when Discord refused
68
+ * the bot.
69
+ */
70
+ export declare function runCluster(options: ClusterOptions): Promise<number>;
@@ -0,0 +1,213 @@
1
+ // Several processes for one bot. Discord splits a bot into shards, each
2
+ // receiving the events of some servers; a process can only use one CPU, so
3
+ // a large bot spreads its shards over several processes. The first process
4
+ // (the one the developer started) checks everything once, then starts the
5
+ // others and looks after them; each of them runs the project for its share
6
+ // of the shards.
7
+ //
8
+ // What processes must agree on goes through the first one: when a shard may
9
+ // identify. A setup on several machines would give each process another
10
+ // `IdentifyGate`, and nothing else here would change.
11
+ // https://docs.discord.com/developers/events/gateway#sharding
12
+ import { fork } from 'node:child_process';
13
+ import { availableParallelism } from 'node:os';
14
+ import { createInterface } from 'node:readline';
15
+ import { setTimeout as sleep } from 'node:timers/promises';
16
+ import { IdentifyQueue } from '../gateway/identify-queue.js';
17
+ /** How many shards one process runs when nothing else is asked. */
18
+ export const SHARDS_PER_PROCESS = 4;
19
+ /** The exit code of a process Discord refused for good: not started again. */
20
+ export const REFUSED = 78;
21
+ /** The name of the variable a process finds its share in. */
22
+ export const ASSIGNMENT_VARIABLE = 'CHAPTERJS_PROCESS';
23
+ /**
24
+ * How many processes run a bot of `shards` shards: what was asked, or one
25
+ * for every few shards, never more than the machine has CPUs.
26
+ */
27
+ export function planProcesses(shards, asked, cpus = availableParallelism()) {
28
+ const wanted = asked ?? Math.min(cpus, Math.ceil(shards / SHARDS_PER_PROCESS));
29
+ return Math.max(1, Math.min(shards, Math.floor(wanted)));
30
+ }
31
+ /** The shards of each process: as even as it gets, in order. */
32
+ export function splitShards(count, processes) {
33
+ const shares = [];
34
+ let next = 0;
35
+ for (let index = 0; index < processes; index++) {
36
+ const size = Math.ceil((count - next) / (processes - index));
37
+ shares.push(Array.from({ length: size }, (_, offset) => next + offset));
38
+ next += size;
39
+ }
40
+ return shares;
41
+ }
42
+ /** Reads the share of this process, if it was started by another one. */
43
+ export function readAssignment(env) {
44
+ const raw = env[ASSIGNMENT_VARIABLE];
45
+ return raw ? JSON.parse(raw) : null;
46
+ }
47
+ /**
48
+ * In a process started by another: asks that one when a shard may
49
+ * identify, and hears when to stop.
50
+ */
51
+ export function connectToPrimary() {
52
+ const waiting = new Map();
53
+ let lastId = 0;
54
+ const send = (message) => {
55
+ // The first process may be gone: there is nobody to tell any more.
56
+ if (process.connected)
57
+ process.send?.(message);
58
+ };
59
+ let stop;
60
+ const stopped = new Promise(resolve => (stop = resolve));
61
+ process.on('message', (message) => {
62
+ if (message.type === 'identify') {
63
+ waiting.get(message.id)?.();
64
+ waiting.delete(message.id);
65
+ }
66
+ else if (message.type === 'stop') {
67
+ stop();
68
+ }
69
+ });
70
+ // Without the process that started it, nothing looks after this one.
71
+ process.on('disconnect', stop);
72
+ return {
73
+ gate: {
74
+ wait: shard => new Promise(resolve => {
75
+ const id = ++lastId;
76
+ waiting.set(id, resolve);
77
+ send({ type: 'identify', shard, id });
78
+ }),
79
+ },
80
+ ready: info => send({ type: 'ready', ...info }),
81
+ refused: () => send({ type: 'refused' }),
82
+ stopped,
83
+ };
84
+ }
85
+ /**
86
+ * Sends a message to a process, and says whether it could. A process that
87
+ * is exiting has a channel that fails: the error goes to the callback
88
+ * instead of crashing the first process.
89
+ */
90
+ function tell(child, message) {
91
+ if (!child.connected)
92
+ return false;
93
+ child.send(message, () => { });
94
+ return true;
95
+ }
96
+ /**
97
+ * Starts the processes of the bot and looks after them until asked to
98
+ * stop. Resolves with the exit code: 0 when stopped, 1 when Discord refused
99
+ * the bot.
100
+ */
101
+ export async function runCluster(options) {
102
+ const { signal } = options;
103
+ const gate = new IdentifyQueue(options.maxConcurrency, options.identifyInterval);
104
+ const shares = splitShards(options.shards, options.processes);
105
+ const workers = new Map();
106
+ const stopped = new WeakSet();
107
+ const ready = new Map();
108
+ let announced = false;
109
+ let refused = false;
110
+ let end;
111
+ const ended = new Promise(resolve => (end = resolve));
112
+ const stopping = () => signal.aborted || refused;
113
+ const start = (index, delay) => {
114
+ const assignment = {
115
+ index,
116
+ processes: options.processes,
117
+ ids: shares[index],
118
+ count: options.shards,
119
+ };
120
+ const started = Date.now();
121
+ const child = fork(options.script, [...options.args], {
122
+ env: {
123
+ ...options.env,
124
+ [ASSIGNMENT_VARIABLE]: JSON.stringify(assignment),
125
+ },
126
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
127
+ });
128
+ workers.set(index, child);
129
+ // A message sent while the process exits fails; it must not stop this
130
+ // one (see `tell`), and its exit is what matters.
131
+ child.on('error', () => { });
132
+ // Everything a process prints (the framework and the code of the
133
+ // project alike) is shown with which one said it.
134
+ for (const stream of [child.stdout, child.stderr]) {
135
+ createInterface({ input: stream }).on('line', line => options.write(`[${index + 1}] ${line}`));
136
+ }
137
+ child.on('message', (message) => {
138
+ if (message.type === 'identify') {
139
+ void gate.wait(message.shard).then(() => {
140
+ tell(child, { type: 'identify', id: message.id });
141
+ });
142
+ }
143
+ else if (message.type === 'ready') {
144
+ ready.set(index, message);
145
+ if (!announced && ready.size === options.processes) {
146
+ announced = true;
147
+ options.onReady({
148
+ guilds: [...ready.values()].reduce((sum, r) => sum + r.guilds, 0),
149
+ user: message.user,
150
+ });
151
+ }
152
+ }
153
+ else if (message.type === 'refused') {
154
+ refused = true;
155
+ }
156
+ });
157
+ child.on('exit', (code, killedBy) => {
158
+ workers.delete(index);
159
+ if (code === REFUSED)
160
+ refused = true;
161
+ if (stopping()) {
162
+ // The others have nothing to wait for either.
163
+ for (const other of workers.values())
164
+ stopWorker(other);
165
+ if (workers.size === 0)
166
+ end();
167
+ return;
168
+ }
169
+ // A process that ran for a while starts again at once; one that
170
+ // keeps falling waits longer each time.
171
+ const next = Date.now() - started > 60_000
172
+ ? (options.restartDelay ?? 1000)
173
+ : Math.min(60_000, Math.max(delay, 1) * 2);
174
+ options.onRestart(index, killedBy ? `stopped by ${killedBy}` : `exit code ${code}`);
175
+ void sleep(delay).then(() => {
176
+ if (stopping()) {
177
+ if (workers.size === 0)
178
+ end();
179
+ }
180
+ else {
181
+ start(index, next);
182
+ }
183
+ });
184
+ });
185
+ };
186
+ const stopWorker = (child) => {
187
+ // Asked once: a process already stopping has nothing more to hear.
188
+ if (stopped.has(child))
189
+ return;
190
+ stopped.add(child);
191
+ if (!tell(child, { type: 'stop' }))
192
+ child.kill('SIGTERM');
193
+ // A process that does not stop is not waited for forever.
194
+ const timer = setTimeout(() => child.kill('SIGKILL'), options.stopTimeout ?? 15_000);
195
+ timer.unref();
196
+ child.once('exit', () => clearTimeout(timer));
197
+ };
198
+ for (let index = 0; index < options.processes; index++) {
199
+ start(index, options.restartDelay ?? 1000);
200
+ }
201
+ const stopAll = () => {
202
+ if (workers.size === 0)
203
+ end();
204
+ for (const child of workers.values())
205
+ stopWorker(child);
206
+ };
207
+ if (signal.aborted)
208
+ stopAll();
209
+ else
210
+ signal.addEventListener('abort', stopAll, { once: true });
211
+ await ended;
212
+ return refused ? 1 : 0;
213
+ }
package/dist/cli/dev.d.ts CHANGED
@@ -1 +1,18 @@
1
- export declare function dev(): Promise<void>;
1
+ import type { Log } from './log.js';
2
+ export interface DevOptions {
3
+ /** The folder of the project. */
4
+ cwd: string;
5
+ env: Record<string, string | undefined>;
6
+ /** The version of the framework. */
7
+ version: string;
8
+ log: Log;
9
+ /** Whether a person is watching the terminal. */
10
+ interactive: boolean;
11
+ /** Aborted when the user asks to stop (Ctrl+C). */
12
+ signal: AbortSignal;
13
+ /** Only tests change these. */
14
+ pollInterval?: number;
15
+ deferAfter?: number;
16
+ }
17
+ /** Runs until stopped. Resolves with the exit code of the process. */
18
+ export declare function dev(options: DevOptions): Promise<number>;