Compare commits

...

161 Commits

Author SHA1 Message Date
Jacob Nguyen
d1680e51fc change task signature 2024-07-18 16:47:13 -05:00
jacob
c35337c3cf fix swap not accepting functions 2024-07-16 23:09:24 -05:00
Jacob Nguyen
5d0260ab4e documentation 2024-07-07 18:21:16 -05:00
Jacob Nguyen
2014e0ea8e refactor n better signatures for task scheduler 2024-07-06 14:12:03 -05:00
Jacob Nguyen
482105712b refactor to not type leak and job cancellation 2024-07-05 17:18:26 -05:00
Jacob Nguyen
870972000d more descriptive errors 2024-07-05 15:48:55 -05:00
Jacob Nguyen
210aa41c7e scheduler ids 2024-07-05 14:46:31 -05:00
Jacob Nguyen
c252854954 revising cron modules and better error messages 2024-07-04 19:18:52 -05:00
Jacob Nguyen
92ca9eb859 namespace presence types again 2024-07-03 22:25:24 -05:00
Jacob Nguyen
e0f631a8e2 temp fix 2024-06-30 17:39:00 -05:00
Jacob Nguyen
3128b441c5 better documentation 2024-06-30 17:39:00 -05:00
Jacob Nguyen
14e80016da Update src/core/functions.ts
Co-authored-by: Evo <85353424+EvolutionX-10@users.noreply.github.com>
2024-06-30 17:32:08 -05:00
jacob
908d584cc5 fix 2024-06-24 23:18:19 -05:00
Jacob Nguyen
3e9b9229c8 Merge branch 'main' into feat/v4 2024-06-22 14:20:29 -05:00
Jacob Nguyen
1d8dbb8962 fix up tests and cleanup 2024-06-22 12:10:52 -05:00
Jacob Nguyen
7c8e39defb fix tests 2024-06-14 20:31:05 -05:00
Jacob Nguyen
90f5ea7bda update locals api, docs, tests 2024-06-14 20:27:23 -05:00
Jacob Nguyen
2f778f4dc2 s 2024-06-13 13:28:15 -05:00
Jacob Nguyen
9c358e1928 nvm, now i did 2024-06-13 13:28:03 -05:00
Jacob Nguyen
c764de12ac finish ioc transition 2024-06-13 12:26:37 -05:00
Jacob Nguyen
19abb7cb22 ss 2024-06-12 23:09:28 -05:00
Jacob Nguyen
30feb790b1 remove asset 2024-06-12 23:09:13 -05:00
Jacob Nguyen
222ecd9b61 documentation, add json for Asset 2024-06-11 00:27:36 -05:00
Jacob Nguyen
67bb4d4b9f fix: ioc deps not created correctly 2024-06-10 00:22:10 -05:00
Jacob Nguyen
bf071b7af4 -file,-updateModule,publish? 2024-06-08 00:20:24 -05:00
Jacob Nguyen
45665292ae add init hooks not firing 2024-06-04 00:48:30 -05:00
Jacob Nguyen
2120b18c4e more intuitive context.options and Asset typings 2024-06-02 23:43:55 -05:00
Jacob Nguyen
898fdf52a3 assets fn complete 2024-05-31 00:48:51 -05:00
Jacob Nguyen
c8230334f2 fix typings 2024-05-28 13:43:56 -05:00
Jacob Nguyen
792015a64e readd vitest and Asset fn 2024-05-27 17:01:22 -05:00
jacob
fb418c0675 refact,simplf 2024-05-25 16:22:25 -05:00
jacob
12a8f0c5d7 once on eventModules 2024-05-25 12:39:24 -05:00
Jacob Nguyen
af0f909c44 consolidate fmt 2024-05-23 22:00:12 -05:00
jacob
76d9db7b98 inlineinignsd 2024-05-23 19:36:08 -05:00
Jacob Nguyen
86dd0cd842 testing n shi 2024-05-23 01:02:02 -05:00
Jacob Nguyen
1de21b8b37 handling customId params working 2024-05-22 23:33:42 -05:00
Jacob Nguyen
327e56fc1c dynamic customIds 2024-05-22 00:44:59 -05:00
Jacob Nguyen
814fc4f01d proper mapping 2024-05-21 01:41:34 -05:00
Jacob Nguyen
7f4004e043 parsingParams kinda 2024-05-21 01:18:20 -05:00
Jacob Nguyen
88598b0948 fix initPlugins not reassigning 2024-05-21 00:35:19 -05:00
Jacob Nguyen
e700297bfc fix faiklling test 2024-05-20 19:36:36 -05:00
Jacob Nguyen
e0f6a4cd16 initplugins inject deps, inconspicuos 2024-05-20 12:21:18 -05:00
Jacob Nguyen
15511a4868 porg 2024-05-20 00:49:51 -05:00
Jacob Nguyen
735a9e3816 plugin system loking better, tbd type 2024-05-19 22:50:21 -05:00
Jacob Nguyen
0beeb4c064 add deps to plugin calls and execute 2024-05-19 22:33:57 -05:00
Jacob Nguyen
699adf276c simplify plugin args and prepare for reduction among plugins 2024-05-17 23:13:12 -05:00
Jacob Nguyen
960f90c544 freeze module after plugins, updateModule, and more 2024-05-17 20:16:16 -05:00
Jacob Nguyen
6717672722 plugin data reduction & args changes 2024-05-17 15:07:32 -05:00
Jacob Nguyen
ca9b84ba21 save b4 thunder and lightning 2024-05-16 23:41:44 -05:00
Jacob Nguyen
d3227e5ec1 updating ioc api, experimenting with cron 2024-05-16 17:03:04 -05:00
Jacob Nguyen
44c072f401 ksdjkldsfld 2024-05-16 00:21:29 -05:00
Jacob Nguyen
203e8c8ecf cron works now, poc 2024-05-15 16:52:33 -05:00
Jacob Nguyen
d905f08993 i think cron works 2024-05-15 16:42:21 -05:00
Jacob Nguyen
0d82658fc5 up to speed with event modules 2024-05-15 14:59:24 -05:00
Jacob Nguyen
16a84e85d1 add more tests, polish up ioc 2024-05-15 01:44:23 -05:00
Jacob Nguyen
ec45f80be6 refactor deps list 2024-05-15 00:29:59 -05:00
Jacob Nguyen
eb8ba6799b fdssdf 2024-05-14 23:29:20 -05:00
Jacob Nguyen
a7aea4be1a ready handler revamped so much cleaner 2024-05-14 23:19:57 -05:00
Duro
04c4625bfa docs: change @param wrapper -> @param maybeWrapper for Sern.init (#360)
change `@param wrapper` -> `@param maybeWrapper` for typedoc fix
2024-05-14 21:52:24 -05:00
Jacob Nguyen
880311f08c refactor, add cron types, reinstante module loader 2024-05-14 12:01:18 -05:00
Jacob Nguyen
0a05cbba3f dsfsd 2024-05-13 16:06:02 -05:00
Jacob Nguyen
6e2f4b616f refactor and clean up and reenter v3 module loading 2024-05-13 15:27:12 -05:00
Jacob Nguyen
8554eeaef4 working on bun compat 2024-05-10 01:18:44 -05:00
Jacob Nguyen
4d74e63dd3 fix absPath gen 2024-05-10 00:23:22 -05:00
jacob
ffb2872f8b ensure container is init'd 2024-05-09 20:42:09 -05:00
Jacob Nguyen
08ef80522f fdsfD 2024-05-04 12:24:22 -05:00
Jacob Nguyen
f762033504 removeiti 2024-05-03 18:50:47 -05:00
Jacob Nguyen
cd92b54839 for now copy paste new ioc system 2024-05-03 18:13:34 -05:00
Jacob Nguyen
b0e9d15fa7 generify partition 2024-05-03 17:35:25 -05:00
jacob
545105e45b some refactor 2024-05-03 16:14:55 -05:00
jacob
d7ebdb2edc cleanup tests, codegen, and importing handler 2024-05-02 17:27:57 -05:00
Jacob Nguyen
d3245c8a0c prototyping linking static handler 2024-05-02 01:22:40 -05:00
jacob
219eda9bf7 more refactoring 2024-05-01 18:24:56 -05:00
jacob
0488f45677 refactor allat 2024-05-01 16:46:12 -05:00
jacob
e8cfcc2525 reffactor 2024-04-30 20:31:06 -05:00
jacob
d6eedb1b4d remove barrel for core/structs 2024-04-30 20:23:17 -05:00
jacob
5b33a9d1bb still broken but progress 2024-04-30 15:03:19 -05:00
Jacob Nguyen
52d1b5a37a fix circular dependency 2024-04-29 00:48:37 -05:00
Jacob Nguyen
6c61ae7d6a fix test 2024-04-29 00:17:58 -05:00
Jacob Nguyen
87c5631e57 clean up dependencies 2024-04-29 00:13:16 -05:00
Jacob Nguyen
821de65b86 internal namespace 2024-04-29 00:08:57 -05:00
Jacob Nguyen
e6fba9d8b5 Presence namespaced types removed 2024-04-28 23:37:58 -05:00
Jacob Nguyen
071d5eac49 the end of sern init?? 2024-04-28 22:16:54 -05:00
Jacob Nguyen
68c5f09b46 fix text compile 2024-04-28 21:20:36 -05:00
Jacob Nguyen
6105f7b4ef TEAR IT UP 2024-04-28 20:50:49 -05:00
Jacob Nguyen
9faae7eca7 consolidate default services in single file 2024-04-28 20:36:50 -05:00
Jacob Nguyen
f5136ba1ca consolidate interfaces in single file 2024-04-28 20:26:53 -05:00
Jacob Nguyen
76ee9c6edf Remove module store, manager, and Intializable type 2024-04-28 19:34:08 -05:00
Jacob Nguyen
3d10ee1c59 tearing it up 2024-04-28 18:44:58 -05:00
Jacob Nguyen
599a02c9df command modules do not depend on anything but itself 2024-04-28 15:50:29 -05:00
Jacob Nguyen
6228f53244 Refactorings 2024-04-28 14:14:51 -05:00
jacob
30230d49c3 step 1 2024-04-28 13:48:50 -05:00
Jacob Nguyen
91b3768e37 bump tsresults 2024-03-21 10:46:19 -05:00
renovate[bot]
d6f49d1d97 chore(deps): update actions/setup-node digest to 1a4442c (#354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2024-03-21 10:35:43 -05:00
renovate[bot]
8ecd30cf18 chore(deps): update actions/checkout digest to f43a0e5 (#353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2024-03-19 10:22:39 -05:00
github-actions[bot]
a19edaf883 chore(main): release 3.3.4 (#359)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-18 01:57:13 -05:00
Jacob Nguyen
90e55dfa14 fix: sern emitter err (#358)
* prep for fix

* fix ? (not tested

* fix error event not emitting payload
2024-03-18 01:47:14 -05:00
github-actions[bot]
2106522812 chore(main): release 3.3.3 (#357)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-24 18:35:24 -06:00
Jacob Nguyen
ce8c4bf649 fix: typings and cleanup (#356)
* fix typings and cleanup

* import type

* rm unused import
2024-02-18 09:34:53 -06:00
jacob
e89b918390 dfs 2024-02-17 11:40:19 -06:00
jacob
f8b69ae542 stuff 2024-02-17 11:38:37 -06:00
Jacob Nguyen
48f9f6ec16 fix: rm deprecated class modules, clean up, rm indirection (#355)
* refactor: rm deprecations, clean up, rm indirection

* fix: singleton init not being fired when inserting function

* refactor and generic internal add

* deprecate a few things that i impusively added lol
2024-02-17 11:35:53 -06:00
jacob
86ebb221ed deprecate a few things that i impusively added lol 2024-02-15 12:36:00 -06:00
jacob
4efdbb21fb refactor and generic internal add 2024-02-15 12:26:34 -06:00
jacob
07b11b357b fix: singleton init not being fired when inserting function 2024-02-14 15:58:50 -06:00
jacob
ac7f47c590 refactor: rm deprecations, clean up, rm indirection 2024-02-12 21:47:35 -06:00
45cbda7b42 refactor: cleanup (#348)
* some wip code

Co-authored-by: Jacob Nguyen <jacoobes@users.noreply.github.com>

* general idea

* style

* making shrimple truly optional

* got optional localizer working

* proposing api notation?

* prepare for localization map

* add localsFor

* merge some internals

* boss call

* add test for init functionality

* add documentation

* inline and cleanup

* feat: logging for experimental json loading

* loosen typings

* dev workflow and cleaning up comments

* cleaning up a bit more

* rename Localizer -> Localization

* more documentation, change dir for default localizer

* some tests

* "

* move stuff, refactor, deprecate

* yarnb

* Update index.ts

---------

Co-authored-by: Jacob Nguyen <jacoobes@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
Co-authored-by: jacob <jacoobes@sern.dev>
2024-02-09 17:46:16 -06:00
Jacob Nguyen
5cad432589 Update README.md 2024-01-08 14:31:49 -06:00
Jacob Nguyen
044a10dace Update README.md 2024-01-08 14:29:49 -06:00
github-actions[bot]
9d5c6c714f chore(main): release 3.3.2 (#352)
* chore(main): release 3.3.2

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2024-01-08 12:16:52 -06:00
Jacob Nguyen
4f2387119a fix: presence feature not workign on cjs applications (#351)
* start fix

* fix on unix

* better solution
2024-01-08 12:11:27 -06:00
Jacob Nguyen
a6fa4e3dcb fix docs build 2024-01-07 15:38:22 -06:00
github-actions[bot]
c281832db2 chore(main): release 3.3.1 (#350)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-07 15:27:43 -06:00
Jacob Nguyen
a359f73fa2 fix: crashing when slash command is used as text command (#349)
* progress on fix

* fix: ids
2024-01-07 15:26:08 -06:00
655bb8d358 revert: the last commit 2024-01-05 20:47:25 +01:00
e8d5029834 chore: update fortnite file 2024-01-05 20:46:38 +01:00
Jacob Nguyen
b0399f9507 refactor: minor (#347)
* some refactoring

* accidental merge

* refactor: ensure all asserts have error message to avoid cryptic messages

* general refactoring

* move controller to create-plugin
2024-01-02 13:04:59 -06:00
renovate[bot]
b962dae36c chore(deps): update actions/setup-node digest to 1a4442c (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 11:40:18 -06:00
github-actions[bot]
c73cf96cb2 chore(main): release 3.3.0 (#346)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-27 11:13:11 -06:00
Jacob Nguyen
7458befe8a feat: presence (#345)
* presence

* from event presence and refactoring

* refine presence api

* add tests and more comments

* sss

---------

Co-authored-by: SrIzan10 <66965250+SrIzan10@users.noreply.github.com>
2023-12-27 11:11:32 -06:00
Jacob Nguyen
efe49391e8 Update README.md 2023-12-27 01:51:41 -06:00
Jacob Nguyen
3140f80c10 Update README.md 2023-12-27 01:46:55 -06:00
github-actions[bot]
504cdee7b2 chore(main): release 3.2.1 (#344)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-21 12:49:42 -06:00
Jacob Nguyen
c7661f272c chore: bump version 2023-12-21 12:47:24 -06:00
Jacob Nguyen
daac37c288 fix: logger swap failing 2023-12-21 12:47:02 -06:00
ysf
a579e272d0 revolutionary (#342) 2023-12-15 17:03:23 -06:00
github-actions[bot]
2051aa1ac0 chore(main): release 3.2.0 (#341)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-15 16:23:39 -06:00
Jacob Nguyen
237c8537c6 chore: release 3.2.0
Release-As: 3.2.0
2023-12-15 16:19:38 -06:00
Jacob Nguyen
77fb00d386 feat/abstractiti (#340)
* progress on better error handling

* wiring onError callback through module loader and resolver

* fix error callbacks not being stored

* update onError to be record

* type alias

* wiring

* seems to work

* update error handling contract and wire more

* add command error builder

* fix merge

* progress on error handling

* naive onError handling, not tested

* progres

* proress

* progress on abstracting away iti

* seems to work

* fix tests

* better typings

* add doc

* abstracting iti

* remove onerror for this pr

* feat: better way to add dependencies

* fix tests
2023-12-15 16:09:13 -06:00
renovate[bot]
89f6bbb975 chore(deps): update google-github-actions/release-please-action digest to db8f2c6 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-21 10:18:07 -06:00
github-actions[bot]
8ef4ee87e9 chore(main): release 3.1.1 (#338)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-05 21:25:18 -06:00
Neo
fd39858636 fix: queuing events (#332) @Benzo-Fury (#333)
fix: queuing events

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-11-05 21:23:27 -06:00
Jacob Nguyen
132b625070 refactor: rm redudant fns and formatting 2023-11-04 16:57:13 -05:00
renovate[bot]
03439fec43 chore(deps): update google-github-actions/release-please-action digest to 4c5670f (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-21 22:30:40 -05:00
Jacob Nguyen
fc87e99ed0 Update README.md 2023-09-09 01:08:16 -05:00
renovate[bot]
a08541a8e7 chore(deps): update actions/checkout digest to f43a0e5 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 17:06:17 -05:00
renovate[bot]
8bd5eb4949 chore(deps): lock file maintenance (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 17:05:33 -05:00
github-actions[bot]
e1059f93f7 chore(main): release 3.1.0 (#330)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-04 15:43:47 -05:00
renovate[bot]
800531453f chore(deps): pin dependencies (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-22 11:37:22 -05:00
Jacob Nguyen
c9f2d75665 deprecate: mode (#325)
* test: add tests for context

* deprecate: mode

* revert docs for deprecated option
2023-08-19 07:07:47 +05:30
Jacob Nguyen
e59e0b9d40 test: add tests for context (#324) 2023-08-18 10:46:46 -05:00
Jacob Nguyen
26ccd118ff feat: dispose hooks (deprecate useContainerRaw) (#323)
* feat: dispose hooks

* build: unminify, add source map, deprecate useContainerRaw

* fix regression of context and fix tsup build
2023-08-17 12:51:24 -05:00
Jacob Nguyen
4b97d86908 chore: upgrade ts-results-es (#322) 2023-08-13 10:55:39 -05:00
xxDeveloper
b1c82448bd chore: Create FUNDING.yml (#321)
* chore: Create FUNDING.yml

* Rename FUNDING.yml to .github/FUNDING.yml
2023-08-08 10:29:40 +03:00
Jacob Nguyen
d80081384a Update README.md 2023-08-07 17:23:58 -05:00
mina
50253ca322 feat: add guaranteed channelId and userId getters to Context (#320) 2023-08-06 15:28:38 -05:00
github-actions[bot]
215aca2f46 chore(main): release 3.0.2 (#319)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-06 10:45:17 -05:00
Jacob Nguyen
a7f5ea269f fix: invalid id for cts, mts, cjs, mjs files, node paths (#318)
* better error messages

* fix: invalid id for cts, mts, cjs, mjs files
2023-08-06 10:43:34 -05:00
Jacob Nguyen
52d6368440 Delete codeql-analysis.yml 2023-08-06 00:36:50 -05:00
Jacob Nguyen
1e723a4154 Update npm-publish.yml 2023-08-06 00:34:43 -05:00
Jacob Nguyen
5fe13f43d2 better npm-publish.yml 2023-08-06 00:33:40 -05:00
Jacob Nguyen
ab9d39306a Create test.yml (#317)
* Create test.yml

* Update test.yml

* Update test.yml
2023-08-06 00:29:43 -05:00
github-actions[bot]
d429f3adbf chore(main): release 3.0.1 (#316)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-04 19:05:48 -05:00
Jacob Nguyen
5e011b471e remove warning 2023-08-04 19:01:27 -05:00
Jacob Nguyen
41344608c6 fix: collectors 2023-08-04 19:00:50 -05:00
github-actions[bot]
7a72cc4fe3 chore(main): release 3.0.0 (#315)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-29 17:13:42 -05:00
Jacob Nguyen
70cca0dbb0 chore: release 3.0.0
Release-As: 3.0.0
2023-07-29 17:11:51 -05:00
Jacob Nguyen
7798e36458 feat!: v3 (#294)
* refactor: move things to core, imports not fixed yet

* work on strategy and lifted Context

* remove id from lifted Context

* refactor: remove dependence on discord.js for module stoore

* moving and fixing imports

* chore: move operators into core

* chore: fix paths

* add wrapper platform field

* add deprecation warning

* chore:update paths

* chore:remove const function

* chore: remove deprecated symbols

* docs: add documentation to internal function

* chore: remove deprecated support for plugins

* chore: remove dependence on discord.js Awaitable type

* chore: update typings

* lift requiredDependencyKeys out of makeFetcher

* move strategy to index.ts and add adapters

* chore: fix typings

* chore: move command args matrix as binding

* feat: make Context platform specific, CoreContext as Core

* chore: remove extra file

* chore: move prettier into package.json

* chore(core): update imports and operators

* chore(core): add DefaultWrapper as sern classic

* move eslint and prettier configs to json

* chore: remove utils folder in favor of single file

* chore: remove redundant directories for single files

* chore: remove redundant directories for single files

* refactor: move and update things

* chore: move commands into seperate file

* chore: serverless work

* chore: remove redundant directories for single files

* chore: rename, wip refactoring

* chore: redundant directory

* refactor: internalize operators

* feat!: new module resolution algorithm

* chore: refactor and move things

* chore: refactor and add multiplatform typings

* chore: remove leaky import

* chore: add agnostic predicates

* chore: add old context here until i figure out what to do

* chore: update Proccessed typing to ./core

* chore: add tweetnacl

* revert: multiplatform

* revert: multiplatform

* chore: modularize and split typings

* chore: revert multiplatform

* chore: revert multi and mov sernEmitter

* chore: revert multi and clean up code

* refactor: add createGenericHandler

* refactor: remove unneeded signatures and fix imports

* feat: add getPublishableCommands to ModuleManager

* chore: remove bad imports

* style: pretty

* revert: remove AnyDependencies type

* refactor: fold switch case

* docs: specifics

* chore: change all file names to camel case

* refactor: change all files to camelcase and refactor

* revert: remove cloudflare typings

* feat: SernEmitter now captures promise rejections

* chore: fix InitArgs missing

* chore: move typings

* chore: move and clean

* chore: delete plugins dir

* chore: cleanup dispatchers subdirectory for single file

* chore: move context into structures directory

* refactor: cleaning up code and renaming variables

* chore: update name of function to reflect use

* revert: multiple entry points

* revert: readd discordEvent

* refactor: rename, format, move things

* feat: types organization and cleaning up code base

* fix: unaliased modules would throw error

* build: speed up build

* revert: readd module store and add contract

* add separate id for id processing

* chore: progress of globalizing dependencies type

* chore: update container and init hook progress

* style: format & lint

* feat: dev and prod mode

* fix: directories ignoring incorrectly

* refactor: move metadata outside of module declarations

* revert: re export command executable and event executable

* refactor: a lot

* fix: plugins for class modules and module loader

* style: pretty

* fix class based module loading

* feat: globalize dependencies type

* revert: internal name

* feat: add new sern emitter event

* refactor: remove cast

* refactor: add better typings for sern event modules

* test: add tests

* test: add more tests

* feat: change error handling contract

* chore: make changes in codebase after error contract change

* docs: add purpose of d.ts file

* revert removal of crash method and mark deprecated

* fix: typings for options- have access to all properties now

* refactor: npx knip

* 3.0.0-rc1

* chore: fix for version 3 and reexport old types

* fix: reexport payload and button modules

* fix: component commands incorrectly aligned and ordered

* chore: bump version

* test: add id generation testing

* refactor: algorithm for module resolution

* chore: bump vers

* test: add eventDispatcher test

* *.test.ts

* fix: autocomplete nested option

* chore: bump vers

* add npmignore .yarn

* feat: experimental loading sern.config.json

* refactor: simplify build

* chore: bump vers

* chore: add documentation for service api

* add since

* feat: add possible mode option in file loading mode

* refactor: remove two unneeded functions and refactor to throw early

* refactor: clean up handler code

* fix: undefined this binding

* refactor: clean up signatures and types

* refactor: make evident the internal api and move around stuff

* refactor: remove circular dependencies

* fix circulars and imports

* oops, moving around mroe stuff

* refresh lock

* chore: import type and prettier

* style: prettier

* feat: solidify init logic

* fix module-loading.ts

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-07-29 17:10:19 -05:00
Peter-MJ-Parker
9144485c39 My bot uses sern! (#313)
feat: My bot uses sern!
2023-07-03 21:24:05 -05:00
Evo
cf15b67ede chore: bless seren (#310) 2023-06-19 07:20:24 -05:00
jacob
57cc94ff81 Empty-Commit 2023-06-18 09:59:38 -05:00
92 changed files with 4445 additions and 3406 deletions

View File

@@ -1,13 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"extends": ["plugin:@typescript-eslint/recommended"],
"parserOptions": { "ecmaVersion": "latest", "sourceType": "script" },
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": ["error", "always"],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
}
}

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
open_collective: sern

View File

@@ -1,39 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ main ]
paths: ["src/**/*"]
pull_request:
branches: [ main ]
paths: ["src/**/*"]
schedule:
- cron: '37 20 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -20,10 +20,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: 17
@@ -39,7 +39,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4
with:
commit-message: "style: pretty please"
branch: prettier

View File

@@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2
uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2

34
.github/workflows/npm-publish-dev.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Continuous Delivery
on:
push:
branches:
- main
paths:
- 'src/**'
- 'package.json'
jobs:
Publish:
name: Publishing Dev
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Set up Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- name: Install Node.js dependencies
run: npm i && npm run build:dev
- name: Publish to npm
run: |
npm version premajor --preid "dev.$(git rev-parse --verify --short HEAD)" --git-tag-version=false
npm publish --tag dev
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -2,17 +2,21 @@ name: NPM / Publish
on:
workflow_dispatch:
# We only publish if the version of sern handler is different. workflow automatically cancels if verson is the same
push:
branches:
- 'main'
jobs:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: 17
- run: yarn --immutable
- run: yarn build:prod
- uses: JS-DevTools/npm-publish@v1
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: "public"

View File

@@ -6,7 +6,7 @@ jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
- uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3
with:
release-type: node
package-name: release-please-action

29
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 19.x, 20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install -g yarn
- run: yarn install
- run: yarn test

6
.gitignore vendored
View File

@@ -91,3 +91,9 @@ dist
# Yarn files
.yarn/install-state.gz
.yarn/build-state.yml
.yalc
yalc.lock
*.svg

View File

@@ -8,6 +8,7 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yarn
# Runtime data
pids
*.pid
@@ -80,9 +81,7 @@ typings/
# FuseBox cache
.fusebox/
# TypeScript build output
dist
test
# VisualStudio Config file
.vs
@@ -114,3 +113,4 @@ tsconfig-cjs.json
tsconfig-esm.json
renovate.json
fortnite

View File

@@ -1,8 +0,0 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
}

View File

@@ -1,5 +1,107 @@
# Changelog
## [3.3.4](https://github.com/sern-handler/handler/compare/v3.3.3...v3.3.4) (2024-03-18)
### Bug Fixes
* sern emitter err ([#358](https://github.com/sern-handler/handler/issues/358)) ([90e55df](https://github.com/sern-handler/handler/commit/90e55dfa1466c91e5da48922251309331921b1ef))
## [3.3.3](https://github.com/sern-handler/handler/compare/v3.3.2...v3.3.3) (2024-02-25)
### Bug Fixes
* rm deprecated class modules, clean up, rm indirection ([#355](https://github.com/sern-handler/handler/issues/355)) ([48f9f6e](https://github.com/sern-handler/handler/commit/48f9f6ec16e650d574bd24dcbb0ed176933bfe17))
* singleton init not being fired when inserting function ([07b11b3](https://github.com/sern-handler/handler/commit/07b11b357baac0c3c7055c022bc353995c80f766))
* typings and cleanup ([#356](https://github.com/sern-handler/handler/issues/356)) ([ce8c4bf](https://github.com/sern-handler/handler/commit/ce8c4bf6492b9680fb1c1a530d3e0028f214ad2f))
## [3.3.2](https://github.com/sern-handler/handler/compare/v3.3.1...v3.3.2) (2024-01-08)
### Bug Fixes
* presence feature not working on cjs applications ([#351](https://github.com/sern-handler/handler/issues/351)) ([4f23871](https://github.com/sern-handler/handler/commit/4f2387119acfde036d0d1626553e9050f55627d1))
## [3.3.1](https://github.com/sern-handler/handler/compare/v3.3.0...v3.3.1) (2024-01-07)
### Bug Fixes
* crashing when slash command is used as text command ([#349](https://github.com/sern-handler/handler/issues/349)) ([a359f73](https://github.com/sern-handler/handler/commit/a359f73fa24127a4964d411c8c1c0dfea5edc0f1))
### Reverts
* the last commit ([655bb8d](https://github.com/sern-handler/handler/commit/655bb8d35815fe0ce9797d8b169310a07b284ae0))
## [3.3.0](https://github.com/sern-handler/handler/compare/v3.2.1...v3.3.0) (2023-12-27)
### Features
* presence ([#345](https://github.com/sern-handler/handler/issues/345)) ([7458bef](https://github.com/sern-handler/handler/commit/7458befe8a5900480cd71900df02a8364837dc00))
## [3.2.1](https://github.com/sern-handler/handler/compare/v3.2.0...v3.2.1) (2023-12-21)
### Bug Fixes
* logger swap failing ([daac37c](https://github.com/sern-handler/handler/commit/daac37c28858c42b21042bdcb8141239db634e7d))
## [3.2.0](https://github.com/sern-handler/handler/compare/v3.1.1...v3.2.0) (2023-12-15)
### Miscellaneous Chores
* release 3.2.0 ([237c853](https://github.com/sern-handler/handler/commit/237c8537c66052309d7e13a7e6e0a4f7995c2558))
## [3.1.1](https://github.com/sern-handler/handler/compare/v3.1.0...v3.1.1) (2023-11-06)
### Bug Fixes
* queuing events ([fd39858](https://github.com/sern-handler/handler/commit/fd39858636d3038abb6d91021b65c99c488a3d6e))
* queuing events ([#332](https://github.com/sern-handler/handler/issues/332)) @Benzo-Fury ([#333](https://github.com/sern-handler/handler/issues/333)) ([fd39858](https://github.com/sern-handler/handler/commit/fd39858636d3038abb6d91021b65c99c488a3d6e))
## [3.1.0](https://github.com/sern-handler/handler/compare/v3.0.2...v3.1.0) (2023-09-04)
### Features
* add guaranteed `channelId` and `userId` getters to `Context` ([#320](https://github.com/sern-handler/handler/issues/320)) ([50253ca](https://github.com/sern-handler/handler/commit/50253ca322e7d6dbd2313139c0187a1028f71109))
* dispose hooks (deprecate useContainerRaw) ([#323](https://github.com/sern-handler/handler/issues/323)) ([26ccd11](https://github.com/sern-handler/handler/commit/26ccd118ff8cbcde94158a4d09fc0df18da9f254))
## [3.0.2](https://github.com/sern-handler/handler/compare/v3.0.1...v3.0.2) (2023-08-06)
### Bug Fixes
* invalid id for cts, mts, cjs, mjs files, node paths ([#318](https://github.com/sern-handler/handler/issues/318)) ([a7f5ea2](https://github.com/sern-handler/handler/commit/a7f5ea269fb344e221d10dbdc26a1611ffc8138f))
## [3.0.1](https://github.com/sern-handler/handler/compare/v3.0.0...v3.0.1) (2023-08-05)
### Bug Fixes
* collectors ([4134460](https://github.com/sern-handler/handler/commit/41344608c677b6069c46412f5f16e4337182ca7d))
## [3.0.0](https://github.com/sern-handler/handler/compare/v2.6.3...v3.0.0) (2023-07-29)
### ⚠ BREAKING CHANGES
* v3 ([#294](https://github.com/sern-handler/handler/issues/294))
### Features
* v3 ([#294](https://github.com/sern-handler/handler/issues/294)) ([7798e36](https://github.com/sern-handler/handler/commit/7798e36458c7f555d2bcb8a5857a6db47b7211da))
### Miscellaneous Chores
* release 3.0.0 ([70cca0d](https://github.com/sern-handler/handler/commit/70cca0dbb01e70b47a8c899b1fc4f43dee5ed8ed))
## [2.6.3](https://github.com/sern-handler/handler/compare/v2.6.2...v2.6.3) (2023-06-17)

View File

@@ -18,30 +18,17 @@
- For you. A framework that's tailored to your exact needs.
- Lightweight. Does a lot while being small.
- Latest features. Support for discord.js v14 and all of its interactions.
- Hybrid, customizable and composable commands. Create them just how you like.
- Start quickly. Plug and play or customize to your liking.
- Embraces reactive programming. For consistent and reliable backend.
- Switch and customize how errors are handled, logging, and more.
- works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
- Unleash its full potential with a powerful CLI and awesome plugins.
## 📜 Installation
```sh
npm install @sern/handler
```
```sh
yarn add @sern/handler
```
```sh
pnpm add @sern/handler
```
[Start here!!](https://sern.dev/docs/guide/walkthrough/new-project)
## 👶 Basic Usage
<details open><summary>ping.ts</summary>
<details><summary>ping.ts</summary>
```ts
export default commandModule({
@@ -55,55 +42,7 @@ export default commandModule({
});
```
</details>
<details open><summary>modal.ts</summary>
```ts
export default commandModule({
type: CommandType.Modal,
//Installed a plugin to make sure modal fields pass a validation.
plugins : [
assertFields({
fields: {
name: /^([^0-9]*)$/
},
failure: (errors, modal) => modal.reply('your submission did not pass the validations')
})
],
execute : (modal) => {
modal.reply('thanks for the submission!');
}
})
```
</details>
<details open><summary>index.ts</summary>
```ts
import { Client, GatewayIntentBits } from 'discord.js';
import { Sern, single, type Dependencies } from '@sern/handler';
//client has been declared previously
interface MyDependencies extends Dependencies {
'@sern/client': Singleton<Client>;
}
export const useContainer = Sern.makeDependencies<MyDependencies>({
build: root => root
.add({ '@sern/client': single(() => client) })
});
//View docs for all options
Sern.init({
defaultPrefix: '!', // removing defaultPrefix will shut down text commands
commands: 'src/commands',
// events: 'src/events' (optional),
containerConfig : {
get: useContainer
}
});
client.login("YOUR_BOT_TOKEN_HERE");
```
</details>
## 🤖 Bots Using sern
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
@@ -112,7 +51,9 @@ client.login("YOUR_BOT_TOKEN_HERE");
- [ava](https://github.com/SrIzan10/ava), A discord bot that plays KNGI and Gensokyo Radio.
- [Murayama](https://github.com/murayamabot/murayama), :pepega:
- [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud), A fun bot for a small - but growing - server.
- [Man Nomic](https://github.com/jacoobes/man-nomic), A simple information bot to provide information to the nomic-ai discord community.
- [Linear-Discord](https://github.com/sern-handler/linear-discord) Display and manage a linear dashboard.
## 💻 CLI
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.

1
fortnite Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,26 +1,27 @@
{
"name": "@sern/handler",
"packageManager": "yarn@3.5.1",
"version": "2.6.3",
"packageManager": "yarn@3.5.0",
"version": "4.0.0",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"scripts": {
"watch": "tsup --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"watch": "tsc --watch",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"build:dev": "tsup && tsup --dts-only --outDir dist",
"build:prod": "tsup --minify && tsup --dts-only --outDir dist",
"publish": "npm run build:prod",
"pretty": "prettier --write ."
"build:dev": "tsc",
"build:prod": "tsc",
"prepare": "tsc",
"pretty": "prettier --write .",
"tdd": "vitest",
"test": "vitest --run",
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
},
"keywords": [
"sern-handler",
@@ -34,25 +35,64 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"iti": "^0.6.0",
"@sern/ioc": "^1.1.0",
"callsites": "^3.1.0",
"cron": "^3.1.7",
"deepmerge": "^4.3.1",
"rxjs": "^7.8.0",
"ts-results-es": "^3.6.0"
"ts-results-es": "^4.1.0"
},
"devDependencies": {
"@types/node": "^18.15.11",
"@faker-js/faker": "^8.0.1",
"@types/node": "^20.0.0",
"@types/node-cron": "^3.0.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"discord.js": "^14.9.0",
"esbuild": "^0.17.0",
"esbuild-ifdef": "^0.2.0",
"discord.js": "^14.15.3",
"eslint": "8.39.0",
"prettier": "2.8.8",
"tsup": "^6.7.0",
"typescript": "5.0.2"
"typescript": "5.0.2",
"vitest": "^1.6.0"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "script"
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"quotes": [
2,
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"semi": [
"error",
"always"
],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/handler.git"
},
"homepage": "https://sern.dev"
"engines": {
"node": ">= 20.0.x"
},
"homepage": "https://sern.dev",
"overrides": {
"ws": "8.17.1"
},
"resolutions": {
"ws": "8.17.1"
}
}

View File

@@ -1,4 +1,9 @@
{
"extends": [
"config:base",
"helpers:pinGitHubActionDigests",
"group:allNonMajor"
],
"major": {
"dependencyDashboardApproval": true,
"reviewers": ["EvolutionX-10", "jacoobes", "Murtatrxx"]

126
src/core/functions.ts Normal file
View File

@@ -0,0 +1,126 @@
import type { Module, SernAutocompleteData, SernOptionsData } from '../types/core-modules';
import type {
AnySelectMenuInteraction,
ButtonInteraction,
ChatInputCommandInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
AutocompleteInteraction
} from 'discord.js';
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
import { PluginType } from './structures/enums';
import assert from 'assert';
import type { Payload } from '../types/utility';
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg
* @param prefix The prefix to remove
* @returns The message without the prefix
* @example
* message.content = '!ping';
* console.log(fmt(message.content, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: string, prefix?: string): string[] {
if(!prefix) throw Error("Unable to parse message without prefix");
return msg.slice(prefix.length).trim().split(/\s+/g);
}
export function partitionPlugins<T,V>
(arr: Array<{ type: PluginType }> = []): [T[], V[]] {
const controlPlugins = [];
const initPlugins = [];
for (const el of arr) {
switch (el.type) {
case PluginType.Control: controlPlugins.push(el); break;
case PluginType.Init: initPlugins.push(el); break;
}
}
return [controlPlugins, initPlugins] as [T[], V[]];
}
/**
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* @param iAutocomplete
* @param options
*/
export function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name);
for (const option of cur.options ?? []) _options.push(option);
} break;
case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command);
} break;
default: {
if ('autocomplete' in cur && cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
}
} break;
}
}
}
interface InteractionTypable {
type: InteractionType;
}
//discord.js pls fix ur typings or i will >:(
type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction;
type AnyCommandInteraction =
| ChatInputCommandInteraction
| MessageContextMenuCommandInteraction
| UserContextMenuCommandInteraction;
export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction {
return i.type === InteractionType.MessageComponent;
}
export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
return i.type === InteractionType.ApplicationCommand;
}
export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction {
return i.type === InteractionType.ApplicationCommandAutocomplete;
}
export function isModal(i: InteractionTypable): i is ModalSubmitInteraction {
return i.type === InteractionType.ModalSubmit;
}
export function resultPayload<T extends 'success'|'warning'|'failure'>
(type: T, module?: Module, reason?: unknown) {
return { type, module, reason } as Payload & { type : T };
}
export function pipe<T>(arg: unknown, firstFn: Function, ...fns: Function[]): T {
let result = firstFn(arg);
for (let fn of fns) {
result = fn(result);
}
return result;
}

74
src/core/id.ts Normal file
View File

@@ -0,0 +1,74 @@
import { ApplicationCommandType, ComponentType, type Interaction, InteractionType } from 'discord.js';
import { CommandType, EventType } from './structures/enums';
const parseParams = (event: { customId: string }, append: string) => {
const hasSlash = event.customId.indexOf('/')
if(hasSlash === -1) {
return { id:event.customId+append };
}
const baseid = event.customId.substring(0, hasSlash);
const params = event.customId.substring(hasSlash+1);
return { id: baseid+append, params }
}
/**
* Construct unique ID for a given interaction object.
* @param event The interaction object for which to create an ID.
* @returns An array of unique string IDs based on the type and properties of the interaction object.
*/
export function reconstruct<T extends Interaction>(event: T) {
switch (event.type) {
case InteractionType.MessageComponent: {
const data = parseParams(event, `_C${event.componentType}`)
return [data];
}
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete:
return [{ id: `${event.commandName}_A${event.commandType}` }, { id: `${event.commandName}_B` }];
//Modal interactions are classified as components for sern
case InteractionType.ModalSubmit: {
const data = parseParams(event, '_M');
return [data];
}
}
}
/**
*
* A magic number to represent any commandtype that is an ApplicationCommand.
*/
const PUBLISHABLE = 0b000000001111;
const TypeMap = new Map<number, number>([[CommandType.Text, 0],
[CommandType.Both, 0],
[CommandType.Slash, ApplicationCommandType.ChatInput],
[CommandType.CtxUser, ApplicationCommandType.User],
[CommandType.CtxMsg, ApplicationCommandType.Message],
[CommandType.Button, ComponentType.Button],
[CommandType.StringSelect, ComponentType.StringSelect],
[CommandType.Modal, InteractionType.ModalSubmit],
[CommandType.UserSelect, ComponentType.UserSelect],
[CommandType.MentionableSelect, ComponentType.MentionableSelect],
[CommandType.RoleSelect, ComponentType.RoleSelect],
[CommandType.ChannelSelect, ComponentType.ChannelSelect]]);
/*
* Generates an id based on name and CommandType.
* A is for any ApplicationCommand. C is for any ComponentCommand
* Then, another number fetched from TypeMap
*/
export function create(name: string, type: CommandType | EventType) {
if(type == CommandType.Text) {
return `${name}_T`;
}
if(type == CommandType.Both) {
return `${name}_B`;
}
if(type == CommandType.Modal) {
return `${name}_M`;
}
const am = (PUBLISHABLE & type) !== 0 ? 'A' : 'C';
return `${name}_${am}${TypeMap.get(type)!}`
}

55
src/core/interfaces.ts Normal file
View File

@@ -0,0 +1,55 @@
import type { AnyFunction } from '../types/utility';
/**
* Represents an initialization contract.
* Let dependencies implement this to initiate some logic.
*/
export interface Init {
init(): unknown;
}
/**
* Represents a Disposable contract.
* Let dependencies implement this to dispose and cleanup.
*/
export interface Disposable {
dispose(): unknown;
}
export interface Emitter {
addListener(eventName: string | symbol, listener: AnyFunction): this;
removeListener(eventName: string | symbol, listener: AnyFunction): this;
emit(eventName: string | symbol, ...payload: any[]): boolean;
}
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* @deprecated
* Version 4 will remove this method
*/
crash(err: Error): never;
/**
* A function that is called on every throw.
* @param error
*/
updateAlive(error: Error): void;
}
/**
* @since 2.0.0
*/
export interface Logging<T = unknown> {
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
export type LogPayload<T = unknown> = { message: T };

163
src/core/ioc.ts Normal file
View File

@@ -0,0 +1,163 @@
import { Service as $Service, Services as $Services } from '@sern/ioc/global'
import { Container } from '@sern/ioc';
import * as Contracts from './interfaces';
import * as __Services from './structures/default-services';
import type { Logging } from './interfaces';
import { __init_container, useContainerRaw } from '@sern/ioc/global';
import { EventEmitter } from 'node:events';
import { Client } from 'discord.js';
import { Module } from '../types/core-modules';
import { UnpackFunction } from '../types/utility';
export function disposeAll(logger: Logging|undefined) {
useContainerRaw()
?.disposeAll()
.then(() => logger?.info({ message: 'Cleaning container and crashing' }));
}
type Insertable = | ((container: Dependencies) => object)
| object
const dependencyBuilder = (container: Container) => {
return {
/**
* Insert a dependency into your container.
* Supply the correct key and dependency
*/
add(key: keyof Dependencies, v: Insertable) {
if(typeof v !== 'function') {
container.addSingleton(key, v)
} else {
//@ts-ignore
container.addWiredSingleton(key, (cntr) => v(cntr))
}
},
/**
* @param key the key of the dependency
* @param v The dependency to swap out.
* Swap out a preexisting dependency.
*/
swap(key: keyof Dependencies, v: Insertable) {
if(typeof v !== 'function') {
container.swap(key, v);
} else {
container.swap(key, v(container.deps()));
}
},
};
};
type ValidDependencyConfig =
(c: ReturnType<typeof dependencyBuilder>) => any
/**
* makeDependencies constructs a dependency injection container for sern handler to use.
* This is required to start the handler, and is to be called before Sern.init.
* @example
* ```ts
* await makeDependencies(({ add }) => {
* add('@sern/client', new Client({ intents, partials })
* })
* ```
*/
export async function makeDependencies (conf: ValidDependencyConfig) {
const container = await __init_container({ autowire: false });
//We only include logger if it does not exist
const includeLogger = !container.hasKey('@sern/logger');
if(includeLogger) {
container.addSingleton('@sern/logger', new __Services.DefaultLogging);
}
container.addSingleton('@sern/errors', new __Services.DefaultErrorHandling);
container.addSingleton('@sern/modules', new Map);
container.addSingleton('@sern/emitter', new EventEmitter)
container.addSingleton('@sern/scheduler', new __Services.TaskScheduler)
conf(dependencyBuilder(container));
await container.ready();
}
/**
* The Service api, which allows users to access dependencies in places IOC cannot reach.
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
* Our scaffolding tool takes care of this.
* Note: this method only works AFTER your container has been initiated
* @since 3.0.0
* @example
* ```ts
* const client = Service('@sern/client');
* ```
* @param key a key that corresponds to a dependency registered.
* @throws if container is absent or not present
*/
export function Service<const T extends keyof Dependencies>(key: T) {
return $Service(key) as Dependencies[T]
}
/**
* @since 3.0.0
* The plural version of {@link Service}
* @throws if container is absent or not present
* @returns array of dependencies, in the same order of keys provided
*
*/
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
return $Services<T, IntoDependencies<T>>(...keys)
}
/**
* @deprecated
* Creates a singleton object.
* @param cb
*/
export function single<T>(cb: () => T) {
console.log('The `single` function is deprecated and has no effect')
return cb();
}
/**
* @deprecated
* @since 2.0.0
* Creates a transient object
* @param cb
*/
export function transient<T>(cb: () => () => T) {
console.log('The `transient` function is deprecated and has no effect')
return cb()();
}
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
export type IntoDependencies<Tuple extends [...any[]]> = {
[Index in keyof Tuple]: UnpackFunction<NonNullable<DependencyFromKey<Tuple[Index]>>>; //Unpack and make NonNullable
} & { length: Tuple['length'] };
export interface CoreDependencies {
/**
* discord.js client.
*/
'@sern/client': Client;
/**
* sern emitter listens to events that happen throughout
* the handler. some include module.register, module.activate.
*/
'@sern/emitter': Contracts.Emitter;
/**
* An error handler which is the final step before
* the sern process actually crashes.
*/
'@sern/errors': Contracts.ErrorHandling;
/**
* Optional logger. Performs ... logging
*/
'@sern/logger'?: Contracts.Logging;
/**
* Readonly module store. sern stores these
* by module.meta.id -> Module
*/
'@sern/modules': Map<string, Module>;
'@sern/scheduler': __Services.TaskScheduler
}

View File

@@ -0,0 +1,70 @@
import path from 'node:path';
import { existsSync } from 'node:fs';
import { readdir } from 'fs/promises';
import assert from 'node:assert';
import * as Id from './id'
import { Module } from '../types/core-modules';
export const parseCallsite = (site: string) => {
const pathobj = path.posix.parse(site.replace(/file:\\?/, "")
.split(path.sep)
.join(path.posix.sep))
return { name: pathobj.name,
absPath : path.posix.format(pathobj) }
}
export const shouldHandle = (pth: string, filenam: string) => {
const file_name = filenam+path.extname(pth);
let newPath = path.join(path.dirname(pth), file_name)
.replace(/file:\\?/, "");
return { exists: existsSync(newPath),
path: 'file://'+newPath };
}
/**
* Import any module based on the absolute path.
* This can accept four types of exported modules
* commonjs, javascript :
* ```js
* exports = commandModule({ })
* //or
* exports.default = commandModule({ })
* ```
* esm javascript, typescript, and commonjs typescript
* export default commandModule({})
*/
export async function importModule<T>(absPath: string) {
let fileModule = await import(absPath);
let commandModule: Module = fileModule.default;
assert(commandModule , `No default export @ ${absPath}`);
if ('default' in commandModule) {
commandModule = commandModule.default as Module;
}
const p = path.parse(absPath)
commandModule.name ??= p.name; commandModule.description ??= "...";
commandModule.meta = {
id: Id.create(commandModule.name, commandModule.type),
absPath,
};
return { module: commandModule as T };
}
export async function* readRecursive(dir: string): AsyncGenerator<string> {
const files = await readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.posix.join(dir, file.name);
if (file.isDirectory()) {
if (!file.name.startsWith('!')) {
yield* readRecursive(fullPath);
}
} else if (!file.name.startsWith('!')) {
yield "file:///"+path.resolve(fullPath);
}
}
}

51
src/core/modules.ts Normal file
View File

@@ -0,0 +1,51 @@
import type { ClientEvents } from 'discord.js';
import { EventType } from '../core/structures/enums';
import type {
InputCommand,
InputEvent,
Module,
ScheduledTask,
} from '../types/core-modules';
import { partitionPlugins } from './functions'
import type { Awaitable } from '../types/utility';
/**
* @since 1.0.0 The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommand): Module {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
return { ...mod,
onEvent,
plugins,
locals: {} } as Module;
}
/**
* @since 1.0.0
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEvent): Module {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
if(onEvent.length !== 0) throw Error("Event modules cannot have ControlPlugins");
return { ...mod,
plugins,
locals: {} } as Module;
}
/** Create event modules from discord.js client events,
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
once?: boolean;
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod, });
}
export function scheduledTask(ism: ScheduledTask) { return ism }

45
src/core/plugin.ts Normal file
View File

@@ -0,0 +1,45 @@
import { CommandType, PluginType } from './structures/enums';
import type { Plugin, PluginResult, CommandArgs, InitArgs } from '../types/core-plugin';
import { Err, Ok } from 'ts-results-es';
export function makePlugin<V extends unknown[]>(
type: PluginType,
execute: (...args: any[]) => any,
): Plugin<V> {
return { type, execute } as Plugin<V>;
}
/**
* @since 2.5.0
*/
export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (args: InitArgs) => PluginResult
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: (val?: Record<string,unknown>) => Ok(val),
stop: (val?: string) => Err(val),
};
export type Controller = typeof controller;

66
src/core/presences.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { ActivitiesOptions } from "discord.js";
import type { IntoDependencies } from "./ioc";
import type { Emitter } from "./interfaces";
type Status = 'online' | 'idle' | 'invisible' | 'dnd'
type PresenceReduce = (previous: Presence.Result) => Presence.Result;
export const Presence = {
/**
* A small wrapper to provide type inference.
* Create a Presence module which **MUST** be put in a file called presence.(language-extension)
* adjacent to the file where **Sern.init** is CALLED.
*/
module : <T extends (keyof Dependencies)[]>(conf: Presence.Config<T>) => conf,
/**
* Create a Presence body which can be either:
* - once, the presence is activated only once.
* - repeated, per cycle or event, the presence can be changed.
*/
of : (root: Omit<Presence.Result, 'repeat' | 'onRepeat'>) => {
return {
/**
* @example
* Presence
* .of({ activities: [{ name: "deez nuts" }] }) //starts presence with "deez nuts".
* .repeated(prev => {
* return {
* afk: true,
* activities: prev.activities?.map(s => ({ ...s, name: s.name+"s" }))
* };
* }, 10000)) //every 10 s, the callback sets the presence to the value returned.
*/
repeated: (onRepeat: PresenceReduce, repeat: number | [Emitter, string]) => {
return { repeat, onRepeat, ...root }
},
/**
* @example
* ```ts
* Presence.of({
* activities: [{ name: "Chilling out" }]
* }).once() // Sets the presence once, with what's provided in '.of()'
* ```
*/
once: () => root
};
}
}
export declare namespace Presence {
export type Config<T extends (keyof Dependencies)[]> = {
inject?: [...T]
execute: (...v: IntoDependencies<T>) => Presence.Result;
}
export interface Result {
status?: Status;
afk?: boolean;
activities?: ActivitiesOptions[];
shardId?: number[];
repeat?: number | [Emitter, string];
onRepeat?: (previous: Result) => Result;
}
}

View File

@@ -0,0 +1,143 @@
import type {
BaseInteraction,
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
MessageReplyOptions,
Snowflake,
User,
} from 'discord.js';
import { CoreContext } from '../structures/core-context';
import { Result, Ok, Err } from 'ts-results-es';
import * as assert from 'assert';
import type { ReplyOptions } from '../../types/utility';
import { fmt } from '../functions'
import { SernError } from './enums';
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
get options() {
if(this.isMessage()) {
const [, ...rest] = fmt(this.message.content, this.prefix);
return rest;
} else {
return this.interaction.options;
}
}
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>,
private __prefix?: string) {
super(ctx);
}
public get prefix() {
return this.__prefix;
}
public get id(): Snowflake {
return safeUnwrap(this.ctx
.map(m => m.id)
.mapErr(i => i.id));
}
public get channel() {
return safeUnwrap(this.ctx.map(m => m.channel).mapErr(i => i.channel));
}
public get channelId(): Snowflake {
return safeUnwrap(this.ctx
.map(m => m.channelId)
.mapErr(i => i.channelId));
}
/**
* If context is holding a message, message.author
* else, interaction.user
*/
public get user(): User {
return safeUnwrap(this.ctx
.map(m => m.author)
.mapErr(i => i.user));
}
public get userId(): Snowflake {
return this.user.id;
}
public get createdTimestamp(): number {
return safeUnwrap(this.ctx
.map(m => m.createdTimestamp)
.mapErr(i => i.createdTimestamp));
}
public get guild() {
return safeUnwrap(this.ctx
.map(m => m.guild)
.mapErr(i => i.guild));
}
public get guildId() {
return safeUnwrap(this.ctx
.map(m => m.guildId)
.mapErr(i => i.guildId));
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return safeUnwrap(this.ctx
.map(m => m.member)
.mapErr(i => i.member));
}
get message(): Message {
return this.ctx.expect(SernError.MismatchEvent);
}
get interaction(): ChatInputCommandInteraction {
return this.ctx.expectErr(SernError.MismatchEvent);
}
public get client(): Client {
return safeUnwrap(this.ctx
.map(m => m.client)
.mapErr(i => i.client));
}
public get inGuild(): boolean {
return safeUnwrap(this.ctx
.map(m => m.inGuild())
.mapErr(i => i.inGuild()));
}
public async reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
.map(m => m.reply(content as MessageReplyOptions))
.mapErr(i =>
i.reply(content as InteractionReplyOptions).then(() => i.fetchReply())),
);
}
static wrap(wrappable: BaseInteraction | Message, prefix?: string): Context {
if ('interaction' in wrappable) {
return new Context(Ok(wrappable), prefix);
}
assert.ok(wrappable.isChatInputCommand(), "Context created with bad interaction.");
return new Context(Err(wrappable), prefix);
}
}
function safeUnwrap<T>(res: Result<T, T>) {
if(res.isOk()) {
return res.expect("Tried unwrapping message field: " + res)
}
return res.expectErr("Tried unwrapping interaction field" + res)
}

View File

@@ -0,0 +1,18 @@
import { Result as Either } from 'ts-results-es';
import * as assert from 'node:assert';
/**
* @since 3.0.0
*/
export abstract class CoreContext<M, I> {
protected constructor(protected ctx: Either<M, I>) {
assert.ok(typeof ctx === 'object' && ctx != null, "Context was nonobject or null");
}
public isMessage(): this is CoreContext<M, never> {
return this.ctx.isOk();
}
public isSlash(): this is CoreContext<never, I> {
return !this.isMessage();
}
}

View File

@@ -0,0 +1,89 @@
import { ScheduledTask } from '../../types/core-modules';
import type { LogPayload, Logging, ErrorHandling, Disposable } from '../interfaces';
import { CronJob } from 'cron';
/**
* @internal
* @since 2.0.0
* Version 4.0.0 will internalize this api. Please refrain from using the defaults!
*/
export class DefaultErrorHandling implements ErrorHandling {
crash(err: Error): never {
throw err;
}
updateAlive(err: Error) {
throw err;
}
}
/**
* @internal
* @since 2.0.0
* Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
*/
export class DefaultLogging implements Logging {
private date() { return new Date() }
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}
export class TaskScheduler implements Disposable {
private __tasks: Map<string, CronJob<any, any>> = new Map();
schedule(uuid: string, task: ScheduledTask, deps: Dependencies) {
if (this.__tasks.has(uuid)) {
throw Error("while scheduling a task \
found another task of same name. Not scheduling " +
uuid + "again." );
}
try {
const onTick = async function(this: CronJob) {
task.execute({ id: uuid,
lastTimeExecution: this.lastExecution,
nextTimeExecution: this.nextDate().toJSDate() }, { deps })
}
const job = CronJob.from({ cronTime: task.trigger, onTick, timeZone: task.timezone });
job.start();
this.__tasks.set(uuid, job);
} catch (error) {
throw Error(`while scheduling a task ${uuid} ` + error);
}
}
kill(taskName: string): boolean {
const job = this.__tasks.get(taskName);
if (job) {
job.stop();
this.__tasks.delete(taskName);
return true;
}
return false;
}
get tasks(): string[] {
return Array.from(this.__tasks.keys());
}
dispose() {
this.__tasks.forEach((_, id) => {
this.kill(id);
this.__tasks.delete(id);
})
}
}

View File

@@ -15,45 +15,18 @@
* ```
*/
export enum CommandType {
/**
* The CommandType for text commands
*/
Text = 1,
/**
* The CommandType for slash commands
*/
Slash = 2,
/**
* The CommandType for hybrid commands, text and slash
*/
Text = 1 << 0,
Slash = 1 << 1,
Both = 3,
/**
* The CommandType for UserContextMenuInteraction commands
*/
CtxUser = 4,
/**
* The CommandType for MessageContextMenuInteraction commands
*/
CtxMsg = 8,
/**
* The CommandType for ButtonInteraction commands
*/
Button = 16,
/**
* The CommandType for StringSelectMenuInteraction commands
*/
StringSelect = 32,
/**
* The CommandType for ModalSubmitInteraction commands
*/
Modal = 64,
/**
* The CommandType for the other SelectMenuInteractions
*/
ChannelSelect = 256,
MentionableSelect = 512,
RoleSelect = 1024,
UserSelect = 2048,
CtxUser = 1 << 2,
CtxMsg = 1 << 3,
Button = 1 << 4,
StringSelect = 1 << 5,
Modal = 1 << 6,
UserSelect = 1 << 7,
RoleSelect = 1 << 8,
MentionableSelect = 1 << 9,
ChannelSelect = 1 << 10,
}
/**
@@ -75,16 +48,16 @@ export enum EventType {
/**
* The EventType for handling discord events
*/
Discord = 1,
Discord,
/**
* The EventType for handling sern events
*/
Sern = 2,
Sern,
/**
* The EventType for handling external events.
* Could be for example, `process` events, database events
*/
External = 3,
External,
}
/**
@@ -106,35 +79,56 @@ export enum PluginType {
* The PluginType for InitPlugins
*/
Init = 1,
/**
* @deprecated
* Use PluginType.Init instead
*/
Command = 1,
/**
* @deprecated
* Use PluginType.Control instead
*/
Event = 2,
/**
* The PluginType for EventPlugins
*/
Control = 2,
}
/**
* @deprecated - Use strings 'success' | 'failure' | 'warning'
* @enum { string }
*/
export enum PayloadType {
/**
* The PayloadType for a SernEmitter success event
*/
Success = 'success',
/**
* The PayloadType for a SernEmitter failure event
*/
Failure = 'failure',
/**
* The PayloadType for a SernEmitter warning event
*/
Warning = 'warning',
}
/**
* @enum { string }
*/
export const enum SernError {
/**
* Throws when registering an invalid module.
* This means it is undefined or an invalid command type was provided
*/
InvalidModuleType = 'Detected an unknown module type',
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
UndefinedModule = `A module could not be detected`,
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
MismatchModule = `A module type mismatched with event emitted!`,
/**
* Unsupported interaction at this moment.
*/
NotSupportedInteraction = `This interaction is not supported.`,
/**
* One plugin called `controller.stop()` (end command execution / loading)
*/
PluginFailure = `A plugin failed to call controller.next()`,
/**
* A crash that occurs when accessing an invalid property of Context
*/
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
/**
* Unsupported feature attempted to access at this time
*/
NotSupportedYet = `This feature is not supported yet`,
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -1,50 +0,0 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import util from 'util';
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
*/
keepAlive: number;
/**
* Utility function to crash
* @param error
*/
crash(error: Error): never;
/**
* A function that is called on every crash. Updates keepAlive
* @param error
*/
updateAlive(error: Error): void;
}
/**
* @since 2.0.0
*/
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(_: Error) {
this.keepAlive--;
}
}
export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
const err = pload instanceof Error ? pload : Error(util.format(pload));
if (crashHandler.keepAlive == 0) {
crashHandler.crash(err);
}
//formatted payload
logging?.error({ message: util.format(pload) });
crashHandler.updateAlive(err);
return caught;
};
}

View File

@@ -1,3 +0,0 @@
export { ErrorHandling, DefaultErrorHandling } from './errorHandling';
export { Logging, DefaultLogging } from './logging';
export { ModuleManager, DefaultModuleManager } from './moduleManager';

View File

@@ -1,31 +0,0 @@
import type { LogPayload } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface Logging<T = unknown> {
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
/**
* @since 2.0.0
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}

View File

@@ -1,27 +0,0 @@
import type { CommandModuleDefs } from '../../types/module';
import type { CommandType, ModuleStore } from '../structures';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface ModuleManager {
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
): Processed<CommandModuleDefs[T]> | undefined;
set(strat: (ms: ModuleStore) => void): void;
}
/**
* @since 2.0.0
*/
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {}
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
) {
return strat(this.moduleStore);
}
set(strat: (ms: ModuleStore) => void): void {
strat(this.moduleStore);
}
}

View File

@@ -1,2 +0,0 @@
export { single, transient, many } from './lifetimeFunctions';
export { useContainerRaw } from './provider';

View File

@@ -1,63 +0,0 @@
import { _const } from '../utilities/functions';
type NotFunction =
| string
| number
| boolean
| null
| undefined
| bigint
| readonly any[]
| { apply?: never; [k: string]: any }
| { call?: never; [k: string]: any };
/**
* @deprecated
* @param cb
*/
export function single<T extends NotFunction>(cb: T): () => T;
/**
* New signature
* @since 2.0.0
* @param cb
*/
export function single<T extends () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0.
* Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
* very annoying).
* For future versions, ensure that single is being passed as a **callback!!**
* @param cb
*/
export function single<T>(cb: T) {
if (typeof cb === 'function') return cb;
return () => cb;
}
/**
* @deprecated
* @param cb
* Deprecated signature
*/
export function transient<T extends NotFunction>(cb: T): () => () => T;
export function transient<T extends () => () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0
* Following iti's singleton and transient implementation,
* use transient if you want a new dependency every time your container getter is called
* @param cb
*/
export function transient<T>(cb: (() => () => T) | T) {
if (typeof cb !== 'function') return () => () => cb;
return cb;
}
/**
* @__PURE__
* @deprecated
* @param value
* Please use the transient function instead
*/
// prettier-ignore
export const many = <T>(value: T) => () => _const(value);

View File

@@ -1,86 +0,0 @@
import type { Container } from 'iti';
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
import SernEmitter from '../sernEmitter';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
import { Result } from 'ts-results-es';
import { BehaviorSubject } from 'rxjs';
import { createContainer } from 'iti';
import { type Wrapper, ModuleStore, SernError } from '../structures';
export const containerSubject = new BehaviorSubject(defaultContainer());
/**
* Given the user's conf, check for any excluded dependency keys.
* Then, call conf.build to get the rest of the users' dependencies.
* Finally, update the containerSubject with the new container state
* @param conf
*/
export function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Get the current container. This should have no client or possible logger yet.
const currentContainer = containerSubject.getValue();
const excludeLogger = conf.exclude?.has('@sern/logger');
if (!excludeLogger) {
currentContainer.add({
'@sern/logger': () => new DefaultLogging(),
});
}
//Build the container based on the callback provided by the user
const container = conf.build(currentContainer);
//Check if the built container contains @sern/client or throw
// a runtime exception
Result.wrap(() => container.get('@sern/client')).expect(SernError.MissingRequired);
if (!excludeLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
//I'm sorry little one
containerSubject.next(container as any);
}
export function useContainer<T extends Dependencies>() {
const container = containerSubject.getValue() as Container<T, {}>;
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => Result.wrap(() => container.get(key)).unwrapOr(undefined)) as MapDeps<T, V>;
}
/**
* Returns the underlying data structure holding all dependencies.
* Please be careful as this only gets the client's current state.
* Exposes some methods from iti
*/
export function useContainerRaw<T extends Dependencies>() {
return containerSubject.getValue() as Container<T, {}>;
}
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
function defaultContainer() {
return createContainer()
.add({ '@sern/errors': () => new DefaultErrorHandling() })
.add({ '@sern/store': () => new ModuleStore() })
.add(ctx => {
return {
'@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']),
};
})
.add({ '@sern/emitter': () => new SernEmitter() }) as Container<
Omit<Dependencies, '@sern/client' | '@sern/logger'>,
{}
>;
}
export function makeFetcher(wrapper: Wrapper) {
const requiredDependencyKeys = [
'@sern/emitter',
'@sern/client',
'@sern/errors',
'@sern/logger',
] as ['@sern/emitter', '@sern/client', '@sern/errors', '@sern/logger'];
return <Keys extends (keyof Dependencies)[]>(otherKeys: [...Keys]) =>
wrapper.containerConfig.get(...requiredDependencyKeys, ...otherKeys) as MapDeps<
Dependencies,
[...typeof requiredDependencyKeys, ...Keys]
>;
}

View File

@@ -1,67 +0,0 @@
import type { Processed } from '../../../types/handler';
import type { AutocompleteInteraction } from 'discord.js';
import { SernError } from '../../structures';
import treeSearch from '../../utilities/treeSearch';
import type { BothCommand, CommandModule, Module, SlashCommand } from '../../../types/module';
import { EventEmitter } from 'events';
import * as assert from 'assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import { arrayifySource, callPlugin } from '../operators';
import { createResultResolver } from '../observableHandling';
export function dispatchCommand(module: Processed<CommandModule>, createArgs: () => unknown[]) {
const args = createArgs();
return {
module,
args,
};
}
function intoPayload(module: Processed<Module>) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
onNext: ({ args }) => args,
});
/**
* Creates an observable from { source }
* @param module
* @param source
*/
export function eventDispatcher(module: Processed<Module>, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
const execute: OperatorFunction<unknown[], unknown> = concatMap(async args =>
module.execute(...args),
);
return fromEvent(source, module.name).pipe(
intoPayload(module),
concatMap(createResult),
execute,
);
}
export function dispatchAutocomplete(
module: Processed<BothCommand | SlashCommand>,
interaction: AutocompleteInteraction,
) {
const option = treeSearch(interaction, module.options);
if (option !== undefined) {
return {
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [interaction],
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
}

View File

@@ -1,2 +0,0 @@
export * from './dispatchers';
export * from './provideArgs';

View File

@@ -1,23 +0,0 @@
import type { ChatInputCommandInteraction, Interaction, Message } from 'discord.js';
import { Context } from '../../structures';
import type { Args, SlashOptions } from '../../../types/handler';
/**
* function overloads to create an arguments list for Context
* @param wrap
* @param messageArgs
*/
export function contextArgs(
wrap: Message,
messageArgs?: string[],
): () => [Context, ['text', string[]]];
export function contextArgs(wrap: Interaction): () => [Context, ['slash', SlashOptions]];
export function contextArgs(wrap: Interaction | Message, messageArgs?: string[]) {
const ctx = Context.wrap(wrap as ChatInputCommandInteraction | Message);
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.interaction.options];
return () => [ctx, args] as [Context, Args];
}
export function interactionArg<T extends Interaction>(interaction: T) {
return () => [interaction] as [T];
}

View File

@@ -1,118 +0,0 @@
import { Interaction } from 'discord.js';
import {
catchError,
concatMap,
EMPTY,
filter,
finalize,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
} from 'rxjs';
import { CommandType, type ModuleStore, SernError } from '../structures';
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module';
import { ErrorHandling, handleError } from '../contracts/errorHandling';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
function makeInteractionProcessor(
modules: ModuleManager,
): OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }> {
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
return pipe(
concatMap(event => {
if (event.isMessageComponent()) {
const customId = event.customId;
const module = get(ms => {
return ms.InteractionHandlers[event.componentType].get(customId);
});
return of({ module, event });
} else if (event.isCommand() || event.isAutocomplete()) {
const commandName = event.commandName;
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(commandName) ??
ms.BothCommands.get(commandName),
);
return of({ module, event });
} else if (event.isModalSubmit()) {
const module = get(ms => ms.ModalSubmit.get(event.customId));
return of({ module, event });
} else return EMPTY;
}),
filter(m => m.module !== undefined),
) as OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }>;
}
export function makeInteractionCreate([s, client, err, log, modules]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
]) {
//map. If nothing again,this means a slash command
const interactionStream$ = fromEvent(client, 'interactionCreate') as Observable<Interaction>;
const interactionProcessor = makeInteractionProcessor(modules);
return interactionStream$
.pipe(
interactionProcessor,
map(createDispatcher),
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(module => executeModule(s, module)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
.subscribe();
}
function createDispatcher({
module,
event,
}: {
event: Interaction;
module: Processed<CommandModule>;
}) {
switch (module.type) {
case CommandType.Text:
throw Error(SernError.MismatchEvent);
case CommandType.Slash:
case CommandType.Both: {
if (event.isAutocomplete()) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
*/
return dispatchAutocomplete(module, event);
} else {
return dispatchCommand(module, contextArgs(event));
}
}
default:
return dispatchCommand(module, interactionArg(event));
}
}

View File

@@ -1,92 +0,0 @@
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of, pipe } from 'rxjs';
import { type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import type { CommandModule, TextCommand } from '../../types/module';
import { ErrorHandling, handleError } from '../contracts/errorHandling';
import { contextArgs, dispatchCommand } from './dispatchers';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg
* @param prefix The prefix to remove
* @returns The message without the prefix
* @example
* message.content = '!ping';
* console.log(fmt(message, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: string, prefix: string): string[] {
return msg.slice(prefix.length).trim().split(/\s+/g);
}
/**
* An operator function that processes a message to fetch a command module and prepares context payload.
* @param defaultPrefix
* @param get
*/
const createMessageProcessor = (
defaultPrefix: string,
get: (
cb: (ms: ModuleStore) => Processed<CommandModule> | undefined,
) => CommandModule | undefined,
) =>
pipe(
ignoreNonBot(defaultPrefix),
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
const [prefix, ...rest] = fmt(message.content, defaultPrefix);
const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
if (module === undefined) {
return EMPTY;
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
}),
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
);
export function makeMessageCreate(
[s, client, err, log, modules]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
],
defaultPrefix?: string,
) {
if (!defaultPrefix) {
return EMPTY.subscribe();
}
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
const messageStream$ = fromEvent(client, 'messageCreate') as Observable<Message>;
const messageProcessor = createMessageProcessor(defaultPrefix, get);
return messageStream$
.pipe(
messageProcessor,
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(s, payload)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'messageCreate stream closed or reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
.subscribe();
}

View File

@@ -1,121 +0,0 @@
import type { Awaitable, Message } from 'discord.js';
import { concatMap, EMPTY, filter, from, Observable, of, tap, throwError } from 'rxjs';
import { Result } from 'ts-results-es';
import type { CommandModule, EventModule, Module } from '../../types/module';
import SernEmitter from '../sernEmitter';
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
import type { ImportPayload, Processed } from '../../types/handler';
import type { ControlPlugin, VoidResult } from '../../types/plugin';
function hasPrefix(prefix: string, content: string) {
const prefixInContent = content.slice(0, prefix.length);
return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
}
/**
* Ignores messages from any person / bot except itself
* @param prefix
*/
export function ignoreNonBot(prefix: string) {
const messageFromHumanAndHasPrefix = ({ author, content }: Message) =>
!author.bot && hasPrefix(prefix, content);
return filter(messageFromHumanAndHasPrefix);
}
/**
* Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty
* if the task is an error, throw an error down the stream which will be handled by catchError
* @param emitter reference to SernEmitter that will emit a successful execution of module
* @param module the module that will be executed with task
* @param task the deferred execution which will be called
*/
export function executeModule(
emitter: SernEmitter,
{
module,
task,
}: {
module: Processed<Module>;
task: () => Awaitable<unknown>;
},
) {
return of(module).pipe(
//converting the task into a promise so rxjs can resolve the Awaitable properly
concatMap(() => Result.wrapAsync(async () => task())),
concatMap(result => {
if (result.ok) {
emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY;
} else {
return throwError(() => SernEmitter.failure(module, result.val));
}
}),
);
}
/**
* A higher order function that
* - creates a stream of {@link VoidResult} { config.createStream }
* - any failures results to { config.onFailure } being called
* - if all results are ok, the stream is converted to { config.onSuccess }
* emit config.onSuccess Observable
* @param config
* @returns receiver function for flattening a stream of data
*/
export function createResultResolver<
T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] },
Args extends { module: T; [key: string]: unknown },
Output,
>(config: {
onStop?: (module: T) => unknown;
onNext: (args: Args) => Output;
createStream: (args: Args) => Observable<VoidResult>;
}) {
return (args: Args) => {
const task$ = config.createStream(args);
return task$.pipe(
tap(result => {
if (result.err) {
config.onStop?.(args.module);
}
}),
everyPluginOk,
filterMapTo(() => config.onNext(args)),
);
};
}
/**
* Calls a module's init plugins and checks for Err. If so, call { onStop } and
* ignore the module
*/
export function callInitPlugins<
T extends Processed<CommandModule | EventModule>,
Args extends ImportPayload<T>,
>(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) {
return concatMap(
createResultResolver({
createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
...config,
}),
);
}
/**
* Creates an executable task ( execute the command ) if all control plugins are successful
* @param onStop emits a failure response to the SernEmitter
*/
export function makeModuleExecutor<
M extends Processed<Module>,
Args extends { module: M; args: unknown[] },
>(onStop: (m: M) => unknown) {
const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return concatMap(
createResultResolver({
onStop,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
onNext,
}),
);
}

View File

@@ -1,82 +0,0 @@
/**
* This file holds sern's rxjs operators used for processing data.
* Each function should be modular and testable, not bound to discord / sern
* and independent of each other
*/
import { concatMap, defaultIfEmpty, EMPTY, every, map, of, OperatorFunction, pipe } from 'rxjs';
import type { AnyModule } from '../../../types/module';
import { nameOrFilename } from '../../utilities/functions';
import type { PluginResult, VoidResult } from '../../../types/plugin';
import { guayin } from '../../plugins';
import { controller } from '../../sern';
import { Result } from 'ts-results-es';
import { ImportPayload, Processed } from '../../../types/handler';
/**
* if {src} is true, mapTo V, else ignore
* @param item
*/
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
/**
* Calls any plugin with {args}.
* @param args if an array, its spread and plugin called.
*/
export function callPlugin(args: unknown): OperatorFunction<
{
execute: (...args: unknown[]) => PluginResult;
},
VoidResult
> {
return concatMap(async plugin => {
const isNewPlugin = Reflect.has(plugin, guayin);
if (isNewPlugin) {
if (Array.isArray(args)) {
return plugin.execute(...args);
}
return plugin.execute(args);
} else {
return plugin.execute(args, controller);
}
});
}
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
export const fillDefaults = <T extends AnyModule>({ module, absPath }: ImportPayload<T>) => {
return {
absPath,
module: {
name: nameOrFilename(module?.name, absPath),
description: module?.description ?? '...',
...module,
},
};
};
/**
* If the current value in Result stream is an error, calls callback.
* This also extracts the Ok value from Result
* @param cb
* @returns Observable<{ module: T; absPath: string }>
*/
export function errTap<Ok, Err>(cb: (err: Err) => void): OperatorFunction<Result<Ok, Err>, Ok> {
return concatMap(result => {
if (result.ok) {
return of(result.val);
} else {
cb(result.val as Err);
return EMPTY;
}
});
}
/**
* Checks if the stream of results is all ok.
*/
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
every(result => result.ok),
defaultIfEmpty(true),
);

View File

@@ -1,106 +0,0 @@
import { fromEvent, map, pipe, switchMap, take } from 'rxjs';
import * as Files from '../module-loading/readFile';
import { callInitPlugins } from './observableHandling';
import { CommandType, type ModuleStore, SernError } from '../structures';
import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
import { err, ok } from '../utilities/functions';
import { errTap, fillDefaults } from './operators';
import SernEmitter from '../sernEmitter';
import type { EventEmitter } from 'node:events';
function buildCommandModules(commandDir: string, sernEmitter: SernEmitter) {
return pipe(
switchMap(() => Files.buildModuleStream<CommandModule>(commandDir)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
map(fillDefaults),
);
}
export function makeReadyEvent(
[sEmitter, client, errorHandler, , moduleManager]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
],
commandDir: string,
) {
const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
return readyOnce$
.pipe(
buildCommandModules(commandDir, sEmitter),
callInitPlugins({
onStop: module => {
sEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onNext: ({ module }) => {
sEmitter.emit('module.register', SernEmitter.success(module));
return module;
},
}),
)
.subscribe(module => {
const result = registerModule(moduleManager, module as Processed<CommandModule>);
if (result.err) {
errorHandler.crash(Error(SernError.InvalidModuleType));
}
});
}
function registerModule<T extends Processed<CommandModule>>(
manager: ModuleManager,
mod: T,
): Result<void, void> {
const name = mod.name;
const insert = (cb: (ms: ModuleStore) => void) => {
const set = Result.wrap(() => manager.set(cb));
return set.ok ? ok() : err();
};
switch (mod.type) {
case CommandType.Text: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));
}
case CommandType.Slash:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod),
);
case CommandType.Both: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.BothCommands.set(name, mod));
}
case CommandType.CtxUser:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
case CommandType.CtxMsg:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod),
);
case CommandType.Button:
return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
case CommandType.StringSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
case CommandType.MentionableSelect:
return insert(ms =>
ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod),
);
case CommandType.UserSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
case CommandType.ChannelSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
case CommandType.RoleSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
case CommandType.Modal:
return insert(ms => ms.ModalSubmit.set(name, mod));
default:
return err();
}
}

View File

@@ -1,74 +0,0 @@
import { catchError, finalize, map, mergeAll } from 'rxjs';
import * as Files from '../module-loading/readFile';
import type { Dependencies, Processed } from '../../types/handler';
import { callInitPlugins } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module';
import type { EventEmitter } from 'events';
import SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging } from '../contracts';
import { SernError, EventType, type Wrapper } from '../structures';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../contracts/errorHandling';
import { errTap, fillDefaults } from './operators';
import { useContainerRaw } from '../dependencies';
export function makeEventsHandler(
[s, client, err, log]: [SernEmitter, EventEmitter, ErrorHandling, Logging | undefined],
eventsPath: string,
containerGetter: Wrapper['containerConfig'],
) {
const lazy = (k: string) => containerGetter.get(k as keyof Dependencies)[0];
const eventStream$ = eventObservable(eventsPath, s);
const eventCreation$ = eventStream$.pipe(
map(fillDefaults),
callInitPlugins({
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
);
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch (e.type) {
case EventType.Sern:
return eventDispatcher(e, s);
case EventType.Discord:
return eventDispatcher(e, client);
case EventType.External:
return eventDispatcher(e, lazy(e.emitter));
default:
return err.crash(
Error(SernError.InvalidModuleType + ' while creating event handler'),
);
}
};
eventCreation$
.pipe(
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
log?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
function eventObservable(events: string, emitter: SernEmitter) {
return Files.buildModuleStream<EventModule>(events).pipe(
errTap(reason => {
emitter.emit('module.register', SernEmitter.failure(undefined, reason));
}),
);
}

View File

@@ -1,64 +0,0 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, mergeMap } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { ImportPayload } from '../../types/handler';
import { pathToFileURL } from 'node:url';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
}
return arrayOfFiles;
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
export async function defaultModuleLoader<T>(
absPath: string,
): Promise<Result<ImportPayload<T>, SernError>> {
// prettier-ignore
let module: T | undefined
/// #if MODE === 'esm'
= (await import(pathToFileURL(absPath).toString())).default
/// #elif MODE === 'cjs'
= require(absPath).default; // eslint-disable-line
/// #endif
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}
/**
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildModuleStream<T>(
commandDir: string,
): Observable<Result<ImportPayload<T>, SernError>> {
const commands = getCommands(commandDir);
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
}
export function fullPathFrom(dir: string) {
return join(process.cwd(), dir);
}
export function getCommands(dir: string): string[] {
return readPath(fullPathFrom(dir));
}

View File

@@ -1,117 +0,0 @@
import type { CommandType } from '../structures/enums';
import type { PluginType } from '../structures/enums';
import type { ClientEvents } from 'discord.js';
import type {
BothCommand,
ButtonCommand,
ChannelSelectCommand,
ContextMenuUser,
DiscordEventCommand,
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
RoleSelectCommand,
SernEventCommand,
SlashCommand,
StringSelectCommand,
TextCommand,
UserSelectCommand,
ContextMenuMsg,
Module,
} from '../../types/module';
import type { Args, Payload, Processed, SlashOptions } from '../../types/handler';
import type Context from '../structures/context';
import type { MessageContextMenuCommandInteraction } from 'discord.js';
import type {
ButtonInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
ModalSubmitInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { EventType } from '../structures/enums';
type CommandArgsMatrix = {
[CommandType.Text]: {
[PluginType.Control]: [Context, ['text', string[]]];
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
};
[CommandType.Slash]: {
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
};
[CommandType.Both]: {
[PluginType.Control]: [Context, Args];
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
};
[CommandType.CtxMsg]: {
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
};
[CommandType.CtxUser]: {
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
};
[CommandType.Button]: {
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
};
[CommandType.StringSelect]: {
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
};
[CommandType.RoleSelect]: {
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
};
[CommandType.ChannelSelect]: {
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
};
[CommandType.MentionableSelect]: {
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
};
[CommandType.UserSelect]: {
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
};
[CommandType.Modal]: {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
};
};
type EventArgsMatrix = {
[EventType.Discord]: {
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
};
[EventType.Sern]: {
[PluginType.Control]: [Payload];
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
};
[EventType.External]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
};
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
export type CommandArgs<
I extends CommandType = CommandType,
J extends PluginType = PluginType,
> = CommandArgsMatrix[I][J];
export type EventArgs<
I extends EventType = EventType,
J extends PluginType = PluginType,
> = EventArgsMatrix[I][J];

View File

@@ -1,64 +0,0 @@
import { CommandType, EventType, PluginType } from '../structures';
import type { Plugin, PluginResult } from '../../types/plugin';
import type { CommandArgs, EventArgs } from './args';
import type { ClientEvents } from 'discord.js';
export const guayin = Symbol('twice<3');
export function makePlugin<V extends unknown[]>(
type: PluginType,
execute: (...args: any[]) => any,
): Plugin<V> {
return {
type,
execute,
[guayin]: undefined,
} as Plugin<V>;
}
/**
* @since 2.5.0
*
*/
export function EventInitPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
*
*/
export function EventControlPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
* @Experimental
* A specialized function for creating control plugins with discord.js ClientEvents.
* Will probably be moved one day!
*/
export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
name: T,
execute: (...args: ClientEvents[T]) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}

View File

@@ -1,2 +0,0 @@
export { EventArgs, InitArgs, CommandArgs } from './args';
export * from './createPlugin';

View File

@@ -1,138 +0,0 @@
import type Wrapper from './structures/wrapper';
import { makeEventsHandler } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums';
import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
import { makeInteractionCreate } from './events/interactionHandler';
import { makeReadyEvent } from './events/readyHandler';
import { makeMessageCreate } from './events/messageHandler';
import type {
CommandModule,
CommandModuleDefs,
EventModule,
EventModuleDefs,
InputCommand,
InputEvent,
} from '../types/module';
import type { Dependencies, DependencyConfiguration } from '../types/handler';
import { composeRoot, makeFetcher, useContainer } from './dependencies/provider';
import type { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js';
/**
* @since 1.0.0
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
* ```ts title="src/index.ts"
* Sern.init({
* defaultPrefix: '!',
* commands: 'dist/commands',
* events: 'dist/events',
* containerConfig : {
* get: useContainer
* }
* })
* ```
*/
export function init(wrapper: Wrapper) {
const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined;
const requiredDependenciesAnd = makeFetcher(wrapper);
const startTime = performance.now();
const { events } = wrapper;
if (events !== undefined) {
makeEventsHandler(requiredDependenciesAnd([]), events, wrapper.containerConfig);
}
const dependencies = requiredDependenciesAnd(['@sern/modules']);
makeReadyEvent(dependencies, wrapper.commands);
makeMessageCreate(dependencies, wrapper.defaultPrefix);
makeInteractionCreate(dependencies);
const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: ok,
stop: err,
};
/**
* @since 1.0.0
* The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
plugins,
} as CommandModule;
}
/**
* @since 1.0.0
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEvent): EventModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
plugins,
} as EventModule;
}
/**
* Create event modules from discord.js client events,
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
plugins?: AnyEventPlugin[];
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod });
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Until there are more optional dependencies, just check if the logger exists
composeRoot(conf);
return useContainer<T>();
}
/**
* @Experimental
* Will be refactored / changed in future
*/
export abstract class CommandExecutable<Type extends CommandType> {
abstract type: Type;
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: CommandModuleDefs[Type]['execute'];
}
/**
* @Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: EventModuleDefs[Type]['execute'];
}

View File

@@ -1,88 +0,0 @@
import { EventEmitter } from 'events';
import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from './structures';
import type { Module } from '../types/module';
/**
* @since 1.0.0
*/
class SernEmitter extends EventEmitter {
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param listener what to do with the data
*/
public override on<T extends keyof SernEventsMapping>(
eventName: T,
listener: (...args: SernEventsMapping[T][]) => void,
): this {
return super.on(eventName, listener);
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param listener what to do with the data
*/
public override once<T extends keyof SernEventsMapping>(
eventName: T,
listener: (...args: SernEventsMapping[T][]) => void,
): this {
return super.once(eventName, listener);
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param args the arguments for emitting the eventName
*/
public override emit<T extends keyof SernEventsMapping>(
eventName: T,
...args: SernEventsMapping[T]
): boolean {
return super.emit(eventName, ...args);
}
private static payload<T extends Payload>(
type: PayloadType,
module?: Module,
reason?: unknown,
) {
return { type, module, reason } as T;
}
/**
* Creates a compliant SernEmitter failure payload
* @param module
* @param reason
*/
static failure(module?: Module, reason?: unknown) {
//The generic cast Payload & { type : PayloadType.* } coerces the type to be a failure payload
// same goes to the other methods below
return SernEmitter.payload<Payload & { type: PayloadType.Failure }>(
PayloadType.Failure,
module,
reason,
);
}
/**
* Creates a compliant SernEmitter module success payload
* @param module
*/
static success(module: Module) {
return SernEmitter.payload<Payload & { type: PayloadType.Success }>(
PayloadType.Success,
module,
);
}
/**
* Creates a compliant SernEmitter module warning payload
* @param reason
*/
static warning(reason: unknown) {
return SernEmitter.payload<Payload & { type: PayloadType.Warning }>(
PayloadType.Warning,
undefined,
reason,
);
}
}
export default SernEmitter;

View File

@@ -1,107 +0,0 @@
import type {
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
Snowflake,
MessageReplyOptions,
User,
} from 'discord.js';
import { Result as Either, Ok as Left, Err as Right } from 'ts-results-es';
import type { ReplyOptions } from '../../types/handler';
import { SernError } from './errors';
function safeUnwrap<T>(res: Either<T, T>) {
return res.val;
}
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export default class Context {
private constructor(private ctx: Either<Message, ChatInputCommandInteraction>) {}
/**
* Getting the Message object. Crashes if module type is
* CommandType.Slash or the event fired in a Both command was
* ChatInputCommandInteraction
*/
public get message() {
return this.ctx.expect(SernError.MismatchEvent);
}
/**
* Getting the ChatInputCommandInteraction object. Crashes if module type is
* CommandType.Text or the event fired in a Both command was
* Message
*/
public get interaction() {
return this.ctx.expectErr(SernError.MismatchEvent);
}
public get id(): Snowflake {
return this.ctx.val.id;
}
public get channel() {
return this.ctx.val.channel;
}
/**
* If context is holding a message, message.author
* else, interaction.user
*/
public get user(): User {
return safeUnwrap(this.ctx.map(m => m.author).mapErr(i => i.user));
}
public get createdTimestamp(): number {
return this.ctx.val.createdTimestamp;
}
public get guild() {
return this.ctx.val.guild;
}
public get guildId() {
return this.ctx.val.guildId;
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return this.ctx.val.member;
}
public get client(): Client {
return this.ctx.val.client;
}
public get inGuild(): boolean {
return this.ctx.val.inGuild();
}
public isMessage() {
return this.ctx.map(() => true).unwrapOr(false);
}
public isSlash() {
return !this.isMessage();
}
static wrap(wrappable: ChatInputCommandInteraction | Message): Context {
if ('token' in wrappable) {
return new Context(Right(wrappable));
}
return new Context(Left(wrappable));
}
public reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
.map(m => m.reply(content as string | MessageReplyOptions))
.mapErr(i =>
i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()),
),
);
}
}

View File

@@ -1,38 +0,0 @@
/**
* @enum { string }
*/
export enum SernError {
/**
* Throws when registering an invalid module.
* This means it is undefined or an invalid command type was provided
*/
InvalidModuleType = 'Detected an unknown module type',
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
UndefinedModule = `A module could not be detected`,
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
MismatchModule = `A module type mismatched with event emitted!`,
/**
* Unsupported interaction at this moment.
*/
NotSupportedInteraction = `This interaction is not supported.`,
/**
* One plugin called `controller.stop()` (end command execution / loading)
*/
PluginFailure = `A plugin failed to call controller.next()`,
/**
* A crash that occurs when accessing an invalid property of Context
*/
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
/**
* Unsupported feature attempted to access at this time
*/
NotSupportedYet = `This feature is not supported yet`,
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -1,6 +0,0 @@
import Context from './context';
import type Wrapper from './wrapper';
import { ModuleStore } from './moduleStore';
export * from './errors';
export * from './enums';
export { Context, Wrapper, ModuleStore };

View File

@@ -1,27 +0,0 @@
import type { CommandModule } from '../../types/module';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
* Storing all command modules
* This dependency is usually injected into ModuleManager
*/
export class ModuleStore {
readonly BothCommands = new Map<string, Processed<CommandModule>>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.Message]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.ChatInput]: new Map<string, Processed<CommandModule>>(),
};
readonly ModalSubmit = new Map<string, Processed<CommandModule>>();
readonly TextCommands = new Map<string, Processed<CommandModule>>();
readonly InteractionHandlers = {
[ComponentType.Button]: new Map<string, Processed<CommandModule>>(),
[ComponentType.StringSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.ChannelSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.MentionableSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.RoleSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.UserSelect]: new Map<string, Processed<CommandModule>>(),
};
}

View File

@@ -1,20 +0,0 @@
import type { Dependencies } from '../../types/handler';
/**
* @since 1.0.0
* An object to be passed into Sern#init() function.
* @typedef {object} Wrapper
*/
interface Wrapper {
/**
* @deprecated
* This will be moved to a new field in 3.0.0
*/
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;
readonly containerConfig: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
}
export default Wrapper;

View File

@@ -1,36 +0,0 @@
import * as Files from '../module-loading/readFile';
import { basename } from 'path';
import { Err, Ok } from 'ts-results-es';
/**
* A function that returns whatever value is provided.
* Warning: this evaluates { @param value }. It does not defer a value.
* @param value
* @__PURE__
*/
// prettier-ignore
export const _const = <T>(value: T) => () => value;
/**
*
* @param modName
* @param absPath
*/
export function nameOrFilename(modName: string | undefined, absPath: string) {
return modName ?? Files.fmtFileName(basename(absPath));
}
//function wrappers for empty ok / err
export const ok = _const(Ok.EMPTY);
export const err = _const(Err.EMPTY);
export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
const t: T[] = [];
const v: V[] = [];
for (const el of arr) {
if (condition(el)) {
t.push(el as T);
} else {
v.push(el as V);
}
}
return [t, v];
}

View File

@@ -1,54 +0,0 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
import assert from 'assert';
/**
* Uses an iterative DFS to check if an autocomplete node exists
* @param iAutocomplete
* @param options
*/
export default function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({...a}));
let subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand:
{
subcommands.add(cur.name);
for (const option of cur.options ?? [])
_options.push(option);
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
for (const command of cur.options ?? [])
_options.push(command);
}
break;
default:
{
if ('autocomplete' in cur && cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
assert('command' in cur, "No command property found for autocomplete option");
if(subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches = subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return cur;
}
} else {
if(cur.name === choice.name) {
return cur;
}
}
}
}
break;
}
}}

270
src/handlers/event-utils.ts Normal file
View File

@@ -0,0 +1,270 @@
import type { Interaction, Message, BaseInteraction } from 'discord.js';
import util from 'node:util';
import {
EMPTY, type Observable, concatMap, filter,
throwError, fromEvent, map, type OperatorFunction,
catchError, finalize, pipe, from, take, share, of,
} from 'rxjs';
import * as Id from '../core/id'
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
import { SernError } from '../core/structures/enums'
import { Err, Ok, Result } from 'ts-results-es';
import type { UnpackedDependencies } from '../types/utility';
import type { CommandModule, Module, Processed } from '../types/core-modules';
import * as assert from 'node:assert';
import { Context } from '../core/structures/context';
import { CommandType } from '../core/structures/enums'
import { inspect } from 'node:util'
import { disposeAll } from '../core/ioc';
import { resultPayload, isAutocomplete, treeSearch, fmt } from '../core/functions'
import merge from 'deepmerge'
function handleError<C>(crashHandler: ErrorHandling, emitter: Emitter, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
if(!emitter.emit('error', pload)) {
const err = pload instanceof Error ? pload : Error(util.inspect(pload, { colors: true }));
logging?.error({ message: util.inspect(pload) });
crashHandler.updateAlive(err);
}
return caught;
};
}
const arrayify= <T>(src: T) =>
Array.isArray(src) ? src : [src];
interface ExecutePayload {
module: Module;
args: unknown[];
deps: Dependencies;
params?: string;
[key: string]: unknown
}
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
concatMap(result => {
if(result.isOk()) {
return of(result.value)
}
onErr(result.error);
return EMPTY;
})
export const sharedEventStream = <T>(e: Emitter, eventName: string) =>
(fromEvent(e, eventName) as Observable<T>).pipe(share());
function intoPayload(module: Module, deps: Dependencies) {
return pipe(map(arrayify),
map(args => ({ module, args, deps })),
map(p => p.args));
}
/**
* Creates an observable from { source }
* @param module
* @param source
*/
export function eventDispatcher(deps: Dependencies, module: Module, source: unknown) {
assert.ok(source && typeof source === 'object',
`${source} cannot be constructed into an event listener`);
const execute: OperatorFunction<unknown[]|undefined, unknown> =
concatMap(async args => {
if(args) return Reflect.apply(module.execute, null, args);
});
//@ts-ignore
let ev = fromEvent(source ,module.name!);
//@ts-ignore
if(module['once']) {
ev = ev.pipe(take(1))
}
return ev.pipe(intoPayload(module, deps),
execute);
}
interface DispatchPayload {
module: Processed<CommandModule>;
event: BaseInteraction;
defaultPrefix?: string;
deps: Dependencies;
params?: string
};
export function createDispatcher({ module, event, defaultPrefix, deps, params }: DispatchPayload): ExecutePayload {
assert.ok(CommandType.Text !== module.type,
SernError.MismatchEvent + 'Found text command in interaction stream');
if(isAutocomplete(event)) {
assert.ok(module.type === CommandType.Slash
|| module.type === CommandType.Both, "Autocomplete option on non command interaction");
const option = treeSearch(event, module.options);
assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(module));
const { command } = option;
return { module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [event],
deps };
}
switch (module.type) {
case CommandType.Slash:
case CommandType.Both: {
return { module, args: [Context.wrap(event, defaultPrefix)], deps };
}
default: return { module, args: [event], deps, params };
}
}
function createGenericHandler<Source, Narrowed extends Source, Output>(
source: Observable<Source>,
makeModule: (event: Narrowed) => Promise<Output>,
) {
return (pred: (i: Source) => i is Narrowed) =>
source.pipe(
filter(pred), // only handle this stream if it passes pred
concatMap(makeModule)); // create a payload, preparing to execute
}
/**
*
* Creates an RxJS observable that filters and maps incoming interactions to their respective modules.
* @param i An RxJS observable of interactions.
* @param mg The module manager instance used to retrieve the module path for each interaction.
* @returns A handler to create a RxJS observable of dispatchers that take incoming interactions and execute their corresponding modules.
*/
export function createInteractionHandler<T extends Interaction>(
source: Observable<Interaction>,
deps: Dependencies,
defaultPrefix?: string
) {
const mg = deps['@sern/modules'];
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
source,
async event => {
const possibleIds = Id.reconstruct(event);
let modules = possibleIds
.map(({ id, params }) => ({ module: mg.get(id), params }))
.filter(({ module }) => module !== undefined);
if(modules.length == 0) {
return Err.EMPTY;
}
const [{module, params}] = modules;
return Ok(createDispatcher({
module: module as Processed<CommandModule>,
event, defaultPrefix, deps, params
}));
});
}
export function createMessageHandler(
source: Observable<Message>,
defaultPrefix: string,
deps: Dependencies,
) {
const mg = deps['@sern/modules'];
return createGenericHandler(source, async event => {
const [prefix] = fmt(event.content, defaultPrefix);
let module= mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
if(!module) {
return Err('Possibly undefined behavior: could not find a static id to resolve');
}
return Ok({ args: [Context.wrap(event, defaultPrefix)], module, deps })
});
}
/**
* Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty
* if the task is an error, throw an error down the stream which will be handled by catchError
* thank u kingomes
* @param emitter reference to SernEmitter that will emit a successful execution of module
* @param module the module that will be executed with task
* @param task the deferred execution which will be called
*/
export function executeModule(emitter: Emitter, { module, args }: ExecutePayload) {
return from(Result.wrapAsync(async () => module.execute(...args)))
.pipe(concatMap(result => {
if (result.isOk()) {
emitter.emit('module.activate', resultPayload('success', module));
return EMPTY;
}
return throwError(() => resultPayload('failure', module, result.error));
}))
};
/**
* A higher order function that
* - calls all control plugins.
* - any failures results to { config.onStop } being called
* - if all results are ok, the stream is converted to { config.onNext }
* config.onNext will be returned if everything is okay.
* @param config
* @returns function which calls all plugins and returns onNext or fail
*/
export function createResultResolver<Output>(config: {
onStop?: (module: Module, err?: string) => unknown;
onNext: (args: ExecutePayload, map: Record<string, unknown>) => Output;
}) {
const { onStop, onNext } = config;
return async (payload: ExecutePayload) => {
const task = await callPlugins(payload);
if (!task) throw Error("Plugin did not return anything.");
if(task.isOk()) {
return onNext(payload, task.value) as Output;
} else {
onStop?.(payload.module, String(task.error));
}
};
};
function isObject(item: unknown) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
//_module is frozen, preventing from mutations
export async function callInitPlugins(_module: Module, deps: Dependencies, emit?: boolean) {
let module = _module;
const emitter = deps['@sern/emitter'];
for(const plugin of module.plugins ?? []) {
const result = await plugin.execute({ module, absPath: module.meta.absPath, deps });
if (!result) throw Error("Plugin did not return anything. " + inspect(plugin, false, Infinity, true));
if(result.isErr()) {
if(emit) {
emitter?.emit('module.register',
resultPayload('failure', module, result.error ?? SernError.PluginFailure));
}
throw Error(result.error ?? SernError.PluginFailure);
}
}
return module
}
export async function callPlugins({ args, module, deps, params }: ExecutePayload) {
let state = {};
for(const plugin of module.onEvent??[]) {
const result = await plugin.execute(...args, { state, deps, params, type: module.type });
if(result.isErr()) {
return result;
}
if(isObject(result.value)) {
state = merge(state, result.value!);
}
}
return Ok(state);
}
/**
* Creates an executable task ( execute the command ) if all control plugins are successful
* @param onStop emits a failure response to the SernEmitter
*/
export function intoTask(onStop: (m: Module) => unknown) {
const onNext = ({ args, module, deps, params }: ExecutePayload, state: Record<string, unknown>) => ({
module,
args: [...args, { state, deps, params, type: module.type }],
deps
});
return createResultResolver({ onStop, onNext });
}
export const handleCrash = ({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies, metadata: string) =>
pipe(catchError(handleError(err, sem, log)),
finalize(() => {
log?.info({ message: 'A stream closed: ' + metadata });
disposeAll(log);
}))

View File

@@ -0,0 +1,30 @@
import type { Interaction } from 'discord.js';
import { mergeMap, merge, concatMap, EMPTY } from 'rxjs';
import { createInteractionHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash } from './event-utils';
import { SernError } from '../core/structures/enums'
import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
import { UnpackedDependencies } from '../types/utility';
export default function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) {
//i wish javascript had clojure destructuring
const { '@sern/client': client,
'@sern/emitter': emitter } = deps
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler(interactionStream$, deps, defaultPrefix);
const interactionHandler$ = merge(handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal));
return interactionHandler$
.pipe(filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))),
concatMap(intoTask(module => {
emitter.emit('module.activate', resultPayload('failure', module, SernError.PluginFailure))
})),
mergeMap(payload => {
if(payload)
return executeModule(emitter, payload)
return EMPTY;
}),
handleCrash(deps, "interaction handling"));
}

50
src/handlers/message.ts Normal file
View File

@@ -0,0 +1,50 @@
import { EMPTY, mergeMap, concatMap } from 'rxjs';
import type { Message } from 'discord.js';
import { createMessageHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash} from './event-utils';
import { SernError } from '../core/structures/enums'
import { resultPayload } from '../core/functions'
import { UnpackedDependencies } from '../types/utility';
import type { Emitter } from '../core/interfaces';
/**
* Ignores messages from any person / bot except itself
* @param prefix
*/
function isNonBot(prefix: string) {
return (msg: Message): msg is Message => !msg.author.bot && hasPrefix(prefix, msg.content);
}
function hasPrefix(prefix: string, content: string) {
const prefixInContent = content.slice(0, prefix.length);
return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
}
export default
function (deps: UnpackedDependencies, defaultPrefix?: string) {
const {"@sern/emitter": emitter,
'@sern/logger': log,
'@sern/client': client} = deps
if (!defaultPrefix) {
log?.debug({ message: 'No prefix found. message handler shutting down' });
return EMPTY;
}
const messageStream$ = sharedEventStream<Message>(client as unknown as Emitter, 'messageCreate');
const handle = createMessageHandler(messageStream$, defaultPrefix, deps);
const msgCommands$ = handle(isNonBot(defaultPrefix));
return msgCommands$.pipe(
filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))),
concatMap(intoTask(module => {
const result = resultPayload('failure', module, SernError.PluginFailure);
emitter.emit('module.activate', result);
})),
mergeMap(payload => {
if(payload)
return executeModule(emitter, payload)
return EMPTY;
}),
handleCrash(deps, "message handling")
)
}

40
src/handlers/presence.ts Normal file
View File

@@ -0,0 +1,40 @@
import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take } from "rxjs"
import { Presence } from "../core/presences";
import { Services } from "../core/ioc";
import assert from "node:assert";
import * as Files from "../core/module-loading";
type SetPresence = (conf: Presence.Result) => Promise<unknown>
const parseConfig = async (conf: Promise<Presence.Result>) => {
return conf.then(s => {
if('repeat' in s) {
const { onRepeat, repeat } = s;
assert(repeat !== undefined, "repeat option is undefined");
assert(onRepeat !== undefined, "onRepeat callback is undefined, but repeat exists");
const src$ = typeof repeat === 'number'
? interval(repeat)
: fromEvent(...repeat);
return src$.pipe(scan(onRepeat, s),
startWith(s));
}
return of(s).pipe(take(1));
})
};
export const presenceHandler = (path: string, setPresence: SetPresence) => {
const presence = Files
.importModule<Presence.Config<(keyof Dependencies)[]>>(path)
.then(({ module }) => {
//fetch services with the order preserved, passing it to the execute fn
const fetchedServices = Services(...module.inject ?? []);
return async () => module.execute(...fetchedServices);
})
const module$ = from(presence);
return module$.pipe(
//compose:
//call the execute function, passing that result into parseConfig.
//concatMap resolves the promise, and passes it to the next concatMap.
concatMap(fn => parseConfig(fn())),
// subscribe to the observable parseConfig yields, and set the presence.
concatMap(conf => conf.pipe(map(setPresence))));
}

32
src/handlers/ready.ts Normal file
View File

@@ -0,0 +1,32 @@
import * as Files from '../core/module-loading'
import { once } from 'node:events';
import { resultPayload } from '../core/functions';
import { CommandType } from '../core/structures/enums';
import { Module } from '../types/core-modules';
import { UnpackedDependencies } from '../types/utility';
import { callInitPlugins } from './event-utils';
export default async function(dir: string, deps : UnpackedDependencies) {
const { '@sern/client': client,
'@sern/logger': log,
'@sern/emitter': sEmitter,
'@sern/modules': commands } = deps;
log?.info({ message: "Waiting on discord client to be ready..." })
await once(client, "ready");
log?.info({ message: "Client signaled ready, registering modules" });
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
// possibly optimize to concurrently import modules
for await (const path of Files.readRecursive(dir)) {
let { module } = await Files.importModule<Module>(path);
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect;
if(!validType) {
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
}
const resultModule = await callInitPlugins(module, deps, true);
// FREEZE! no more writing!!
commands.set(resultModule.meta.id, Object.freeze(resultModule));
sEmitter.emit('module.register', resultPayload('success', resultModule));
}
sEmitter.emit('modulesLoaded');
}

16
src/handlers/tasks.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as Files from '../core/module-loading'
import { UnpackedDependencies } from "../types/utility";
import type { ScheduledTask } from "../types/core-modules";
import { relative } from "path";
import { fileURLToPath } from "url";
export const registerTasks = async (tasksPath: string, deps: UnpackedDependencies) => {
const taskManager = deps['@sern/scheduler']
for await (const f of Files.readRecursive(tasksPath)) {
let { module } = await Files.importModule<ScheduledTask>(f);
//module.name is assigned by Files.importModule<>
// the id created for the task is unique
const uuid = module.name+"/"+relative(tasksPath,fileURLToPath(f))
taskManager.schedule(uuid, module, deps)
}
}

View File

@@ -0,0 +1,33 @@
import { EventType, SernError } from '../core/structures/enums';
import { callInitPlugins, eventDispatcher, handleCrash } from './event-utils'
import { EventModule, Module } from '../types/core-modules';
import * as Files from '../core/module-loading'
import type { UnpackedDependencies } from '../types/utility';
import { from, map, mergeAll } from 'rxjs';
const intoDispatcher = (deps: UnpackedDependencies) =>
(module : EventModule) => {
switch (module.type) {
case EventType.Sern:
return eventDispatcher(deps, module, deps['@sern/emitter']);
case EventType.Discord:
return eventDispatcher(deps, module, deps['@sern/client']);
case EventType.External:
return eventDispatcher(deps, module, deps[module.emitter]);
default: throw Error(SernError.InvalidModuleType + ' while creating event handler');
}
};
export default async function(deps: UnpackedDependencies, eventDir: string) {
const eventModules: EventModule[] = [];
for await (const path of Files.readRecursive(eventDir)) {
let { module } = await Files.importModule<Module>(path);
await callInitPlugins(module, deps)
eventModules.push(module as EventModule);
}
from(eventModules)
.pipe(map(intoDispatcher(deps)),
mergeAll(), // all eventListeners are turned on
handleCrash(deps, "event modules"))
.subscribe();
}

View File

@@ -1,18 +1,72 @@
import SernEmitter from './handler/sernEmitter';
export * as Sern from './sern';
export type {
Module,
CommandModule,
EventModule,
BothCommand,
ContextMenuMsg,
ContextMenuUser,
SlashCommand,
TextCommand,
ButtonCommand,
StringSelectCommand,
MentionableSelectCommand,
UserSelectCommand,
ChannelSelectCommand,
RoleSelectCommand,
ModalSubmitCommand,
DiscordEventCommand,
SernEventCommand,
ExternalEventCommand,
CommandModuleDefs,
EventModuleDefs,
SernAutocompleteData,
SernOptionsData,
SernSubCommandData,
SernSubCommandGroupData,
SDT,
ScheduledTask
} from './types/core-modules';
export type {
PluginResult,
InitPlugin,
ControlPlugin,
Plugin,
AnyPlugin,
} from './types/core-plugin';
export type { Payload, SernEventsMapping } from './types/utility';
export {
eventModule,
commandModule,
EventExecutable,
CommandExecutable,
controller,
eventModule,
discordEvent,
} from './handler/sern';
export * as Sern from './handler/sern';
export * from './types/handler';
export * from './types/module';
export * from './types/plugin';
export * from './handler/structures';
export * from './handler/plugins';
export * from './handler/contracts';
export { SernEmitter };
export * from './handler/dependencies';
scheduledTask
} from './core/modules';
export * from './core/presences'
export * from './core/interfaces'
export * from './core/plugin';
export { CommandType, PluginType, PayloadType, EventType } from './core/structures/enums';
export { Context } from './core/structures/context';
export { type CoreDependencies, makeDependencies, single, transient, Service, Services } from './core/ioc';
import type { Container } from '@sern/ioc';
/**
* @deprecated This old signature will be incompatible with future versions of sern >= 4.0.0. See {@link makeDependencies}
* @example
* ```ts
* To switch your old code:
await makeDependencies(({ add }) => {
add('@sern/client', new Client())
})
* ```
*/
export interface DependencyConfiguration {
build: (root: Container) => Container;
}

71
src/sern.ts Normal file
View File

@@ -0,0 +1,71 @@
//side effect: global container
import { useContainerRaw } from '@sern/ioc/global';
import callsites from 'callsites';
import * as Files from './core/module-loading';
import { merge } from 'rxjs';
import eventsHandler from './handlers/user-defined-events';
import ready from './handlers/ready';
import messageHandler from './handlers/message';
import interactionHandler from './handlers/interaction';
import { presenceHandler } from './handlers/presence';
import { UnpackedDependencies } from './types/utility';
import type { Presence} from './core/presences';
import { registerTasks } from './handlers/tasks';
interface Wrapper {
commands: string;
defaultPrefix?: string;
events?: string;
tasks?: string;
}
/**
* @since 1.0.0
* @param maybeWrapper Options to pass into sern.
* Function to start the handler up
* @example
* ```ts title="src/index.ts"
* Sern.init({
* commands: 'dist/commands',
* events: 'dist/events',
* })
* ```
*/
export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
const startTime = performance.now();
const deps = useContainerRaw().deps<UnpackedDependencies>();
if (maybeWrapper.events !== undefined) {
eventsHandler(deps, maybeWrapper.events)
.then(() => {
deps['@sern/logger']?.info({ message: "Events registered" });
});
} else {
deps['@sern/logger']?.info({ message: "No events registered" });
}
const initCallsite = callsites()[1].getFileName();
const presencePath = Files.shouldHandle(initCallsite!, "presence");
//Ready event: load all modules and when finished, time should be taken and logged
ready(maybeWrapper.commands, deps)
.then(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
deps['@sern/logger']?.info({ message: `sern: registered in ${time} s` });
if(presencePath.exists) {
const setPresence = async (p: Presence.Result) => {
return deps['@sern/client'].user?.setPresence(p);
}
presenceHandler(presencePath.path, setPresence).subscribe();
}
if(maybeWrapper.tasks) {
registerTasks(maybeWrapper.tasks, deps);
}
})
.catch(err => { throw err });
const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix);
const interactions$ = interactionHandler(deps, maybeWrapper.defaultPrefix);
// listening to the message stream and interaction stream
merge(messages$, interactions$).subscribe();
}

View File

@@ -1,34 +1,35 @@
import type {
ApplicationCommandAttachmentOption,
ApplicationCommandChannelOptionData,
ApplicationCommandChoicesData,
ApplicationCommandNonOptionsData,
ApplicationCommandNumericOptionData,
ApplicationCommandOptionData,
APIApplicationCommandBasicOption,
APIApplicationCommandOptionBase,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
AutocompleteInteraction,
Awaitable,
BaseApplicationCommandOptionsData,
AutocompleteInteraction,
ButtonInteraction,
ChannelSelectMenuInteraction,
ClientEvents,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
ChatInputCommandInteraction,
} from 'discord.js';
import { CommandType } from '../handler/structures/enums';
import type { Args, SlashOptions } from './handler';
import type Context from '../handler/structures/context';
import type { InitPlugin, ControlPlugin } from './plugin';
import { EventType } from '../handler/structures/enums';
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
import type { SernEventsMapping } from './handler';
import type { ClientEvents } from 'discord.js';
import type { CommandType, EventType } from '../core/structures/enums';
import { Context } from '../core/structures/context'
import { ControlPlugin, InitPlugin, Plugin } from './core-plugin';
import { Awaitable, SernEventsMapping, UnpackedDependencies } from './utility';
//state, deps, type (very original)
export type SDT = {
state: Record<string,unknown>;
deps: Dependencies;
type: CommandType,
params?: string
};
export type Processed<T> = T & { name: string; description: string };
export interface Module {
type: CommandType | EventType;
@@ -36,79 +37,12 @@ export interface Module {
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
execute: (...args: any[]) => Awaitable<any>;
}
export interface TextCommand extends Module {
type: CommandType.Text;
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => Awaitable<unknown>;
}
export interface SlashCommand extends Module {
type: CommandType.Slash;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
alias?: string[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
execute: (ctx: ButtonInteraction) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction) => Awaitable<unknown>;
}
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description'> {
onEvent: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
meta: {
id: string;
absPath: string;
}
locals: Record<string,unknown>
execute(...args: any[]): Awaitable<any>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
@@ -118,21 +52,90 @@ export interface SernEventCommand<T extends keyof SernEventsMapping = keyof Sern
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: keyof Dependencies;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface AutocompleteCommand {
onEvent?: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
extends Module {
name?: T;
type: EventType.Discord;
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: string;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
export interface TextCommand extends Module {
type: CommandType.Text;
execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export interface SlashCommand extends Module {
type: CommandType.Slash;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, tbd: SDT) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
@@ -147,11 +150,9 @@ export type CommandModule =
| RoleSelectCommand
| ModalSubmitCommand;
export type AnyModule = CommandModule | EventModule;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export type CommandModuleDefs = {
export interface CommandModuleDefs {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
@@ -164,13 +165,13 @@ export type CommandModuleDefs = {
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
}
export type EventModuleDefs = {
export interface EventModuleDefs {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
}
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
@@ -182,47 +183,76 @@ export interface SernAutocompleteData
command: AutocompleteCommand;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent' | 'meta' | 'locals'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent' | 'meta' | 'locals'> ;
};
export type InputEvent = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: AnyEventPlugin[] };
[T in EventType]: EventModulesNoPlugins[T] & {
once?: boolean;
plugins?: InitPlugin[]
};
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
[T in CommandType]: CommandModuleNoPlugins[T] & {
plugins?: Plugin[];
};
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type BaseOptions =
| ApplicationCommandChoicesData
| ApplicationCommandNonOptionsData
| ApplicationCommandChannelOptionData
| ApplicationCommandNumericOptionData
| ApplicationCommandAttachmentOption
export type SernOptionsData =
| SernSubCommandData
| SernSubCommandGroupData
| APIApplicationCommandBasicOption
| SernAutocompleteData;
export interface SernSubCommandData extends BaseApplicationCommandOptionsData {
export interface SernSubCommandData
extends APIApplicationCommandOptionBase<ApplicationCommandOptionType.Subcommand> {
type: ApplicationCommandOptionType.Subcommand;
required?: never;
options?: BaseOptions[];
options?: SernOptionsData[];
}
export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.SubcommandGroup;
required?: never;
options?: SernSubCommandData[];
}
export type SernOptionsData<U extends ApplicationCommandOptionData = ApplicationCommandOptionData> =
U extends ApplicationCommandSubCommandData
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;
export interface ScheduledTaskContext {
/**
* the uuid of the current task being run
*/
id: string;
/**
* the last time this task was executed. If this is the first time, it is null.
*/
lastTimeExecution: Date | null;
/**
* The next time this task will be executed.
*/
nextTimeExecution: Date | null;
}
//name subject to change
interface TaskAttrs {
/**
* An object of dependencies configured in `makeDependencies`
*/
deps: UnpackedDependencies
}
export interface ScheduledTask {
name?: string;
trigger: string | Date;
timezone?: string;
execute(tasks: ScheduledTaskContext, sdt: TaskAttrs): Awaitable<void>
}

71
src/types/core-plugin.ts Normal file
View File

@@ -0,0 +1,71 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { Result } from 'ts-results-es';
import type {
Module,
Processed,
SDT,
} from './core-modules';
import type { Awaitable } from './utility';
import type { CommandType, PluginType } from '../core/structures/enums'
import type { Context } from '../core/structures/context'
import type {
ButtonInteraction,
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
export type PluginResult = Awaitable<Result<Record<string,unknown>|undefined, string|undefined>>;
export interface InitArgs<T extends Processed<Module> = Processed<Module>> {
module: T;
absPath: string;
deps: Dependencies
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
execute: (...args: Args) => PluginResult;
}
export interface InitPlugin<Args extends any[] = any[]> extends Plugin<Args> {
type: PluginType.Init;
execute: (...args: Args) => PluginResult;
}
export interface ControlPlugin<Args extends any[] = any[]> extends Plugin<Args> {
type: PluginType.Control;
}
export type AnyPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<Module>>]>;
export type CommandArgs<I extends CommandType = CommandType> = CommandArgsMatrix[I]
interface CommandArgsMatrix {
[CommandType.Text]: [Context, SDT];
[CommandType.Slash]: [Context, SDT];
[CommandType.Both]: [Context, SDT];
[CommandType.CtxMsg]: [MessageContextMenuCommandInteraction, SDT];
[CommandType.CtxUser]: [UserContextMenuCommandInteraction, SDT];
[CommandType.Button]: [ButtonInteraction, SDT];
[CommandType.StringSelect]: [StringSelectMenuInteraction, SDT];
[CommandType.RoleSelect]: [RoleSelectMenuInteraction, SDT];
[CommandType.ChannelSelect]: [ChannelSelectMenuInteraction, SDT];
[CommandType.MentionableSelect]: [MentionableSelectMenuInteraction, SDT];
[CommandType.UserSelect]: [UserSelectMenuInteraction, SDT];
[CommandType.Modal]: [ModalSubmitInteraction, SDT];
}

27
src/types/dependencies.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
// This file serves an the interface for developers to augment the Dependencies interface
// Developers will have to create a new file dependencies.d.ts in the root directory, augmenting
// this type
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { CoreDependencies } from '../core/ioc';
declare global {
/**
* discord.js client.
* '@sern/client': Client
* sern emitter listens to events that happen throughout
* the handler. some include module.register, module.activate.
* '@sern/emitter': Contracts.Emitter;
* An error handler which is the final step before
* the sern process actually crashes.
'@sern/errors': Contracts.ErrorHandling;
* Optional logger. Performs ... logging
* '@sern/logger'?: Contracts.Logging;
* Readonly module store. sern stores these
* by module.meta.id -> Module
* '@sern/modules': Map<string, Module>;
*/
interface Dependencies extends CoreDependencies {}
}

View File

@@ -1,71 +0,0 @@
import type { CommandInteractionOptionResolver } from 'discord.js';
import type { PayloadType } from '../handler/structures/enums';
import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { EventEmitter } from 'events';
import type { CommandModule, EventModule, AnyModule } from './module';
import type { UnpackFunction } from 'iti';
import type { ErrorHandling, Logging, ModuleManager } from '../handler/contracts';
import type { ModuleStore } from '../handler/structures/moduleStore';
import type SernEmitter from '../handler/sernEmitter';
import type { Container } from 'iti';
// Thanks to @kelsny
export type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
}[keyof T];
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
/**
* After modules are transformed, name and description are given default values if none
* are provided to Module. This type represents that transformation
*/
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
export type SernEventsMapping = {
'module.register': [Payload];
'module.activate': [Payload];
error: [Payload];
warning: [Payload];
};
export type LogPayload<T = unknown> = { message: T };
export type Singleton<T> = () => T;
export type Transient<T> = () => () => T;
export interface Dependencies {
'@sern/client': Singleton<EventEmitter>;
'@sern/logger'?: Singleton<Logging>;
'@sern/emitter': Singleton<SernEmitter>;
'@sern/store': Singleton<ModuleStore>;
'@sern/modules': Singleton<ModuleManager>;
'@sern/errors': Singleton<ErrorHandling>;
}
export type ReplyOptions =
| string
| Omit<InteractionReplyOptions, 'fetchReply'>
| MessageReplyOptions;
//prettier-ignore
export type MapDeps<Deps extends Dependencies, T extends readonly unknown[]> = T extends [
infer First extends keyof Deps,
...infer Rest extends readonly unknown[],
]
? [
UnpackFunction<Deps[First]>,
...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps, Rest>),
]
: [never];
//Basically, '@sern/client' | '@sern/store' | '@sern/modules' | '@sern/error' | '@sern/emitter' will be provided defaults, and you can exclude the rest
export type OptionalDependencies = '@sern/logger';
export type Processed<T> = T & { name: string; description: string };
export type Deprecated<Message extends string> = [never, Message];
export interface DependencyConfiguration<T extends Dependencies> {
exclude?: Set<OptionalDependencies>;
build: (root: Container<Omit<Dependencies, '@sern/client'>, {}>) => Container<T, {}>;
}
export type ImportPayload<T> = { module: T; absPath: string };

View File

@@ -1,74 +0,0 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { Awaitable } from 'discord.js';
import type { Err, Ok, Result } from 'ts-results-es';
import type { PluginType } from '../handler/structures/enums';
import type { CommandModule, EventModule } from './module';
import type { CommandArgs, InitArgs } from '../handler/plugins';
import type { Deprecated, Processed } from './handler';
import type { CommandType } from '../handler/structures/enums';
export type PluginResult = Awaitable<VoidResult>;
export type VoidResult = Result<void, void>;
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
execute: (...args: Args) => PluginResult;
}
export interface InitPlugin<Args extends any[] = any[]> {
type: PluginType.Init;
execute: (...args: Args) => PluginResult;
}
export interface ControlPlugin<Args extends any[] = any[]> {
type: PluginType.Control;
execute: (...args: Args) => PluginResult;
}
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;
/**
* @deprecated
* Use the newer helper functions and import { controller } from '@sern/handler'
*/
export interface CommandPlugin<T extends CommandType = CommandType> {
name?: string;
description?: string;
type: PluginType.Command;
execute: (
m: InitArgs<Processed<CommandModule>>,
controller?: Deprecated<'Please import controller instead'>,
) => PluginResult;
}
/**
* @deprecated
* Use the newer helper functions
*/
export interface EventPlugin<T extends CommandType> {
name?: string;
description?: string;
type: PluginType.Event;
execute: (args: CommandArgs<T, PluginType.Event>, controller?: Controller) => PluginResult;
}
export type DiscordEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type ExternalEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type SernEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type AutocompletePlugin = Deprecated<'Please view alternatives: '>;
export type SernEventPlugin = Deprecated<'Please view alternatives: '>;
export type ExternalEventPlugin = Deprecated<'Please view alternatives: '>;
export type DiscordEventPlugin = Deprecated<'Please view alternatives: '>;

27
src/types/utility.ts Normal file
View File

@@ -0,0 +1,27 @@
import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { Module } from './core-modules';
import type { Result } from 'ts-results-es';
export type Awaitable<T> = PromiseLike<T> | T;
export type VoidResult = Result<void, void>;
export type AnyFunction = (...args: any[]) => unknown;
export interface SernEventsMapping {
'module.register': [Payload];
'module.activate': [Payload];
error: [{ type: 'failure'; module?: Module; reason: string | Error }];
warning: [Payload];
modulesLoaded: [never?];
}
export type Payload =
| { type: 'success'; module: Module }
| { type: 'failure'; module?: Module; reason: string | Error }
| { type: 'warning'; module: undefined; reason: string };
export type UnpackFunction<T> = T extends (...args: any) => infer U ? U : T
export type UnpackedDependencies = {
[K in keyof Dependencies]: UnpackFunction<Dependencies[K]>
}
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;

85
test/core/context.test.ts Normal file
View File

@@ -0,0 +1,85 @@
import { describe, vi, it, expect } from'vitest'
import { Context } from '../../src';
import { faker } from '@faker-js/faker'
describe('Context', () => {
// Mocked message and interaction objects for testing
const mockMessage = {
id: 'messageId',
channel: 'channelId',
channelId: 'channelId',
interaction: {
id: faker.string.uuid()
},
author: { id: 'userId' },
createdTimestamp: 1234567890,
guild: 'guildId',
guildId: 'guildId',
member: { id: 'memberId' },
client: { id: 'clientId' },
inGuild: vi.fn().mockReturnValue(true),
reply: vi.fn(),
};
const mockInteraction = {
id: 'interactionId',
user: { id: 'userId' },
channel: 'channelId',
channelId: 'channelId',
createdTimestamp: 1234567890,
guild: 'guildId',
guildId: 'guildId',
fetchReply: vi.fn().mockResolvedValue({}),
member: { id: 'memberId' },
client: { id: 'clientId' },
isChatInputCommand: vi.fn().mockResolvedValue(true),
inGuild: vi.fn().mockReturnValue(true),
reply: vi.fn().mockResolvedValue({}),
};
it('should create a context from a message', () => {
//@ts-ignore
const context = Context.wrap(mockMessage);
expect(context).toBeDefined();
expect(context.id).toBe('messageId');
});
it('should throw error if accessing interaction as message', () => {
//@ts-ignore
const context = Context.wrap(mockMessage);
expect(context).toBeDefined();
expect(() => context.interaction)
.toThrowError('You cannot use message when an interaction fired or vice versa');
})
it('should throw error if accessing message as interaction', () => {
//@ts-ignore
const context = Context.wrap(mockInteraction);
expect(context).toBeDefined();
expect(() => context.message)
.toThrowError('You cannot use message when an interaction fired or vice versa');
})
it('should create a context from an interaction', () => {
//@ts-ignore
const context = Context.wrap(mockInteraction);
expect(context).toBeDefined();
expect(context.id).toBe('interactionId');
});
it('should reply to a context with a message', async () => {
//@ts-ignore
const context = Context.wrap(mockMessage);
const replyOptions = { content: 'Hello, world!' };
await context.reply(replyOptions);
expect(mockMessage.reply).toHaveBeenCalledWith(replyOptions);
});
it('should reply to a context with an interaction', async () => {
//@ts-ignore
const context = Context.wrap(mockInteraction);
const replyOptions = { content: 'Hello, world!' };
await context.reply(replyOptions);
expect(mockInteraction.reply).toHaveBeenCalledWith(replyOptions);
});
});

View File

@@ -0,0 +1,11 @@
import { assertType, describe, it } from 'vitest';
import * as __Services from '../../src/core/structures/default-services';
import * as Contracts from '../../src/core/interfaces';
describe('default contracts', () => {
it('should satisfy contracts', () => {
assertType<Contracts.Logging>(new __Services.DefaultLogging());
assertType<Contracts.ErrorHandling>(new __Services.DefaultErrorHandling());
});
});

View File

@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest';
import {
CommandControlPlugin,
CommandInitPlugin,
EventInitPlugin,
} from '../../src';
import { PluginType, controller } from '../../src';
describe('create-plugins', () => {
it('should make proper control plugins', () => {
const pl2 = CommandControlPlugin(() => controller.next());
expect(pl2).to.have.all.keys(['type', 'execute']);
expect(pl2.type).toBe(PluginType.Control);
expect(pl2.execute).an('function');
});
it('should make proper init plugins', () => {
const pl = EventInitPlugin(() => controller.next());
expect(pl).to.have.all.keys(['type', 'execute']);
expect(pl.type).toBe(PluginType.Init);
expect(pl.execute).an('function');
const pl2 = CommandInitPlugin(() => controller.next());
expect(pl2).to.have.all.keys(['type', 'execute']);
expect(pl2.type).toBe(PluginType.Init);
expect(pl2.execute).an('function');
});
});

382
test/core/functions.test.ts Normal file
View File

@@ -0,0 +1,382 @@
//@ts-nocheck
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PluginType, SernOptionsData, controller } from '../../src/index';
import { partitionPlugins, treeSearch } from '../../src/core/functions';
import { faker } from '@faker-js/faker';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
vi.mock('discord.js', async (importOriginal) => {
const mod = await importOriginal()
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const ButtonInteraction = class {
customId;
type = 3;
componentType = 2;
isButton = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const AutocompleteInteraction = class {
type = 4;
option: string;
constructor(s: string) {
this.option = s;
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
};
};
return {
Collection: mod.Collection,
ComponentType: mod.ComponentType,
InteractionType: mod.InteractionType,
ApplicationCommandOptionType: mod.ApplicationCommandOptionType,
ApplicationCommandType: mod.ApplicationCommandType,
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
};
});
describe('functions', () => {
afterEach(() => {
vi.clearAllMocks();
});
function createRandomPlugins(len: number) {
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
return Array.from({ length: len }, () => ({
type: random(),
execute: () => (random() === 1 ? controller.next() : controller.stop()),
}));
}
function createRandomChoice() {
return {
type: faker.number.int({ min: 1, max: 11 }),
name: faker.word.noun(),
description: faker.word.adjective(),
};
}
it('should partition plugins correctly', () => {
const plugins = createRandomPlugins(100);
const [onEvent, init] = partitionPlugins(plugins);
for (const el of onEvent) expect(el.type).to.equal(PluginType.Control);
for (const el of init) expect(el.type).to.equal(PluginType.Init);
});
it('should tree search options tree depth 1', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('autocomplete');
const options: SernOptionsData[] = [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command: { onEvent: [], execute: vi.fn() },
},
];
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'autocomplete',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('autocomplete');
expect(result.command).to.be.not.undefined;
}),
it('should tree search depth 2', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('should tree search depth n > 2', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
createRandomChoice(),
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('should correctly resolve suboption of the same name given two subcommands ', () => {
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('two subcommands with an option of the same name', () => {
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
let autocmpInteraction2 = new AutocompleteInteraction('nested');
autocmpInteraction2.options.getSubcommand.mockReturnValue(subcommandName + 'a');
autocmpInteraction2.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result2 = treeSearch(autocmpInteraction2, options);
expect(result2).toBeTruthy();
expect(result2?.name).toEqual('nested');
});
it('simulates autocomplete typing and resolution', () => {
const subcommandName = faker.string.alpha();
const optionName = faker.word.noun();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: optionName,
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: vi.fn(),
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: optionName,
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: vi.fn(),
},
},
],
},
],
},
];
let accumulator = '';
let result: unknown;
for (const char of optionName) {
accumulator += char;
const autocomplete = new AutocompleteInteraction(accumulator);
autocomplete.options.getSubcommand.mockReturnValue(subcommandName);
autocomplete.options.getFocused.mockReturnValue({
name: accumulator,
value: faker.string.alpha(),
focused: true,
});
result = treeSearch(autocomplete, options);
}
expect(result).toBeTruthy();
});
});

135
test/core/id.test.ts Normal file
View File

@@ -0,0 +1,135 @@
//@ts-nocheck
import { expect, test, vi } from 'vitest'
import { CommandType } from '../../src/core/structures/enums';
import * as Id from '../../src/core/id'
import { ButtonInteraction, ModalSubmitInteraction } from 'discord.js';
vi.mock('discord.js', async (importOriginal) => {
const mod = await importOriginal()
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const ButtonInteraction = class {
customId;
type = 3;
componentType = 2;
isButton = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const AutocompleteInteraction = class {
type = 4;
option: string;
constructor(s: string) {
this.option = s;
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
};
};
return {
Collection: mod.Collection,
ComponentType: mod.ComponentType,
InteractionType: mod.InteractionType,
ApplicationCommandOptionType: mod.ApplicationCommandOptionType,
ApplicationCommandType: mod.ApplicationCommandType,
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
};
});
test('id -> Text', () => {
expect(Id.create("ping", CommandType.Text)).toBe("ping_T")
})
test('id -> Both', () => {
expect(Id.create("ping", CommandType.Both)).toBe("ping_B")
})
test('id -> CtxMsg', () => {
expect(Id.create("ping", CommandType.CtxMsg)).toBe("ping_A3")
})
test('id -> CtxUsr', () => {
expect(Id.create("ping", CommandType.CtxUser)).toBe("ping_A2")
})
test('id -> Modal', () => {
expect(Id.create("my-modal", CommandType.Modal)).toBe("my-modal_M");
})
test('id -> Button', () => {
expect(Id.create("my-button", CommandType.Button)).toBe("my-button_C2");
})
test('id -> Slash', () => {
expect(Id.create("myslash", CommandType.Slash)).toBe("myslash_A1");
})
test('id -> StringSelect', () => {
expect(Id.create("mystringselect", CommandType.StringSelect)).toBe("mystringselect_C3");
})
test('id -> UserSelect', () => {
expect(Id.create("myuserselect", CommandType.UserSelect)).toBe("myuserselect_C5");
})
test('id -> RoleSelect', () => {
expect(Id.create("myroleselect", CommandType.RoleSelect)).toBe("myroleselect_C6");
})
test('id -> MentionSelect', () => {
expect(Id.create("mymentionselect", CommandType.MentionableSelect)).toBe("mymentionselect_C7");
})
test('id -> ChannelSelect', () => {
const modal = Id.create("mychannelselect", CommandType.ChannelSelect)
expect(modal).toBe("mychannelselect_C8");
})
test('id reconstruct button', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn"))
expect(idload[0].id).toBe("btn_C2")
})
test('id reconstruct button with params', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn/asdf"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe("asdf")
})
test('id reconstruct modal with params', () => {
const idload = Id.reconstruct(new ModalSubmitInteraction("btn/asdf"))
expect(idload[0].id).toBe("btn_M")
expect(idload[0].params).toBe("asdf")
})
test('id reconstruct modal', () => {
const idload = Id.reconstruct(new ModalSubmitInteraction("btn"))
expect(idload[0].id).toBe("btn_M")
expect(idload[0].params).toBe(undefined)
})
test('id reconstruct button with empty params', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn/"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe("")
})
test('id reconstruct with multiple slashes', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn//"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe("/")
})
test('id reconstruct button', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe(undefined)
})

View File

@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest'
import path from 'node:path'
import * as Files from '../../src/core/module-loading'
import { Module } from '../../src/types/core-modules'
import { AssertionError } from 'node:assert'
//TODO: mock fs?
describe('module-loading', () => {
it('should get the filename of the commandmodule (linux, esm)', () => {
const fname = "///home/pooba/Projects/sern/halibu/dist/commands/ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toBe("ping")
})
it('should get filename of commandmodule (linux, cjs)', () => {
const fname = "file:///home/pooba/Projects/sern/halibu/dist/commands/ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toBe("ping")
})
it('should get the filename of the commandmodule (windows, cjs)', () => {
//this test case is impossible on linux.
if(process.platform == 'win32') {
const fname = "C:\\pooba\\Projects\\sern\\halibu\\dist\\commands\\ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toEqual("ping");
}
})
it('should get filename of commandmodule (windows, esm)', () => {
//this test case is impossible on linux.
if(process.platform == 'win32') {
const fname = "file:///C:\\pooba\\Projects\\sern\\halibu\\dist\\commands\\ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toEqual("ping");
}
})
it('should import a commandModule properly', async () => {
const { module } = await Files.importModule<Module>(path.resolve("test", 'mockules', "module.ts"));
expect(module.name).toBe('module')
})
it('should throw when failed commandModule import', async () => {
try {
await Files.importModule(path.resolve('test', 'mockules', 'failed.ts'))
} catch(e) {
expect(e instanceof AssertionError)
}
})
it('should throw when failed commandModule import', async () => {
try {
await Files.importModule(path.resolve('test', 'mockules', 'failed.ts'))
} catch(e) {
expect(e instanceof AssertionError)
}
})
it('reads all modules in mockules', async () => {
const ps = [] as string[]
for await (const fpath of Files.readRecursive(path.resolve('test', 'mockules'))) {
ps.push(fpath)
}
expect(ps.length === 4)
})
})

View File

@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
import { Presence } from '../../src';
// Example test suite for the module function
describe('module function', () => {
it('should return a valid configuration', () => {
const config = Presence.module({
inject: ['dependency1', 'dependency2'],
execute: vi.fn(),
});
expect(config).toBeDefined();
expect(config.inject).toEqual(['dependency1', 'dependency2']);
expect(typeof config.execute).toBe('function');
});
});
describe('of function', () => {
it('should return a valid presence configuration without repeat and onRepeat', () => {
const presenceConfig = Presence.of({
status: 'online',
afk: false,
activities: [{ name: 'Test Activity' }],
shardId: [1, 2, 3],
}).once();
expect(presenceConfig).toBeDefined();
//@ts-ignore Maybe fix?
expect(presenceConfig.repeat).toBeUndefined();
//@ts-ignore Maybe fix?
expect(presenceConfig.onRepeat).toBeUndefined();
expect(presenceConfig).toMatchObject({
status: 'online',
afk: false,
activities: [{ name: 'Test Activity' }],
shardId: [1, 2, 3],
});
});
it('should return a valid presence configuration with repeat and onRepeat', () => {
const onRepeatCallback = vi.fn();
const presenceConfig = Presence.of({
status: 'idle',
activities: [{ name: 'Another Test Activity' }],
}).repeated(onRepeatCallback, 5000);
expect(presenceConfig).toBeDefined();
expect(presenceConfig.repeat).toBe(5000);
expect(presenceConfig.onRepeat).toBe(onRepeatCallback);
expect(presenceConfig).toMatchObject({
status: 'idle',
activities: [{ name: 'Another Test Activity' }],
});
});
})

151
test/handlers.test.ts Normal file
View File

@@ -0,0 +1,151 @@
//@ts-nocheck
import { beforeEach, describe, expect, vi, it, test } from 'vitest';
import { callInitPlugins, eventDispatcher } from '../src/handlers/event-utils';
import { Client, ChatInputCommandInteraction } from 'discord.js'
import { faker } from '@faker-js/faker';
import { Module } from '../src/types/core-modules';
import { Processed } from '../src/types/core-modules';
import { EventEmitter } from 'events';
import { EventType } from '../src/core/structures/enums';
import { CommandControlPlugin, CommandInitPlugin, CommandType, controller } from '../src';
vi.mock('discord.js', async (importOriginal) => {
const mod = await importOriginal()
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const ButtonInteraction = class {
customId;
type = 3;
componentType = 2;
isButton = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const AutocompleteInteraction = class {
type = 4;
option: string;
constructor(s: string) {
this.option = s;
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
};
};
return {
Client : vi.fn(),
Collection: mod.Collection,
ComponentType: mod.ComponentType,
InteractionType: mod.InteractionType,
ApplicationCommandOptionType: mod.ApplicationCommandOptionType,
ApplicationCommandType: mod.ApplicationCommandType,
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
ChatInputCommandInteraction: vi.fn()
};
});
function createRandomPlugin (s: 'go', mut?: Partial<Module>) {
return CommandInitPlugin(({ module }) => {
if(mut) {
Object.entries(mut).forEach(([k, v]) => {
module[k] = v
})
}
return s == 'go'
? controller.next()
: controller.stop()
})
}
function createRandomModule(plugins: any[]): Processed<Module> {
return {
type: EventType.Discord,
meta: { id:"", absPath: "" },
description: faker.string.alpha(),
plugins,
name: "cheese",
onEvent: [],
execute: vi.fn(),
};
}
function mockDeps() {
return {
'@sern/client': new Client(),
'@sern/emitter': new EventEmitter()
}
}
describe('eventDispatcher standard', () => {
let m: Processed<Module>;
let ee: EventEmitter;
beforeEach(() => {
ee = new EventEmitter();
m = createRandomModule();
});
it('should throw', () => {
expect(() => eventDispatcher(mockDeps(), m, 'not event emitter')).toThrowError();
});
it("Shouldn't throw", () => {
expect(() => eventDispatcher(mockDeps(), m, ee)).not.toThrowError();
});
});
test ('call init plugins', async () => {
const deps = mockDeps()
const plugins = createRandomPlugin('go', { name: "abc" })
const mod = createRandomModule([plugins])
const s = await callInitPlugins(mod, deps, false)
expect("abc").equal(s.name)
})
test('init plugins replace array', async () => {
const deps = mockDeps()
const plugins = createRandomPlugin('go', { opts: [] })
const plugins2 = createRandomPlugin('go', { opts: ['a'] })
const mod = createRandomModule([plugins, plugins2])
const s = await callInitPlugins(mod, deps, false)
expect(['a']).deep.equal(s.opts)
})
test('call control plugin ', async () => {
const plugin = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next();
});
const res = await plugin.execute(new ChatInputCommandInteraction(), {})
expect(res.isOk()).toBe(true)
})
test('form sdt', async () => {
const expectedObject = {
"plugin/abc": faker.person.jobArea(),
"plugin2/abc": faker.git.branch(),
"plugin3/cheese": faker.person.jobArea()
}
const plugin = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next({ "plugin/abc": expectedObject['plugin/abc'] });
});
const plugin2 = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next({ "plugin2/abc": expectedObject['plugin2/abc'] });
});
const plugin3 = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next({ "plugin3/cheese": expectedObject['plugin3/cheese'] });
});
})

0
test/mockules/!ignd.ts Normal file
View File

View File

0
test/mockules/failed.ts Normal file
View File

6
test/mockules/module.ts Normal file
View File

@@ -0,0 +1,6 @@
import { CommandType, commandModule } from '../../src/'
export default commandModule({
type: CommandType.Both,
description: "",
execute: (Ctx, args) => {}
})

6
test/mockules/ug/pass.ts Normal file
View File

@@ -0,0 +1,6 @@
import { CommandType, commandModule } from '../../../src/'
export default commandModule({
type: CommandType.Both,
description: "",
execute: (Ctx, args) => {}
})

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"preserveSymlinks": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "dist"],
"include": ["src"]
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/cjs",
"target": "esnext"
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"target": "esnext"
}
}

View File

@@ -1,3 +1,21 @@
{
"extends": "./tsconfig-esm.json"
"compilerOptions": {
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"strictNullChecks": true,
"moduleResolution": "node16",
"skipLibCheck": true,
"declaration": true,
"preserveSymlinks": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"outDir": "dist",
"module": "node16",
"target": "esnext",
"sourceMap": true
},
"exclude": ["node_modules", "dist"],
"include": ["./src", "./src/**/*.d.ts"]
}

View File

@@ -1,48 +0,0 @@
import { defineConfig } from 'tsup';
import { writeFile } from 'fs/promises';
import ifdefPlugin from 'esbuild-ifdef';
const shared = {
entry: ['src/index.ts'],
external: ['discord.js'],
platform: 'node',
clean: true,
sourcemap: false,
};
export default defineConfig([
{
format: 'esm',
target: 'node16',
tsconfig: './tsconfig-esm.json',
outDir: './dist/esm',
treeshake: true,
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'esm' }, verbose: true })],
outExtension() {
return {
js: '.mjs',
};
},
async onSuccess() {
console.log('writing json esm');
await writeFile('./dist/esm/package.json', JSON.stringify({ type: 'module' }));
},
...shared,
},
{
format: 'cjs',
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'cjs' }, verbose: true })],
splitting: false,
target: 'node16',
tsconfig: './tsconfig-cjs.json',
outDir: './dist/cjs',
outExtension() {
return {
js: '.cjs',
};
},
async onSuccess() {
console.log('writing json commonjs');
await writeFile('./dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }));
},
...shared,
},
]);

2496
yarn.lock

File diff suppressed because it is too large Load Diff