meocord 3.1.0 → 3.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/README.md +138 -15
- package/dist/cjs/core/index.cjs +41 -18
- package/dist/cjs/decorator/index.cjs +1 -1
- package/dist/esm/bin/app-template/README.md.template +61 -0
- package/dist/esm/bin/app-template/_env.example.template +2 -0
- package/dist/esm/bin/app-template/_gitignore.template +22 -0
- package/dist/esm/bin/app-template/_prettierrc.mjs.template +10 -0
- package/dist/esm/bin/app-template/eslint.config.ts.template +25 -0
- package/dist/esm/bin/app-template/meocord.config.ts.template +25 -0
- package/dist/esm/bin/app-template/package.json.template +38 -0
- package/dist/esm/bin/app-template/src/app.ts.template +44 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +19 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.ts.template +13 -0
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template +28 -0
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.ts.template +13 -0
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template +29 -0
- package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.ts.template +11 -0
- package/dist/esm/bin/app-template/src/controllers/slash/builders/sample.builder.ts.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.ts.template +19 -0
- package/dist/esm/bin/app-template/src/guards/rate-limit.guard.spec.ts.template +13 -0
- package/dist/esm/bin/app-template/src/guards/rate-limit.guard.ts.template +52 -0
- package/dist/esm/bin/app-template/src/main.ts.template +14 -0
- package/dist/esm/bin/app-template/src/services/sample.service.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/services/sample.service.ts.template +9 -0
- package/dist/esm/bin/app-template/tsconfig.eslint.json.template +5 -0
- package/dist/esm/bin/app-template/tsconfig.json.template +30 -0
- package/dist/esm/bin/app-template/tsconfig.test.json.template +8 -0
- package/dist/esm/bin/app-template/vitest.config.ts.template +35 -0
- package/dist/esm/bin/builder-template/controller/button.controller.template +1 -1
- package/dist/esm/bin/helper/app-generator.helper.js +78 -0
- package/dist/esm/bin/meocord.js +156 -75
- package/dist/esm/core/meocord.app.js +42 -19
- package/dist/esm/decorator/guard.decorator.js +1 -1
- package/dist/esm/util/common.util.js +11 -3
- package/dist/esm/util/generator-cli.util.js +21 -4
- package/dist/esm/util/package-manager.util.js +9 -2
- package/dist/esm/util/package-version.util.js +32 -0
- package/dist/esm/util/runtime.util.js +72 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/decorator/index.d.ts +1 -1
- package/dist/types/interface/index.d.ts +5 -3
- package/package.json +11 -12
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- [Testing](#testing)
|
|
26
26
|
- [Deployment](#deployment)
|
|
27
27
|
- [Contributing](#contributing)
|
|
28
|
+
- [Release Notes](#release-notes)
|
|
28
29
|
- [License](#license)
|
|
29
30
|
|
|
30
31
|
---
|
|
@@ -66,30 +67,57 @@ npx meocord create <your-app-name> --use-pnpm
|
|
|
66
67
|
npx meocord create <your-app-name> --use-yarn
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
The generated project is named after what you passed, pins the framework version that
|
|
71
|
+
created it, and comes with a working slash command, button, select menu, modal, context
|
|
72
|
+
menu, message and reaction controller, plus a guard, a service and a spec for each.
|
|
73
|
+
|
|
74
|
+
Add your bot token and start:
|
|
70
75
|
|
|
71
76
|
```shell
|
|
72
|
-
|
|
77
|
+
cd <your-app-name>
|
|
78
|
+
cp .env.example .env # then put your token in DISCORD_TOKEN
|
|
79
|
+
npx meocord start --dev # development with live-reload
|
|
73
80
|
npx meocord start --build --prod # production build + start
|
|
74
81
|
```
|
|
75
82
|
|
|
83
|
+
The token is read from the environment rather than written into `meocord.config.ts`,
|
|
84
|
+
which is committed — `.env` is gitignored so a token cannot be pushed by accident.
|
|
85
|
+
Building needs no token; only starting does.
|
|
86
|
+
|
|
76
87
|
### Quick Example
|
|
77
88
|
|
|
78
|
-
A minimal slash command
|
|
89
|
+
A minimal slash command. A command Discord knows about needs a builder — that is what gets registered. The
|
|
90
|
+
builder receives the name from `@Command`, so the two cannot drift apart:
|
|
79
91
|
|
|
80
92
|
```typescript
|
|
81
|
-
import {
|
|
93
|
+
import { SlashCommandBuilder } from 'discord.js'
|
|
94
|
+
import { CommandBuilder } from 'meocord/decorator'
|
|
82
95
|
import { CommandType } from 'meocord/enum'
|
|
96
|
+
|
|
97
|
+
@CommandBuilder(CommandType.SLASH)
|
|
98
|
+
export class GreetingCommandBuilder {
|
|
99
|
+
build(commandName: string) {
|
|
100
|
+
return new SlashCommandBuilder()
|
|
101
|
+
.setName(commandName)
|
|
102
|
+
.setDescription('Greets someone')
|
|
103
|
+
.addStringOption(option => option.setName('name').setDescription('Who to greet').setRequired(true))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { Controller, Command, UseGuard } from 'meocord/decorator'
|
|
83
110
|
import { type ChatInputCommandInteraction } from 'discord.js'
|
|
84
|
-
import {
|
|
111
|
+
import { GreetingCommandBuilder } from '@src/controllers/slash/builders/greeting.builder.js'
|
|
112
|
+
import { RateLimitGuard } from '@src/guards/rate-limit.guard.js'
|
|
85
113
|
import { GreetingService } from '@src/services/greeting.service.js'
|
|
86
114
|
|
|
87
115
|
@Controller()
|
|
88
116
|
export class GreetingSlashController {
|
|
89
117
|
constructor(private readonly greetingService: GreetingService) {}
|
|
90
118
|
|
|
91
|
-
@Command('greet',
|
|
92
|
-
@UseGuard({ provide:
|
|
119
|
+
@Command('greet', GreetingCommandBuilder)
|
|
120
|
+
@UseGuard({ provide: RateLimitGuard, params: { limit: 3, windowInSeconds: 10 } })
|
|
93
121
|
async greet(interaction: ChatInputCommandInteraction) {
|
|
94
122
|
const name = interaction.options.getString('name', true)
|
|
95
123
|
const message = await this.greetingService.buildGreeting(name)
|
|
@@ -126,6 +154,10 @@ export class App {}
|
|
|
126
154
|
|
|
127
155
|
```
|
|
128
156
|
.
|
|
157
|
+
├── README.md
|
|
158
|
+
├── .env.example
|
|
159
|
+
├── .gitignore
|
|
160
|
+
├── .prettierrc.mjs
|
|
129
161
|
├── meocord.config.ts
|
|
130
162
|
├── eslint.config.ts
|
|
131
163
|
├── vitest.config.ts
|
|
@@ -231,18 +263,35 @@ npx meocord --help
|
|
|
231
263
|
| `generate` | `g` | Scaffold controllers, services, guards |
|
|
232
264
|
| `show` | — | Display framework info |
|
|
233
265
|
|
|
234
|
-
|
|
266
|
+
Every command's own flags:
|
|
267
|
+
|
|
268
|
+
| Command | Flags |
|
|
269
|
+
| ---------- | ------------------------------------------------------- |
|
|
270
|
+
| `create` | `--use-npm` · `--use-yarn` · `--use-pnpm` · `--use-bun` |
|
|
271
|
+
| `build` | `-d, --dev` · `-p, --prod` |
|
|
272
|
+
| `start` | `-b, --build` · `-d, --dev` · `-p, --prod` |
|
|
273
|
+
| `show` | `-w, --warranty` · `-c, --license` |
|
|
274
|
+
| `generate` | see the sub-commands below |
|
|
275
|
+
|
|
276
|
+
`meocord -V` / `--version` prints the installed version.
|
|
277
|
+
|
|
278
|
+
`start` accepts one environment variable, `MEOCORD_RUNTIME`, which pins the binary the application is run with — see [Which runtime the bot runs on](#which-runtime-the-bot-runs-on).
|
|
235
279
|
|
|
236
280
|
```shell
|
|
237
281
|
npx meocord build --prod # production build
|
|
238
|
-
npx meocord build --dev # development build
|
|
239
282
|
npx meocord start --dev # dev mode with live-reload
|
|
240
283
|
npx meocord start --build --prod # production build + start
|
|
241
|
-
npx meocord g co slash "profile" # generate a slash controller
|
|
242
|
-
npx meocord g --help # list all generator sub-commands
|
|
243
284
|
```
|
|
244
285
|
|
|
245
|
-
###
|
|
286
|
+
### Generators
|
|
287
|
+
|
|
288
|
+
| Sub-command | Alias | Generates |
|
|
289
|
+
| ------------ | ----- | ----------------------------------- |
|
|
290
|
+
| `controller` | `co` | a controller, its spec, its builder |
|
|
291
|
+
| `service` | `s` | a service and its spec |
|
|
292
|
+
| `guard` | `gu` | a guard and its spec |
|
|
293
|
+
|
|
294
|
+
#### Controllers
|
|
246
295
|
|
|
247
296
|
```shell
|
|
248
297
|
npx meocord g co <type> <name>
|
|
@@ -825,7 +874,7 @@ import {
|
|
|
825
874
|
import { ChatInputCommandInteraction } from 'discord.js'
|
|
826
875
|
import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
|
|
827
876
|
import { GreetingService } from '@src/services/greeting.service.js'
|
|
828
|
-
import {
|
|
877
|
+
import { RateLimitGuard } from '@src/guards/rate-limit.guard.js'
|
|
829
878
|
|
|
830
879
|
describe('GreetingSlashController', () => {
|
|
831
880
|
let controller: GreetingSlashController
|
|
@@ -894,6 +943,62 @@ Start in production:
|
|
|
894
943
|
npx meocord start --prod
|
|
895
944
|
```
|
|
896
945
|
|
|
946
|
+
### Which runtime the bot runs on
|
|
947
|
+
|
|
948
|
+
`start` runs the bot on **the runtime you launched it with**. There is nothing to configure and no config key to set — if you typed `bun`, you get a bun process:
|
|
949
|
+
|
|
950
|
+
```shell
|
|
951
|
+
bun run start # dist/main.js runs under bun
|
|
952
|
+
npm run start # dist/main.js runs under node
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Two signals decide it, most explicit first: the runtime executing the CLI, and — when the CLI itself was handed to node — the runner that launched it. `bun run` honours the bin's `#!/usr/bin/env node` shebang, so bun sets `npm_execpath` to its own binary and that is what the bot is spawned with. npm, pnpm and yarn point it at a `.js` file instead, which cannot run the bundle, so those fall through to node as expected.
|
|
956
|
+
|
|
957
|
+
That matters for more than tidiness. Pinning `node` would oblige a bun-only image to install a second runtime purely to launch, or to carry `--bun` on every command. It also decides the allocator: for a bot doing heavy native work — canvas rendering through a napi module, say — glibc's malloc and bun's mimalloc produce very different resident-memory curves on the same workload, because they differ in how eagerly freed pages go back to the OS.
|
|
958
|
+
|
|
959
|
+
Development works the same way. The watcher runs the bundle through the same command production does, so a runtime that works in `--dev` cannot quietly differ from the one that ships.
|
|
960
|
+
|
|
961
|
+
#### Running the CLI itself on bun
|
|
962
|
+
|
|
963
|
+
The resolution above decides what the _bot_ runs on. The CLI process is decided earlier,
|
|
964
|
+
by the interpreter line `#!/usr/bin/env node`, which nothing in the package can influence
|
|
965
|
+
— it is read before any of the program exists. On a machine with no node at all,
|
|
966
|
+
invoking the CLI directly fails before it starts:
|
|
967
|
+
|
|
968
|
+
```
|
|
969
|
+
$ meocord start --prod
|
|
970
|
+
env: node: No such file or directory
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
That line stays as it is because Windows depends on it: npm there never runs the file
|
|
974
|
+
through its shebang, it parses the line and writes a `.cmd` invoking the program named in
|
|
975
|
+
it. `#!/usr/bin/env node` yields `node`; anything else yields a program Windows cannot
|
|
976
|
+
resolve.
|
|
977
|
+
|
|
978
|
+
So on a bun-only image, tell bun to ignore the line. Either per command:
|
|
979
|
+
|
|
980
|
+
```shell
|
|
981
|
+
bun --bun meocord start --prod
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
or once for the project, which is what a bun-only Dockerfile wants:
|
|
985
|
+
|
|
986
|
+
```toml
|
|
987
|
+
# bunfig.toml
|
|
988
|
+
[run]
|
|
989
|
+
bun = true
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
Then plain `bun run start` runs the CLI and the bot on bun, and node need not exist.
|
|
993
|
+
|
|
994
|
+
#### Pinning a specific binary
|
|
995
|
+
|
|
996
|
+
To override both signals — a particular install, or a different runtime for comparison — set `MEOCORD_RUNTIME`:
|
|
997
|
+
|
|
998
|
+
```shell
|
|
999
|
+
MEOCORD_RUNTIME=/usr/local/bin/bun npm run start
|
|
1000
|
+
```
|
|
1001
|
+
|
|
897
1002
|
---
|
|
898
1003
|
|
|
899
1004
|
## Contributing
|
|
@@ -902,9 +1007,27 @@ npx meocord start --prod
|
|
|
902
1007
|
2. Create a feature branch: `git checkout -b feat/your-feature`
|
|
903
1008
|
3. Commit with conventional commits: `git commit -m "feat: add X"`
|
|
904
1009
|
4. Run `bun run lint` and `bun run test` before pushing
|
|
905
|
-
5.
|
|
1010
|
+
5. If you touched anything under `src/bin/`, also run `bun run build && bun run verify:generated`
|
|
1011
|
+
6. Push and open a pull request against `main`
|
|
1012
|
+
|
|
1013
|
+
Include a description of what changed and why, and add tests for any new behaviour.
|
|
1014
|
+
|
|
1015
|
+
`verify:generated` generates one controller of every type through the built CLI — flat and nested, in separate throwaway projects — and typechecks the result against the published package. Rendering a template says nothing about whether the code it produces compiles, and two bugs lived behind exactly that gap. It runs in CI as part of the Build job, so you do not have to remember it; running it locally is just faster than waiting.
|
|
1016
|
+
|
|
1017
|
+
### Commit messages and releases
|
|
1018
|
+
|
|
1019
|
+
Commit messages drive versioning through [semantic-release](https://semantic-release.gitbook.io/). Only these publish:
|
|
1020
|
+
|
|
1021
|
+
| Prefix | Release |
|
|
1022
|
+
| ------------------------------ | ------- |
|
|
1023
|
+
| `feat:` | minor |
|
|
1024
|
+
| `fix:` | patch |
|
|
1025
|
+
| `perf:` | patch |
|
|
1026
|
+
| `BREAKING CHANGE:` in the body | major |
|
|
1027
|
+
|
|
1028
|
+
Everything else — `docs:`, `test:`, `ci:`, `chore:`, `refactor:`, `style:` — lands on `main` without publishing and ships with whatever releasable commit comes next.
|
|
906
1029
|
|
|
907
|
-
|
|
1030
|
+
Pick the prefix by what reaches the installed package, not by which file you edited. JSDoc is compiled into the published `.d.ts` and is what a user reads in their editor, so correcting a wrong `@example` is a `fix:` even though you only touched a comment. A README-only change is `docs:`.
|
|
908
1031
|
|
|
909
1032
|
---
|
|
910
1033
|
|
package/dist/cjs/core/index.cjs
CHANGED
|
@@ -26,23 +26,44 @@ const createErrorEmbed = (description)=>{
|
|
|
26
26
|
return embed;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/** What Discord assumes a command body is when it carries no type of its own. */ const DEFAULT_APPLICATION_COMMAND_TYPE = discord_js.ApplicationCommandType.ChatInput;
|
|
29
30
|
/**
|
|
30
|
-
* The
|
|
31
|
+
* The body a builder registers, which is what Discord sees.
|
|
31
32
|
*
|
|
32
|
-
* Read from the built payload rather than from the `@Command`
|
|
33
|
-
* free to
|
|
34
|
-
|
|
35
|
-
*/ function commandNameOf(builder) {
|
|
33
|
+
* Read from the built payload rather than from the `@Command` arguments: a builder is
|
|
34
|
+
* free to describe the command differently from the strings it was handed.
|
|
35
|
+
*/ function payloadOf(builder) {
|
|
36
36
|
try {
|
|
37
|
-
|
|
38
|
-
return typeof json?.name === 'string' ? json.name : undefined;
|
|
37
|
+
return typeof builder.toJSON === 'function' ? builder.toJSON() : builder;
|
|
39
38
|
} catch {
|
|
40
39
|
// A builder missing a required field throws from toJSON. Surfacing that is the
|
|
41
40
|
// registration call's job, where it is reported against the command Discord
|
|
42
41
|
// rejected -- deduplication should not be what turns it into a startup crash.
|
|
43
|
-
return
|
|
42
|
+
return {};
|
|
44
43
|
}
|
|
45
44
|
}
|
|
45
|
+
/** The name a builder registers under. */ function commandNameOf(builder) {
|
|
46
|
+
const { name } = payloadOf(builder);
|
|
47
|
+
return typeof name === 'string' ? name : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The application command type a builder registers as.
|
|
51
|
+
*
|
|
52
|
+
* A slash builder may leave the field out, so an absent type is read as the chat input
|
|
53
|
+
* one Discord would infer — which keeps untyped slash builders colliding with each
|
|
54
|
+
* other rather than each claiming a key of its own.
|
|
55
|
+
*/ function commandTypeOf(builder) {
|
|
56
|
+
const { type } = payloadOf(builder);
|
|
57
|
+
return typeof type === 'number' ? type : DEFAULT_APPLICATION_COMMAND_TYPE;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The identity Discord gives a command.
|
|
61
|
+
*
|
|
62
|
+
* The numeric type leads, so the two halves can never be read apart wrongly: everything
|
|
63
|
+
* before the first separator is the type, everything after it is the name.
|
|
64
|
+
*/ function registrationKey(builder, fallbackName) {
|
|
65
|
+
return `${commandTypeOf(builder)}:${commandNameOf(builder) ?? fallbackName}`;
|
|
66
|
+
}
|
|
46
67
|
class MeoCordApp {
|
|
47
68
|
/**
|
|
48
69
|
* Runs an event handler so a failure inside it cannot take the process down.
|
|
@@ -109,11 +130,13 @@ class MeoCordApp {
|
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
async registerCommands() {
|
|
112
|
-
// Keyed by
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
133
|
+
// Keyed by type and name together, because that pair is what Discord treats as one
|
|
134
|
+
// command: a user context menu and a message context menu are free to share a name,
|
|
135
|
+
// and keying on the name alone would drop one of them from the payload. Within a
|
|
136
|
+
// single type the name is unique, so a command whose subcommands live in separate
|
|
137
|
+
// methods still contributes its builder once — sending it twice makes Discord
|
|
138
|
+
// reject the whole payload.
|
|
139
|
+
const buildersByCommand = new Map();
|
|
117
140
|
for (const controllerClass of this.controllerClasses){
|
|
118
141
|
const instance = this.getInstance(controllerClass);
|
|
119
142
|
const commandMap = controller_decorator.getCommandMap(instance);
|
|
@@ -122,18 +145,18 @@ class MeoCordApp {
|
|
|
122
145
|
if (!Array.isArray(commandMetadataArray)) continue;
|
|
123
146
|
for (const { builder, type } of commandMetadataArray){
|
|
124
147
|
if (!(type in enum_index.CommandType) || !builder) continue;
|
|
125
|
-
const
|
|
126
|
-
const existing =
|
|
148
|
+
const key = registrationKey(builder, commandName);
|
|
149
|
+
const existing = buildersByCommand.get(key);
|
|
127
150
|
if (existing === undefined) {
|
|
128
|
-
|
|
151
|
+
buildersByCommand.set(key, builder);
|
|
129
152
|
} else if (existing !== builder) {
|
|
130
|
-
this.logger.warn(`Command "${
|
|
153
|
+
this.logger.warn(`Command "${commandNameOf(builder) ?? commandName}" is built more than once for the same ` + `application command type; only the first builder is registered. Two builders of one type ` + `cannot both own a name, so declare the builder on a single @Command and give the others ` + `the plain CommandType.`);
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
156
|
}
|
|
134
157
|
}
|
|
135
158
|
const builders = [
|
|
136
|
-
...
|
|
159
|
+
...buildersByCommand.values()
|
|
137
160
|
];
|
|
138
161
|
try {
|
|
139
162
|
if (this.bot.application) {
|
|
@@ -154,7 +154,7 @@ export class ButtonInteractionGuard implements GuardInterface {
|
|
|
154
154
|
* @example
|
|
155
155
|
* ```typescript
|
|
156
156
|
* // Method-level usage
|
|
157
|
-
* @Command('profile
|
|
157
|
+
* @Command('profile/{id}', CommandType.BUTTON)
|
|
158
158
|
* @UseGuard(
|
|
159
159
|
* { provide: RateLimiterGuard, params: { limit: 2, window: 3000 } },
|
|
160
160
|
* ButtonInteractionGuard
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# {{displayName}}
|
|
2
|
+
|
|
3
|
+
A Discord bot built with [MeoCord](https://github.com/l7aromeo/meocord).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Copy the example environment file and add your bot token:
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
cp .env.example .env
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`.env` is gitignored. Never commit a real token — if one is pushed, reset it in the
|
|
14
|
+
[Discord developer portal](https://discord.com/developers/applications) rather than
|
|
15
|
+
rewriting history.
|
|
16
|
+
|
|
17
|
+
## Running
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
{{packageManager}} run start:dev # watch mode, rebuilds and restarts on change
|
|
21
|
+
{{packageManager}} run start:prod # production
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The bot runs on whichever runtime you launch it with — launch with bun and it is a bun
|
|
25
|
+
process, launch with node and it is a node process.
|
|
26
|
+
|
|
27
|
+
## Building
|
|
28
|
+
|
|
29
|
+
```shell
|
|
30
|
+
{{packageManager}} run build:dev
|
|
31
|
+
{{packageManager}} run build:prod
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Generating components
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
npx meocord g co slash "profile" # a slash controller, its spec and its builder
|
|
38
|
+
npx meocord g s "profile" # a service
|
|
39
|
+
npx meocord g gu "rate-limit" # a guard
|
|
40
|
+
npx meocord g --help # every generator
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Register whatever you generate in the `controllers` array in `src/app.ts` — controllers
|
|
44
|
+
are wired up by that list, not by where they sit on disk.
|
|
45
|
+
|
|
46
|
+
## Testing
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
{{packageManager}} run test
|
|
50
|
+
{{packageManager}} run test:coverage
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`meocord/testing` provides `MeoCordTestingModule` for building a controller with its
|
|
54
|
+
dependencies, plus `createMockInteraction` and friends for driving it without a Discord
|
|
55
|
+
connection.
|
|
56
|
+
|
|
57
|
+
## Linting
|
|
58
|
+
|
|
59
|
+
```shell
|
|
60
|
+
{{packageManager}} run lint
|
|
61
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
dist
|
|
2
|
+
coverage
|
|
3
|
+
|
|
4
|
+
/node_modules
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
yarn-debug.log*
|
|
7
|
+
yarn-error.log*
|
|
8
|
+
|
|
9
|
+
.pnp.*
|
|
10
|
+
.yarn/*
|
|
11
|
+
!.yarn/patches
|
|
12
|
+
!.yarn/plugins
|
|
13
|
+
!.yarn/releases
|
|
14
|
+
!.yarn/sdks
|
|
15
|
+
!.yarn/versions
|
|
16
|
+
|
|
17
|
+
.idea
|
|
18
|
+
.vscode/*
|
|
19
|
+
|
|
20
|
+
.env*
|
|
21
|
+
!.env.example
|
|
22
|
+
.DS_Store
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import unusedImports from 'eslint-plugin-unused-imports'
|
|
2
|
+
import meocordESLint, { typescriptConfig } from 'meocord/eslint'
|
|
3
|
+
|
|
4
|
+
const customConfig = {
|
|
5
|
+
...typescriptConfig,
|
|
6
|
+
plugins: {
|
|
7
|
+
...typescriptConfig.plugins,
|
|
8
|
+
'unused-imports': unusedImports,
|
|
9
|
+
},
|
|
10
|
+
rules: {
|
|
11
|
+
...typescriptConfig.rules,
|
|
12
|
+
'unused-imports/no-unused-imports': 'error',
|
|
13
|
+
'unused-imports/no-unused-vars': [
|
|
14
|
+
'error',
|
|
15
|
+
{
|
|
16
|
+
vars: 'all',
|
|
17
|
+
varsIgnorePattern: '^_',
|
|
18
|
+
args: 'after-used',
|
|
19
|
+
argsIgnorePattern: '^_',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default [...meocordESLint, customConfig]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
import { type MeoCordConfig } from 'meocord/interface'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
appName: '{{displayName}}',
|
|
6
|
+
// Read from the environment rather than written here: this file is committed, and a
|
|
7
|
+
// token pasted into it is a token pushed to the remote. Copy .env.example to .env.
|
|
8
|
+
discordToken: process.env.DISCORD_TOKEN!,
|
|
9
|
+
webpack: config => {
|
|
10
|
+
config.module.rules?.push({
|
|
11
|
+
test: /\.(md|html)$/i,
|
|
12
|
+
type: 'asset/source',
|
|
13
|
+
})
|
|
14
|
+
config.module.rules?.push({
|
|
15
|
+
test: /\.(gif|jpg|jpeg|png|svg|woff|woff2|eot|ttf|otf)$/i,
|
|
16
|
+
type: 'asset/resource',
|
|
17
|
+
exclude: /node_modules/,
|
|
18
|
+
generator: {
|
|
19
|
+
filename: 'assets/[name][ext]',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return config
|
|
24
|
+
},
|
|
25
|
+
} satisfies MeoCordConfig
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{appName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build:dev": "{{runtimePrefix}}meocord build --dev",
|
|
9
|
+
"build:prod": "{{runtimePrefix}}meocord build --prod",
|
|
10
|
+
"start:dev": "{{runtimePrefix}}meocord start --dev",
|
|
11
|
+
"start:prod": "{{runtimePrefix}}meocord start --prod",
|
|
12
|
+
"lint": "eslint --fix && tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"discord.js": "^14.27.0",
|
|
19
|
+
"dotenv": "^17.2.4",
|
|
20
|
+
"meocord": "^{{version}}"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^10.0.1",
|
|
24
|
+
"@typescript-eslint/parser": "^8.58.1",
|
|
25
|
+
"@vitest/coverage-istanbul": "^4.1.10",
|
|
26
|
+
"eslint": "^10.2.0",
|
|
27
|
+
"eslint-config-prettier": "^10.1.8",
|
|
28
|
+
"eslint-plugin-import-x": "^4.16.2",
|
|
29
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
30
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
31
|
+
"globals": "^17.5.0",
|
|
32
|
+
"prettier": "^3.8.2",
|
|
33
|
+
"typescript": "^6.0.2",
|
|
34
|
+
"typescript-eslint": "^8.58.1",
|
|
35
|
+
"unplugin-swc": "^1.5.7",
|
|
36
|
+
"vitest": "^4.1.10"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { GatewayIntentBits, Partials, ActivityType } from 'discord.js'
|
|
2
|
+
import { MeoCord } from 'meocord/decorator'
|
|
3
|
+
import { SampleSlashController } from '@src/controllers/slash/sample.slash.controller'
|
|
4
|
+
import { SampleSelectMenuController } from '@src/controllers/select-menu/sample.select-menu.controller'
|
|
5
|
+
import { SampleButtonController } from '@src/controllers/button/sample.button.controller'
|
|
6
|
+
import { SampleMessageController } from '@src/controllers/message/sample.message.controller'
|
|
7
|
+
import { SampleReactionController } from '@src/controllers/reaction/sample.reaction.controller'
|
|
8
|
+
import { SampleContextMenuController } from '@src/controllers/context-menu/sample.context-menu.controller'
|
|
9
|
+
import { SampleModalSubmitController } from '@src/controllers/modal-submit/sample.modal-submit.controller'
|
|
10
|
+
|
|
11
|
+
@MeoCord({
|
|
12
|
+
controllers: [
|
|
13
|
+
// Slash Commands
|
|
14
|
+
SampleSlashController,
|
|
15
|
+
// Select Menu
|
|
16
|
+
SampleSelectMenuController,
|
|
17
|
+
// Buttons
|
|
18
|
+
SampleButtonController,
|
|
19
|
+
// Message
|
|
20
|
+
SampleMessageController,
|
|
21
|
+
// Reactions
|
|
22
|
+
SampleReactionController,
|
|
23
|
+
// Context Menu
|
|
24
|
+
SampleContextMenuController,
|
|
25
|
+
// Modal Submit
|
|
26
|
+
SampleModalSubmitController,
|
|
27
|
+
],
|
|
28
|
+
clientOptions: {
|
|
29
|
+
intents: [
|
|
30
|
+
GatewayIntentBits.Guilds,
|
|
31
|
+
GatewayIntentBits.GuildMessages,
|
|
32
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
33
|
+
GatewayIntentBits.MessageContent,
|
|
34
|
+
],
|
|
35
|
+
partials: [Partials.Message, Partials.Reaction],
|
|
36
|
+
},
|
|
37
|
+
activities: [
|
|
38
|
+
{
|
|
39
|
+
name: ``,
|
|
40
|
+
type: ActivityType.Custom,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
export default class App {}
|
package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MeoCordTestingModule } from 'meocord/testing'
|
|
2
|
+
import { SampleButtonController } from '@src/controllers/button/sample.button.controller.js'
|
|
3
|
+
|
|
4
|
+
describe('SampleButtonController', () => {
|
|
5
|
+
let controller: SampleButtonController
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const module = MeoCordTestingModule.create({
|
|
9
|
+
controllers: [SampleButtonController],
|
|
10
|
+
}).compile()
|
|
11
|
+
controller = module.get(SampleButtonController)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should be defined', () => {
|
|
15
|
+
expect(controller).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ButtonInteraction } from 'discord.js'
|
|
2
|
+
import { Controller, Command, UseGuard } from 'meocord/decorator'
|
|
3
|
+
import { CommandType } from 'meocord/enum'
|
|
4
|
+
import { RateLimitGuard } from '@src/guards/rate-limit.guard'
|
|
5
|
+
|
|
6
|
+
@Controller()
|
|
7
|
+
export class SampleButtonController {
|
|
8
|
+
@Command('button-click', CommandType.BUTTON)
|
|
9
|
+
@UseGuard(RateLimitGuard)
|
|
10
|
+
async handleButton(interaction: ButtonInteraction) {
|
|
11
|
+
await interaction.reply('Button clicked!')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Command('button-with/{id}', CommandType.BUTTON)
|
|
15
|
+
@UseGuard({ provide: RateLimitGuard, params: { limit: 2, windowInSeconds: 60 } })
|
|
16
|
+
async handleButtonWithId(interaction: ButtonInteraction, { id }: { id: string }) {
|
|
17
|
+
await interaction.reply(`Button with id: ${id} clicked!`)
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ContextMenuCommandBuilder, ApplicationCommandType } from 'discord.js'
|
|
2
|
+
import { CommandBuilder } from 'meocord/decorator'
|
|
3
|
+
import { CommandType } from 'meocord/enum'
|
|
4
|
+
|
|
5
|
+
@CommandBuilder(CommandType.CONTEXT_MENU)
|
|
6
|
+
export class SampleCommandBuilder {
|
|
7
|
+
build() {
|
|
8
|
+
return new ContextMenuCommandBuilder().setName('sample-context-menu').setType(ApplicationCommandType.User)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MeoCordTestingModule } from 'meocord/testing'
|
|
2
|
+
import { SampleContextMenuController } from '@src/controllers/context-menu/sample.context-menu.controller.js'
|
|
3
|
+
|
|
4
|
+
describe('SampleContextMenuController', () => {
|
|
5
|
+
let controller: SampleContextMenuController
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const module = MeoCordTestingModule.create({
|
|
9
|
+
controllers: [SampleContextMenuController],
|
|
10
|
+
}).compile()
|
|
11
|
+
controller = module.get(SampleContextMenuController)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should be defined', () => {
|
|
15
|
+
expect(controller).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction } from 'discord.js'
|
|
2
|
+
import { Controller, Command, UseGuard } from 'meocord/decorator'
|
|
3
|
+
import { SampleCommandBuilder } from '@src/controllers/context-menu/builders/sample.builder'
|
|
4
|
+
import { RateLimitGuard } from '@src/guards/rate-limit.guard'
|
|
5
|
+
|
|
6
|
+
@Controller()
|
|
7
|
+
export class SampleContextMenuController {
|
|
8
|
+
@Command('sample-context-menu', SampleCommandBuilder)
|
|
9
|
+
@UseGuard(RateLimitGuard)
|
|
10
|
+
async handleContextMenu(interaction: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction) {
|
|
11
|
+
await interaction.reply('This is sample reply of context menu command.')
|
|
12
|
+
}
|
|
13
|
+
}
|