Compare commits

..

103 Commits

Author SHA1 Message Date
Jacob Nguyen
adb3d4f402 3.0.0-rc1 2023-05-23 14:50:20 -05:00
Jacob Nguyen
4d1e6f2a71 refactor: npx knip 2023-05-23 11:12:54 -05:00
Jacob Nguyen
63e0daac16 fix: typings for options- have access to all properties now 2023-05-22 22:50:05 -05:00
Jacob Nguyen
49fb1453a5 revert removal of crash method and mark deprecated 2023-05-19 20:48:38 -05:00
Jacob Nguyen
a6db21c720 docs: add purpose of d.ts file 2023-05-19 20:33:11 -05:00
Jacob Nguyen
fc6206a205 chore: make changes in codebase after error contract change 2023-05-19 20:30:32 -05:00
Jacob Nguyen
d7195c45b3 feat: change error handling contract 2023-05-19 20:15:25 -05:00
Jacob Nguyen
698d468acb test: add more tests 2023-05-19 19:43:05 -05:00
Jacob Nguyen
5d842fd5e7 test: add tests 2023-05-19 02:08:08 -05:00
Jacob Nguyen
b6ef27df88 refactor: add better typings for sern event modules 2023-05-18 22:52:37 -05:00
Jacob Nguyen
ea78a98f69 refactor: remove cast 2023-05-18 21:36:41 -05:00
Jacob Nguyen
08e358b32d feat: add new sern emitter event 2023-05-18 21:35:15 -05:00
Jacob Nguyen
47f58a9efa revert: internal name 2023-05-18 20:03:53 -05:00
Jacob Nguyen
b8619df838 feat: globalize dependencies type 2023-05-18 18:54:03 -05:00
Jacob Nguyen
e4a9c8d551 fix class based module loading 2023-05-18 13:06:40 -05:00
Jacob Nguyen
59a13765a5 style: pretty 2023-05-18 11:57:28 -05:00
Jacob Nguyen
f2b53dac32 fix: plugins for class modules and module loader 2023-05-18 11:49:14 -05:00
Jacob Nguyen
8f39fc0896 refactor: a lot 2023-05-15 17:35:38 -05:00
Jacob Nguyen
423b0d2aa7 revert: re export command executable and event executable 2023-05-15 17:22:25 -05:00
Jacob Nguyen
b2ab99b7cc refactor: move metadata outside of module declarations 2023-05-15 17:10:55 -05:00
Jacob Nguyen
b06e457caa fix: directories ignoring incorrectly 2023-05-13 01:04:50 -05:00
Jacob Nguyen
6fd9f8f032 feat: dev and prod mode 2023-05-13 00:52:46 -05:00
Jacob Nguyen
3ed6d84727 style: format & lint 2023-05-11 09:50:51 -05:00
Jacob Nguyen
95ec4d0e3c chore: update container and init hook progress 2023-05-11 01:33:49 -05:00
Jacob Nguyen
5e878cc97b chore: progress of globalizing dependencies type 2023-05-10 21:57:55 -05:00
Jacob Nguyen
57dfa09a73 add separate id for id processing 2023-05-10 21:41:18 -05:00
Jacob Nguyen
0256a38187 revert: readd module store and add contract 2023-05-10 21:40:40 -05:00
Jacob Nguyen
e6f3a55d73 build: speed up build 2023-05-10 21:35:54 -05:00
Jacob Nguyen
7ff8917a26 fix: unaliased modules would throw error 2023-05-10 15:09:26 -05:00
Jacob Nguyen
8a537d670b feat: types organization and cleaning up code base 2023-05-09 22:49:29 -05:00
Jacob Nguyen
cd1568ff69 refactor: rename, format, move things 2023-05-08 11:36:25 -05:00
Jacob Nguyen
9718360530 revert: readd discordEvent 2023-05-08 00:29:48 -05:00
Jacob Nguyen
6bd7573b88 revert: multiple entry points 2023-05-08 00:29:10 -05:00
Jacob Nguyen
993c41f37e chore: update name of function to reflect use 2023-05-08 00:27:36 -05:00
Jacob Nguyen
17423b683e refactor: cleaning up code and renaming variables 2023-05-08 00:27:01 -05:00
Jacob Nguyen
8d507d0520 chore: move context into structures directory 2023-05-07 23:41:23 -05:00
Jacob Nguyen
8dacfafbac chore: cleanup dispatchers subdirectory for single file 2023-05-07 23:37:23 -05:00
Jacob Nguyen
98de65965e chore: delete plugins dir 2023-05-07 00:46:50 -05:00
Jacob Nguyen
e411f81ab9 chore: move and clean 2023-05-07 00:46:22 -05:00
Jacob Nguyen
f6e41cc9b6 chore: move typings 2023-05-07 00:44:29 -05:00
Jacob Nguyen
9f5d2faa61 chore: fix InitArgs missing 2023-05-07 00:43:20 -05:00
Jacob Nguyen
815cbf19eb feat: SernEmitter now captures promise rejections 2023-05-06 23:23:24 -05:00
Jacob Nguyen
4ff2f39fcf revert: remove cloudflare typings 2023-05-06 22:13:59 -05:00
Jacob Nguyen
87585dcc69 refactor: change all files to camelcase and refactor 2023-05-06 22:13:09 -05:00
Jacob Nguyen
8c28e00206 chore: change all file names to camel case 2023-05-06 22:12:37 -05:00
Jacob Nguyen
bf6b99ff03 docs: specifics 2023-05-06 03:18:09 -05:00
Jacob Nguyen
9f213e9e8c refactor: fold switch case 2023-05-06 03:17:43 -05:00
Jacob Nguyen
4794c2655d revert: remove AnyDependencies type 2023-05-06 03:13:02 -05:00
Jacob Nguyen
43181bf916 style: pretty 2023-05-06 02:04:00 -05:00
Jacob Nguyen
8e9b3bbe04 chore: remove bad imports 2023-05-06 02:01:27 -05:00
Jacob Nguyen
098a440669 feat: add getPublishableCommands to ModuleManager 2023-05-06 01:49:42 -05:00
Jacob Nguyen
1e1398fade refactor: remove unneeded signatures and fix imports 2023-05-06 01:16:53 -05:00
Jacob Nguyen
1fea6fa136 refactor: add createGenericHandler 2023-05-06 01:01:54 -05:00
Jacob Nguyen
ff478123a8 chore: revert multi and clean up code 2023-05-05 23:31:51 -05:00
Jacob Nguyen
29499457bb chore: revert multi and mov sernEmitter 2023-05-05 23:14:54 -05:00
Jacob Nguyen
084b5adb5c chore: revert multiplatform 2023-05-05 23:10:10 -05:00
Jacob Nguyen
397857208e chore: modularize and split typings 2023-05-05 23:01:59 -05:00
Jacob Nguyen
ed532af7f7 revert: multiplatform 2023-05-05 22:52:24 -05:00
Jacob Nguyen
6628fa7074 revert: multiplatform 2023-05-05 22:51:59 -05:00
Jacob Nguyen
3d0cf62b9a chore: add tweetnacl 2023-05-05 10:12:39 -05:00
Jacob Nguyen
8e6061b4f2 chore: update Proccessed typing to ./core 2023-05-05 10:12:16 -05:00
Jacob Nguyen
e644b184f2 chore: add old context here until i figure out what to do 2023-05-04 18:55:55 -05:00
Jacob Nguyen
dbfd8cfe40 chore: add agnostic predicates 2023-05-04 18:47:28 -05:00
Jacob Nguyen
b0f6093ac6 chore: remove leaky import 2023-05-04 18:47:13 -05:00
Jacob Nguyen
1288a6b5cc chore: refactor and add multiplatform typings 2023-05-04 18:46:52 -05:00
Jacob Nguyen
7f53521ef4 chore: refactor and move things 2023-05-04 18:46:23 -05:00
Jacob Nguyen
faa0b4882e feat!: new module resolution algorithm 2023-05-04 18:45:54 -05:00
Jacob Nguyen
e2fd44516a refactor: internalize operators 2023-05-04 18:42:02 -05:00
Jacob Nguyen
4125508cd3 chore: redundant directory 2023-05-03 21:09:04 -05:00
Jacob Nguyen
2ecf3e5a5c chore: rename, wip refactoring 2023-05-03 21:08:20 -05:00
Jacob Nguyen
d53b315157 chore: remove redundant directories for single files 2023-05-03 21:05:19 -05:00
Jacob Nguyen
b7e99a85a6 chore: serverless work 2023-05-03 21:04:58 -05:00
Jacob Nguyen
aacf47efab chore: move commands into seperate file 2023-05-03 21:04:10 -05:00
Jacob Nguyen
cae1f648f0 refactor: move and update things 2023-05-03 21:02:51 -05:00
Jacob Nguyen
b7a13d6558 chore: remove redundant directories for single files 2023-05-03 15:08:07 -05:00
Jacob Nguyen
de654532e8 chore: remove redundant directories for single files 2023-05-03 13:01:14 -05:00
Jacob Nguyen
a5e8743afd chore: remove utils folder in favor of single file 2023-05-03 13:00:11 -05:00
Jacob Nguyen
3a8098e523 move eslint and prettier configs to json 2023-05-03 11:53:58 -05:00
Jacob Nguyen
21a20523b8 chore(core): add DefaultWrapper as sern classic 2023-05-02 10:28:58 -05:00
Jacob Nguyen
52b186cc44 chore(core): update imports and operators 2023-05-02 10:27:53 -05:00
Jacob Nguyen
a17be67b48 chore: move prettier into package.json 2023-05-01 20:51:54 -05:00
Jacob Nguyen
6a65dac272 chore: remove extra file 2023-04-29 18:05:37 -05:00
Jacob Nguyen
7f59f81b35 feat: make Context platform specific, CoreContext as Core 2023-04-27 13:23:16 -05:00
Jacob Nguyen
e6f9207a47 chore: move command args matrix as binding 2023-04-27 13:22:15 -05:00
Jacob Nguyen
fa4e6c7fbd chore: fix typings 2023-04-27 13:21:25 -05:00
Jacob Nguyen
d41cc9ea2d move strategy to index.ts and add adapters 2023-04-27 13:20:11 -05:00
Jacob Nguyen
97ad338830 lift requiredDependencyKeys out of makeFetcher 2023-04-26 21:25:53 -05:00
Jacob Nguyen
dcdf120ba7 chore: update typings 2023-04-26 11:58:22 -05:00
Jacob Nguyen
1fa7eef336 chore: remove dependence on discord.js Awaitable type 2023-04-16 14:17:56 -05:00
Jacob Nguyen
37ddef170f chore: remove deprecated support for plugins 2023-04-16 14:13:17 -05:00
Jacob Nguyen
195c155769 docs: add documentation to internal function 2023-04-16 13:33:35 -05:00
Jacob Nguyen
458a0b978f chore: remove deprecated symbols 2023-04-16 13:30:20 -05:00
Jacob Nguyen
bc4802f261 chore:remove const function 2023-04-16 13:27:17 -05:00
Jacob Nguyen
8e2ba871c4 chore:update paths 2023-04-15 16:48:04 -05:00
Jacob Nguyen
83f6568d84 add deprecation warning 2023-04-15 12:12:02 -05:00
Jacob Nguyen
f02bbbf54e add wrapper platform field 2023-04-15 12:12:02 -05:00
jacoobes
89bb50794b chore: fix paths 2023-04-14 13:05:27 -05:00
jacoobes
1a4e046f50 chore: move operators into core 2023-04-14 12:59:13 -05:00
jacoobes
09e57c73ac moving and fixing imports 2023-04-14 12:45:41 -05:00
jacoobes
457cb25cb2 refactor: remove dependence on discord.js for module stoore 2023-04-14 12:17:11 -05:00
Jacob Nguyen
b40f8905ea remove id from lifted Context 2023-04-12 17:06:54 -05:00
Jacob Nguyen
8bdb6d8216 work on strategy and lifted Context 2023-04-12 17:03:35 -05:00
Jacob Nguyen
58b3d85da8 refactor: move things to core, imports not fixed yet 2023-04-12 13:08:45 -05:00
78 changed files with 2000 additions and 4892 deletions

View File

@@ -1,450 +0,0 @@
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
forbidden: [
/* rules from the 'recommended' preset: */
{
name: 'no-circular',
severity: 'warn',
comment:
'This dependency is part of a circular relationship. You might want to revise ' +
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
from: {},
to: {
circular: true,
},
},
{
name: 'no-orphans',
comment:
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
'add an exception for it in your dependency-cruiser configuration. By default ' +
'this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration ' +
'files (.d.ts), tsconfig.json and some of the babel and webpack configs.',
severity: 'warn',
from: {
orphan: true,
pathNot: [
'(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files
'\\.d\\.ts$', // TypeScript declaration files
'(^|/)tsconfig\\.json$', // TypeScript config
'(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$', // other configs
],
},
to: {},
},
{
name: 'no-deprecated-core',
comment:
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
"bound to exist - node doesn't deprecate lightly.",
severity: 'warn',
from: {},
to: {
dependencyTypes: ['core'],
path: [
'^(v8/tools/codemap)$',
'^(v8/tools/consarray)$',
'^(v8/tools/csvparser)$',
'^(v8/tools/logreader)$',
'^(v8/tools/profile_view)$',
'^(v8/tools/profile)$',
'^(v8/tools/SourceMap)$',
'^(v8/tools/splaytree)$',
'^(v8/tools/tickprocessor-driver)$',
'^(v8/tools/tickprocessor)$',
'^(node-inspect/lib/_inspect)$',
'^(node-inspect/lib/internal/inspect_client)$',
'^(node-inspect/lib/internal/inspect_repl)$',
'^(async_hooks)$',
'^(punycode)$',
'^(domain)$',
'^(constants)$',
'^(sys)$',
'^(_linklist)$',
'^(_stream_wrap)$',
],
},
},
{
name: 'not-to-deprecated',
comment:
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
'version of that module, or find an alternative. Deprecated modules are a security risk.',
severity: 'warn',
from: {},
to: {
dependencyTypes: ['deprecated'],
},
},
{
name: 'no-non-package-json',
severity: 'error',
comment:
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
'available on live with an non-guaranteed version. Fix it by adding the package to the dependencies ' +
'in your package.json.',
from: {},
to: {
dependencyTypes: ['npm-no-pkg', 'npm-unknown'],
},
},
{
name: 'not-to-unresolvable',
comment:
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
'module: add it to your package.json. In all other cases you likely already know what to do.',
severity: 'error',
from: {},
to: {
couldNotResolve: true,
},
},
{
name: 'no-duplicate-dep-types',
comment:
"Likely this module depends on an external ('npm') package that occurs more than once " +
'in your package.json i.e. bot as a devDependencies and in dependencies. This will cause ' +
'maintenance problems later on.',
severity: 'warn',
from: {},
to: {
moreThanOneDependencyType: true,
// as it's pretty common to have a type import be a type only import
// _and_ (e.g.) a devDependency - don't consider type-only dependency
// types for this rule
dependencyTypesNot: ['type-only'],
},
},
/* rules you might want to tweak for your specific situation: */
{
name: 'not-to-test',
comment:
"This module depends on code within a folder that should only contain tests. As tests don't " +
"implement functionality this is odd. Either you're writing a test outside the test folder " +
"or there's something in the test folder that isn't a test.",
severity: 'error',
from: {
pathNot: '^(test)',
},
to: {
path: '^(test)',
},
},
{
name: 'not-to-spec',
comment:
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
severity: 'error',
from: {},
to: {
path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$',
},
},
{
name: 'not-to-dev-dep',
severity: 'error',
comment:
"This module depends on an npm package from the 'devDependencies' section of your " +
'package.json. It looks like something that ships to production, though. To prevent problems ' +
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
'section of your package.json. If this module is development only - add it to the ' +
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
from: {
path: '^(src)',
pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$',
},
to: {
dependencyTypes: ['npm-dev'],
},
},
{
name: 'optional-deps-used',
severity: 'info',
comment:
'This module depends on an npm package that is declared as an optional dependency ' +
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
"If you're using an optional dependency here by design - add an exception to your" +
'dependency-cruiser configuration.',
from: {},
to: {
dependencyTypes: ['npm-optional'],
},
},
{
name: 'peer-deps-used',
comment:
'This module depends on an npm package that is declared as a peer dependency ' +
'in your package.json. This makes sense if your package is e.g. a plugin, but in ' +
'other cases - maybe not so much. If the use of a peer dependency is intentional ' +
'add an exception to your dependency-cruiser configuration.',
severity: 'warn',
from: {},
to: {
dependencyTypes: ['npm-peer'],
},
},
],
options: {
/* conditions specifying which files not to follow further when encountered:
- path: a regular expression to match
- dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/main/doc/rules-reference.md#dependencytypes-and-dependencytypesnot
for a complete list
*/
doNotFollow: {
path: 'node_modules',
},
/* conditions specifying which dependencies to exclude
- path: a regular expression to match
- dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies.
leave out if you want to exclude neither (recommended!)
*/
// exclude : {
// path: '',
// dynamic: true
// },
/* pattern specifying which files to include (regular expression)
dependency-cruiser will skip everything not matching this pattern
*/
// includeOnly : '',
/* dependency-cruiser will include modules matching against the focus
regular expression in its output, as well as their neighbours (direct
dependencies and dependents)
*/
// focus : '',
/* list of module systems to cruise */
// moduleSystems: ['amd', 'cjs', 'es6', 'tsd'],
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/'
to open it on your online repo or `vscode://file/${process.cwd()}/` to
open it in visual studio code),
*/
// prefix: '',
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
true: also detect dependencies that only exist before typescript-to-javascript compilation
"specify": for each dependency identify whether it only exists before compilation or also after
*/
tsPreCompilationDeps: true,
/*
list of extensions to scan that aren't javascript or compile-to-javascript.
Empty by default. Only put extensions in here that you want to take into
account that are _not_ parsable.
*/
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
/* if true combines the package.jsons found from the module up to the base
folder the cruise is initiated from. Useful for how (some) mono-repos
manage dependencies & dependency definitions.
*/
// combinedDependencies: false,
/* if true leave symlinks untouched, otherwise use the realpath */
// preserveSymlinks: false,
/* TypeScript project file ('tsconfig.json') to use for
(1) compilation and
(2) resolution (e.g. with the paths property)
The (optional) fileName attribute specifies which file to take (relative to
dependency-cruiser's current working directory). When not provided
defaults to './tsconfig.json'.
*/
tsConfig: {
fileName: 'tsconfig.json',
},
/* Webpack configuration to use to get resolve options from.
The (optional) fileName attribute specifies which file to take (relative
to dependency-cruiser's current working directory. When not provided defaults
to './webpack.conf.js'.
The (optional) `env` and `arguments` attributes contain the parameters to be passed if
your webpack config is a function and takes them (see webpack documentation
for details)
*/
// webpackConfig: {
// fileName: './webpack.config.js',
// env: {},
// arguments: {},
// },
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
for compilation (and whatever other naughty things babel plugins do to
source code). This feature is well tested and usable, but might change
behavior a bit over time (e.g. more precise results for used module
systems) without dependency-cruiser getting a major version bump.
*/
// babelConfig: {
// fileName: './.babelrc'
// },
/* List of strings you have in use in addition to cjs/ es6 requires
& imports to declare module dependencies. Use this e.g. if you've
re-declared require, use a require-wrapper or use window.require as
a hack.
*/
// exoticRequireStrings: [],
/* options to pass on to enhanced-resolve, the package dependency-cruiser
uses to resolve module references to disk. You can set most of these
options in a webpack.conf.js - this section is here for those
projects that don't have a separate webpack config file.
Note: settings in webpack.conf.js override the ones specified here.
*/
enhancedResolveOptions: {
/* List of strings to consider as 'exports' fields in package.json. Use
['exports'] when you use packages that use such a field and your environment
supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack).
If you have an `exportsFields` attribute in your webpack config, that one
will have precedence over the one specified here.
*/
exportsFields: ['exports'],
/* List of conditions to check for in the exports field. e.g. use ['imports']
if you're only interested in exposed es6 modules, ['require'] for commonjs,
or all conditions at once `(['import', 'require', 'node', 'default']`)
if anything goes for you. Only works when the 'exportsFields' array is
non-empty.
If you have a 'conditionNames' attribute in your webpack config, that one will
have precedence over the one specified here.
*/
conditionNames: ['import', 'require', 'node', 'default'],
/*
The extensions, by default are the same as the ones dependency-cruiser
can access (run `npx depcruise --info` to see which ones that are in
_your_ environment. If that list is larger than what you need (e.g.
it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use
TypeScript you can pass just the extensions you actually use (e.g.
[".js", ".jsx"]). This can speed up the most expensive step in
dependency cruising (module resolution) quite a bit.
*/
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
/*
If your TypeScript project makes use of types specified in 'types'
fields in package.jsons of external dependencies, specify "types"
in addition to "main" in here, so enhanced-resolve (the resolver
dependency-cruiser uses) knows to also look there. You can also do
this if you're not sure, but still use TypeScript. In a future version
of dependency-cruiser this will likely become the default.
*/
mainFields: ['main', 'types'],
},
reporterOptions: {
dot: {
/* pattern of modules that can be consolidated in the detailed
graphical dependency graph. The default pattern in this configuration
collapses everything in node_modules to one folder deep so you see
the external modules, but not the innards your app depends upon.
*/
collapsePattern: 'node_modules/(@[^/]+/[^/]+|[^/]+)',
/* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
for details and some examples. If you don't specify a theme
don't worry - dependency-cruiser will fall back to the default one.
*/
// theme: {
// graph: {
// /* use splines: "ortho" for straight lines. Be aware though
// graphviz might take a long time calculating ortho(gonal)
// routings.
// */
// splines: "true"
// },
// modules: [
// {
// criteria: { matchesFocus: true },
// attributes: {
// fillcolor: "lime",
// penwidth: 2,
// },
// },
// {
// criteria: { matchesFocus: false },
// attributes: {
// fillcolor: "lightgrey",
// },
// },
// {
// criteria: { matchesReaches: true },
// attributes: {
// fillcolor: "lime",
// penwidth: 2,
// },
// },
// {
// criteria: { matchesReaches: false },
// attributes: {
// fillcolor: "lightgrey",
// },
// },
// {
// criteria: { source: "^src/model" },
// attributes: { fillcolor: "#ccccff" }
// },
// {
// criteria: { source: "^src/view" },
// attributes: { fillcolor: "#ccffcc" }
// },
// ],
// dependencies: [
// {
// criteria: { "rules[0].severity": "error" },
// attributes: { fontcolor: "red", color: "red" }
// },
// {
// criteria: { "rules[0].severity": "warn" },
// attributes: { fontcolor: "orange", color: "orange" }
// },
// {
// criteria: { "rules[0].severity": "info" },
// attributes: { fontcolor: "blue", color: "blue" }
// },
// {
// criteria: { resolved: "^src/model" },
// attributes: { color: "#0000ff77" }
// },
// {
// criteria: { resolved: "^src/view" },
// attributes: { color: "#00770077" }
// }
// ]
// }
},
archi: {
/* pattern of modules that can be consolidated in the high level
graphical dependency graph. If you use the high level graphical
dependency graph reporter (`archi`) you probably want to tweak
this collapsePattern to your situation.
*/
collapsePattern:
'^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/(@[^/]+/[^/]+|[^/]+)',
/* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
for details and some examples. If you don't specify a theme
for 'archi' dependency-cruiser will use the one specified in the
dot section (see above), if any, and otherwise use the default one.
*/
// theme: {
// },
},
text: {
highlightFocused: true,
},
},
},
};
// generated: dependency-cruiser@13.0.5 on 2023-07-08T03:48:00.632Z

39
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
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,7 +20,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3

View File

@@ -2,10 +2,6 @@ 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

View File

@@ -1,29 +0,0 @@
# 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@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install -g yarn
- run: yarn install
- run: yarn test

4
.gitignore vendored
View File

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

View File

@@ -8,7 +8,6 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yarn
# Runtime data
pids
*.pid
@@ -81,7 +80,9 @@ typings/
# FuseBox cache
.fusebox/
test
# TypeScript build output
dist
# VisualStudio Config file
.vs

532
.yarn/releases/yarn-3.5.1.cjs → .yarn/releases/yarn-3.5.0.cjs vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,4 @@ enableGlobalCache: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.5.1.cjs
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View File

@@ -1,49 +1,5 @@
# Changelog
## [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)
### Bug Fixes
* autocomplete nested option and merge main ([5fdc1ed](https://github.com/sern-handler/handler/commit/5fdc1eda7f4fcc1f94af7eca661660c0edeb3251))
## [2.6.2](https://github.com/sern-handler/handler/compare/v2.6.1...v2.6.2) (2023-04-15)
### Miscellaneous Chores
* release 2.6.2 ([c1f6906](https://github.com/sern-handler/handler/commit/c1f690633c55ba41db1e035b7c16f9e19c70b385))
## [2.6.1](https://github.com/sern-handler/handler/compare/v2.6.0...v2.6.1) (2023-03-17)

View File

@@ -15,16 +15,19 @@
</div>
## Why?
- 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.
- 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.
- Most handlers don't support discord.js 14.7+
- Customizable, composable commands
- Plug and play or customize to your liking
- Embraces reactive programming for consistent and reliable backend
- Customizable logger, error handling, and more
- Active development and growing [community](https://sern.dev/discord)
## 👀 Quick Look
* Support for discord.js v14 and all interactions
* Hybrid commands
* Lightweight and customizable
* ESM, CommonJS and TypeScript support
* A powerful CLI and awesome community-made plugins
## 📜 Installation
@@ -112,7 +115,6 @@ 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.
## 💻 CLI

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -1,15 +1,15 @@
{
"name": "@sern/handler",
"packageManager": "yarn@3.5.0",
"version": "3.0.2",
"version": "3.0.0-rc1",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"main": "./dist/esm/index.mjs",
"module": "./dist/cjs/index.cjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs",
"types": "./dist/index.d.ts"
}
},
@@ -20,11 +20,10 @@
"format": "eslint src/**/*.ts --fix",
"build:dev": "tsup --metafile",
"build:prod": "tsup --minify",
"prepare": "npm run build:prod",
"publish": "npm run build:prod",
"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"
"test": "vitest --run"
},
"keywords": [
"sern-handler",
@@ -40,18 +39,17 @@
"dependencies": {
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-results-es": "^3.6.1"
"ts-results-es": "^3.6.0"
},
"devDependencies": {
"@faker-js/faker": "^8.0.1",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"dependency-cruiser": "^13.0.5",
"@typescript-eslint/parser": "5.58.0",
"discord.js": "14.11.0",
"esbuild": "^0.17.0",
"eslint": "8.39.0",
"prettier": "2.8.8",
"esbuild-ifdef": "^0.2.0",
"eslint": "8.38.0",
"prettier": "2.8.7",
"tsup": "^6.7.0",
"typescript": "5.0.2",
"vitest": "latest"

View File

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

View File

@@ -1,9 +0,0 @@
export * as Id from './id';
export * from './operators';
export * from './predicates';
export * as Files from './module-loading';
export * from './functions';
export type { VoidResult } from '../types/core-plugin';
export { SernError } from './structures/enums';
export { ModuleStore } from './structures/module-store';
export * as DefaultServices from './structures/services';

View File

@@ -1,7 +0,0 @@
import type { AnyFunction } from '../../types/utility';
export interface Emitter {
addListener(eventName: string | symbol, listener: AnyFunction): this;
removeListener(eventName: string | symbol, listener: AnyFunction): this;
emit(eventName: string | symbol, ...payload: any[]): boolean;
}

View File

@@ -8,10 +8,10 @@ export interface ErrorHandling {
keepAlive: number;
/**
* @deprecated
* @deprecated
* Version 4 will remove this method
*/
crash(err: Error): never;
crash(err :Error) : never
/**
* A function that is called on every crash. Updates keepAlive.
* If keepAlive is 0, the process crashes.

View File

@@ -3,4 +3,3 @@ export * from './logging';
export * from './module-manager';
export * from './module-store';
export * from './init';
export * from './emitter';

View File

@@ -1,4 +1,4 @@
import type { Awaitable } from '../../types/utility';
import { Awaitable } from '../../shared';
/**
* Represents an initialization contract.

View File

@@ -1,22 +1,14 @@
import type {
CommandMeta,
CommandModule,
CommandModuleDefs,
Module,
} from '../../types/core-modules';
import { CommandType } from '../structures';
import { CommandMeta, CommandModule, CommandModuleDefs, Module } from '../types/modules';
/**
* @since 2.0.0
*/
export interface ModuleManager {
get(id: string): string | undefined;
getMetadata(m: Module): CommandMeta | undefined;
getMetadata(m: Module): CommandMeta|undefined;
setMetadata(m: Module, c: CommandMeta): void;
set(id: string, path: string): void;
getPublishableCommands(): Promise<CommandModule[]>;
getByNameCommandType<T extends CommandType>(
name: string,
commandType: T,
): Promise<CommandModuleDefs[T]> | undefined;
getByNameCommandType<T extends CommandType>(name: string, commandType: T): Promise<CommandModuleDefs[T]>|undefined;
}

View File

@@ -1,4 +1,4 @@
import type { CommandMeta, Module } from '../../types/core-modules';
import { CommandMeta, Module } from '../types/modules';
/**
* Represents a core module store that stores IDs mapped to file paths.

View File

@@ -1,5 +1,5 @@
import { CommandType, EventType, PluginType } from './structures';
import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/core-plugin';
import type { Plugin, PluginResult, EventArgs, CommandArgs } from './types/plugins';
import type { ClientEvents } from 'discord.js';
export function makePlugin<V extends unknown[]>(

View File

@@ -1,9 +1,8 @@
import { Err, Ok } from 'ts-results-es';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../types/core-modules';
import type { AnyCommandPlugin, AnyEventPlugin, Plugin } from '../types/core-plugin';
import type { SernAutocompleteData, SernOptionsData } from './types/modules';
import { AnyCommandPlugin, AnyEventPlugin, Plugin } from './types/plugins';
import { PluginType } from './structures';
import assert from 'assert';
//function wrappers for empty ok / err
export const ok = /* @__PURE__*/ () => Ok.EMPTY;
@@ -14,7 +13,6 @@ export function partitionPlugins(
): [Plugin[], Plugin[]] {
const controlPlugins = [];
const initPlugins = [];
for (const el of arr) {
switch (el.type) {
case PluginType.Control:
@@ -38,46 +36,31 @@ export function treeSearch(
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();
const _options = options.slice(); // required to prevent direct mutation of options
let autocompleteData: SernAutocompleteData | undefined;
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);
for (const option of cur.options ?? []) {
_options.push(option);
}
}
break;
default:
{
if ('autocomplete' in cur && cur.autocomplete) {
if (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;
}
if (cur.name === choice.name && cur.autocomplete) {
autocompleteData = cur;
}
}
}
break;
}
}
return autocompleteData;
}

View File

@@ -2,3 +2,27 @@ export * from './contracts';
export * from './create-plugins';
export * from './structures';
export * from './ioc';
export type {
CommandModule,
EventModule,
BothCommand,
ContextMenuMsg,
ContextMenuUser,
SlashCommand,
TextCommand,
ButtonCommand,
StringSelectCommand,
MentionableSelectCommand,
UserSelectCommand,
ChannelSelectCommand,
RoleSelectCommand,
ModalSubmitCommand,
DiscordEventCommand,
SernEventCommand,
ExternalEventCommand,
CommandModuleDefs,
EventModuleDefs,
SernAutocompleteData,
SernOptionsData,
} from './types/modules';
export type { Controller, PluginResult, InitPlugin, ControlPlugin, Plugin } from './types/plugins';

View File

@@ -1,7 +1,7 @@
import * as assert from 'assert';
import { composeRoot, useContainer } from './dependency-injection';
import type { DependencyConfiguration } from '../../types/ioc';
import { CoreContainer } from './container';
import { DependencyConfiguration } from './types';
import { CoreContainer } from '../structures/container';
//SIDE EFFECT: GLOBAL DI
let containerSubject: CoreContainer<Partial<Dependencies>>;

View File

@@ -1,12 +1,16 @@
// 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
// 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 './ioc';
import { CoreDependencies } from './types'
declare global {
interface Dependencies extends CoreDependencies {}
interface Dependencies extends CoreDependencies {}
}

View File

@@ -1,7 +1,12 @@
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
import { SernError, DefaultServices } from '../_internal';
import type {
CoreDependencies,
DependencyConfiguration,
IntoDependencies,
} from './types';
import { DefaultLogging } from '../structures';
import { SernError } from '../structures/errors';
import { useContainerRaw } from './base';
import { CoreContainer } from './container';
import { CoreContainer } from '../structures/container';
/**
* @__PURE__
@@ -22,26 +27,11 @@ export function single<T>(cb: () => T) {
export function transient<T>(cb: () => () => T) {
return cb;
}
/**
* The new Service api, a cleaner alternative to useContainer
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
* Usually our scaffolding tool takes care of this.
* @since 3.0.0
* @example
* ```ts
* const client = Service('@sern/client');
* ```
* @param key a key that corresponds to a dependency registered.
*
*/
export function Service<const T extends keyof Dependencies>(key: T) {
return useContainerRaw().get(key)!;
}
/**
* @since 3.0.0
* The plural version of {@link Service}
* @returns array of dependencies, in the same order of keys provided
*/
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as IntoDependencies<T>;
@@ -61,7 +51,7 @@ export async function composeRoot(
const hasLogger = conf.exclude?.has('@sern/logger');
if (!hasLogger) {
container.upsert({
'@sern/logger': () => new DefaultServices.DefaultLogging(),
'@sern/logger': () => new DefaultLogging(),
});
}
//Build the container based on the callback provided by the user
@@ -80,6 +70,11 @@ export async function composeRoot(
}
export function useContainer<const T extends Dependencies>() {
console.warn(`
Warning: using a container hook (useContainer) is not recommended.
Could lead to many unwanted side effects.
Use the new Service(s) api function instead.
`);
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as IntoDependencies<V>;
}

View File

@@ -1,2 +1,3 @@
export { useContainerRaw, makeDependencies } from './base';
export { Service, Services, single, transient } from './dependency-injection';
export type { Singleton, Transient, CoreDependencies } from './types';

26
src/core/ioc/types.ts Normal file
View File

@@ -0,0 +1,26 @@
import { EventEmitter } from 'node:events';
import { Container, UnpackFunction } from 'iti';
export type Singleton<T> = () => T;
export type Transient<T> = () => () => T;
export interface CoreDependencies {
'@sern/client': () => EventEmitter
'@sern/logger'?: () => import('../contracts').Logging;
'@sern/emitter': () => import('../structures/sern-emitter').SernEmitter;
'@sern/store': () => import('../contracts').CoreModuleStore;
'@sern/modules': () => import('../contracts').ModuleManager;
'@sern/errors': () => import('../contracts').ErrorHandling;
}
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
export type IntoDependencies<Tuple extends [...any[]]> = {
[Index in keyof Tuple]: UnpackFunction<DependencyFromKey<Tuple[Index]> & {}>; //Unpack and make NonNullable
} & { length: Tuple['length'] };
export interface DependencyConfiguration {
//@deprecated. Loggers will always be included in the future
exclude?: Set<'@sern/logger'>;
build: (root: Container<Omit<CoreDependencies, '@sern/client'>, {}>) => Container<Dependencies, {}>;
}

View File

@@ -1,45 +1,38 @@
import { Result } from 'ts-results-es';
import { SernError } from './structures/errors';
import { Result, Err, Ok } from 'ts-results-es';
import { Module } from './types/modules';
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
import { readdir, stat } from 'fs/promises';
import { basename, extname, join, resolve, parse } from 'path';
import assert from 'assert';
import { createRequire } from 'node:module';
import type { ImportPayload, Wrapper } from '../types/core';
import type { Module } from '../types/core-modules';
import { basename, extname, join, resolve } from 'path';
import { ImportPayload } from '../handler/types';
export type ModuleResult<T> = Promise<Result<ImportPayload<T>, SernError>>;
export type ModuleResult<T> = Promise<ImportPayload<T>>;
/**
* 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 module = await import(absPath).then(esm => esm.default);
assert(module, `Found no export for module at ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in module) {
module = module.default;
}
return Result
.wrap(() => module.getInstance())
.unwrapOr(module) as T;
// prettier-ignore
let module =
/// #if MODE === 'esm'
import(absPath).then(i => i.default); // eslint-disable-line
/// #elif MODE === 'cjs'
require(absPath).default; // eslint-disable-line
/// #endif
return module.then(m =>
Result
.wrap(() => m.getInstance())
.unwrapOr(m)
) as T;
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let module = await importModule<T>(absPath);
assert(module, `Found an undefined module: ${absPath}`);
return { module, absPath };
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
//todo readd class modules
return Ok({ module, absPath });
}
export const fmtFileName = (fileName: string) => parse(fileName).name;
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
* a directory string is converted into a stream of modules.
@@ -49,44 +42,58 @@ export const fmtFileName = (fileName: string) => parse(fileName).name;
*/
export function buildModuleStream<T extends Module>(
input: ObservableInput<string>,
): Observable<ImportPayload<T>> {
): Observable<Result<ImportPayload<T>, SernError>> {
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
}
export const getFullPathTree = (dir: string, mode: boolean) => readPaths(resolve(dir), mode);
export function getFullPathTree(dir: string, mode: boolean) {
return readPaths(resolve(dir), mode);
}
export const filename = (path: string) => fmtFileName(basename(path));
export function filename(path: string) {
return fmtFileName(basename(path));
}
const isSkippable = (filename: string) => {
//empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
};
function createSkipCondition(base: string) {
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', 'cts'];
return ( type: 'file' | 'directory') => {
if(type === 'file') {
return fmtFileName(base)[0] === '!'
|| !validExtensions.includes(extname(base));
}
return base[0] === '!';
}
}
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file)
}
}
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
const isSkippable = createSkipCondition(base);
if (fileStats.isDirectory()) {
//Todo: refactor so that i dont repeat myself for files (line 71)
if (isSkippable(base)) {
if (isSkippable('directory')) {
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
} else {
yield* readPaths(fullPath, shouldDebug);
}
} else {
if (isSkippable(base)) {
if (isSkippable('file')) {
if (shouldDebug) console.info(`ignored: ${fullPath}`);
} else {
/// #if MODE === 'esm'
yield 'file:///' + fullPath;
/// #elif MODE === 'cjs'
yield fullPath;
/// #endif
}
}
}
@@ -94,42 +101,3 @@ async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<str
throw err;
}
}
const requir = createRequire(import.meta.url);
export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
if (wrapper === 'file') {
console.log('Experimental loading of sern.config.json');
const config = requir(resolve('sern.config.json')) as {
language: string;
defaultPrefix?: string;
mode?: 'PROD' | 'DEV';
paths: {
base: string;
commands: string;
events?: string;
};
};
const makePath = (dir: keyof typeof config.paths) =>
config.language === 'typescript'
? join('dist', config.paths[dir]!)
: join(config.paths[dir]!);
console.log('Loading config: ', config);
const commandsPath = makePath('commands');
console.log('Commands path is set to', commandsPath);
let eventsPath: string | undefined;
if (config.paths.events) {
eventsPath = makePath('events');
console.log('Events path is set to', eventsPath);
}
return {
defaultPrefix: config.defaultPrefix,
commands: commandsPath,
events: eventsPath,
mode: config.mode,
};
}
return wrapper;
}

View File

@@ -15,11 +15,14 @@ import {
OperatorFunction,
pipe,
share,
switchMap,
} from 'rxjs';
import { Emitter, ErrorHandling, Logging } from './contracts';
import { Result } from 'ts-results-es';
import { EventEmitter } from 'node:events';
import { ErrorHandling, Logging } from './contracts';
import util from 'node:util';
import type { PluginResult, VoidResult } from '../types/core-plugin';
import type { Result } from 'ts-results-es'
import { Awaitable } from '../shared';
import { PluginResult, VoidResult } from './types/plugins';
/**
* if {src} is true, mapTo V, else ignore
* @param item
@@ -28,6 +31,20 @@ export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
export function filterMap<In, Out>(
cb: (i: In) => Awaitable<Result<Out, unknown>>,
): OperatorFunction<In, Out> {
return pipe(
switchMap(async input => cb(input)),
concatMap(s => {
if (s.ok) {
return of(s.val);
}
return EMPTY;
}),
);
}
/**
* Calls any plugin with {args}.
* @param args if an array, its spread and plugin called.
@@ -48,6 +65,23 @@ export function callPlugin(args: unknown): OperatorFunction<
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
/**
* 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.
*/
@@ -56,7 +90,7 @@ export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
defaultIfEmpty(true),
);
export const sharedEventStream = <T>(e: Emitter, eventName: string) => {
export const sharedObservable = <T>(e: EventEmitter, eventName: string) => {
return (fromEvent(e, eventName) as Observable<T>).pipe(share());
};
@@ -70,17 +104,3 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
return caught;
};
}
// Temporary until i get rxjs operators working on ts-results-es
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
pipe(
concatMap(result => {
if(result.ok) {
return of(result.val)
}
onErr(result.val);
return EMPTY
})
)

View File

@@ -1,4 +1,4 @@
import type {
import {
AnySelectMenuInteraction,
AutocompleteInteraction,
ButtonInteraction,

View File

@@ -1,10 +1,9 @@
import { Container } from 'iti';
import { SernEmitter } from '../';
import { DefaultErrorHandling, DefaultModuleManager, SernEmitter } from '../';
import { isAsyncFunction } from 'node:util/types';
import * as assert from 'node:assert';
import { Subject } from 'rxjs';
import { DefaultServices, ModuleStore } from '../_internal';
import { ModuleStore } from './module-store';
/**
* Provides all the defaults for sern to function properly.
@@ -12,7 +11,6 @@ import { DefaultServices, ModuleStore } from '../_internal';
*/
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
private ready$ = new Subject<never>();
private beenCalled = new Set<PropertyKey>();
constructor() {
super();
@@ -20,15 +18,12 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
(this as Container<{}, {}>)
.add({
'@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/errors': () => new DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/store': () => new ModuleStore(),
})
.add(ctx => {
return {
'@sern/modules': () =>
new DefaultServices.DefaultModuleManager(ctx['@sern/store']),
};
return { '@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']) };
});
}
@@ -37,7 +32,7 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
!this.isReady(),
'listening for init functions should only occur prior to sern being ready.',
);
const unsubscriber = this.on('containerUpserted', e => this.callInitHooks(e));
const unsubscriber = this.on('containerUpserted', this.callInitHooks);
this.ready$.subscribe({
complete: unsubscriber,
@@ -46,15 +41,15 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
private async callInitHooks(e: { key: keyof T; newContainer: T[keyof T] | null }) {
const dep = e.newContainer;
assert.ok(dep);
//Ignore any dependencies that are not objects or array
if (typeof dep !== 'object' || Array.isArray(dep)) {
return;
}
if ('init' in dep && typeof dep.init === 'function' && !this.beenCalled.has(e.key)) {
if ('init' in dep && typeof dep.init === 'function') {
isAsyncFunction(dep.init) ? await dep.init() : dep.init();
this.beenCalled.add(e.key);
}
}

View File

@@ -8,11 +8,10 @@ import {
Snowflake,
User,
} from 'discord.js';
import { CoreContext } from '../structures/core-context';
import { CoreContext } from './core-context';
import { Result, Ok, Err } from 'ts-results-es';
import * as assert from 'assert';
type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
import { ReplyOptions } from '../../shared';
/**
* @since 1.0.0

View File

@@ -1,5 +1,5 @@
import { Result as Either } from 'ts-results-es';
import { SernError } from '../_internal';
import { SernError } from './errors';
import * as assert from 'node:assert';
/**
@@ -26,6 +26,8 @@ export abstract class CoreContext<M, I> {
//todo: add agnostic options resolver for Context
abstract get options(): unknown;
abstract get id(): string;
static wrap(_: unknown): unknown {
throw Error('You need to override this method; cannot wrap an abstract class');
}

View File

@@ -23,10 +23,10 @@ export enum CommandType {
Button = 1 << 4,
StringSelect = 1 << 5,
Modal = 1 << 6,
UserSelect = 1 << 7,
RoleSelect = 1 << 8,
MentionableSelect = 1 << 9,
ChannelSelect = 1 << 10,
ChannelSelect = 1 << 7,
MentionableSelect = 1 << 8,
RoleSelect = 1 << 9,
UserSelect = 1 << 10,
}
/**
@@ -101,42 +101,3 @@ export enum PayloadType {
*/
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

@@ -0,0 +1,38 @@
/**
* @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,4 +1,4 @@
export { CommandType, PluginType, PayloadType, EventType } from './enums';
export * from './enums';
export * from './context';
export * from './sern-emitter';
export * from './services';

View File

@@ -1,5 +1,5 @@
import { CommandMeta, Module } from '../../types/core-modules';
import { CoreModuleStore } from '../contracts';
import { Module, CommandMeta } from '../types/modules';
/*
* @internal

View File

@@ -1,7 +1,7 @@
import { EventEmitter } from 'node:events';
import { PayloadType } from '../../core/structures';
import { Module } from '../../types/core-modules';
import { SernEventsMapping, Payload } from '../../types/utility';
import { Payload, SernEventsMapping } from '../../shared';
import { Module } from '../types/modules';
/**
* @since 1.0.0

View File

@@ -6,16 +6,17 @@ import { ErrorHandling } from '../../contracts';
* Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
*/
export class DefaultErrorHandling implements ErrorHandling {
crash(err: Error): never {
throw err;
throw err;
}
keepAlive = 5;
updateAlive(err: Error) {
this.keepAlive--;
if (this.keepAlive === 0) {
throw err;
if(this.keepAlive === 0) {
throw err;
}
}
}

View File

@@ -1,7 +1,7 @@
import * as Id from '../../../core/id';
import { createId } from '../../../handler/id';
import { CoreModuleStore, ModuleManager } from '../../contracts';
import { Files } from '../../_internal';
import { CommandMeta, CommandModule, CommandModuleDefs, Module } from '../../../types/core-modules';
import { importModule } from '../../module-loading';
import { CommandMeta, CommandModule, CommandModuleDefs, Module } from '../../types/modules';
import { CommandType } from '../enums';
/**
* @internal
@@ -12,11 +12,11 @@ export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: CoreModuleStore) {}
getByNameCommandType<T extends CommandType>(name: string, commandType: T) {
const id = this.get(Id.create(name, commandType));
if (!id) {
const id = this.get(createId(name, commandType));
if(!id) {
return undefined;
}
return Files.importModule<CommandModuleDefs[T]>(id);
return importModule<CommandModuleDefs[T]>(id);
}
setMetadata(m: Module, c: CommandMeta): void {
@@ -26,11 +26,12 @@ export class DefaultModuleManager implements ModuleManager {
getMetadata(m: Module): CommandMeta {
const maybeModule = this.moduleStore.metadata.get(m);
if (!maybeModule) {
throw Error('Could not find metadata in store for ' + m);
throw Error('Could not find metadata in store for ' + maybeModule);
}
return maybeModule;
}
get(id: string) {
return this.moduleStore.commands.get(id);
}
@@ -44,7 +45,7 @@ export class DefaultModuleManager implements ModuleManager {
return Promise.all(
Array.from(entries)
.filter(([id]) => !(Number.parseInt(id.at(-1)!) & publishable))
.map(([, path]) => Files.importModule<CommandModule>(path)),
.map(([, path]) => importModule<CommandModule>(path)),
);
}
}

View File

@@ -3,6 +3,8 @@ import type {
APIApplicationCommandOptionBase,
ApplicationCommandOptionType,
BaseApplicationCommandOptionsData,
} from 'discord.js';
import {
AutocompleteInteraction,
ButtonInteraction,
ChannelSelectMenuInteraction,
@@ -15,17 +17,19 @@ import type {
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { CommandType, Context, EventType } from '../../src/core';
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin';
import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility';
import { CommandType, Context, EventType } from '../structures';
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './plugins';
import { Awaitable, SernEventsMapping } from '../../shared';
import { Processed } from '../../handler/types';
import { Args, SlashOptions } from '../../shared';
export interface CommandMeta {
fullPath: string;
id: string;
isClass: boolean;
isClass: boolean
}
export type Processed<T> = T & { name: string; description: string };
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
export interface Module {
type: CommandType | EventType;
@@ -33,7 +37,7 @@ export interface Module {
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
execute(...args: any[]): Awaitable<any>;
execute: (...args: any[]) => Awaitable<any>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
@@ -143,6 +147,7 @@ export type CommandModule =
| 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 interface CommandModuleDefs {
@@ -188,11 +193,10 @@ export type InputEvent = {
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & {
plugins?: AnyCommandPlugin[];
};
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
@@ -202,8 +206,7 @@ export type SernOptionsData =
| APIApplicationCommandBasicOption
| SernAutocompleteData;
export interface SernSubCommandData
extends APIApplicationCommandOptionBase<ApplicationCommandOptionType.Subcommand> {
export interface SernSubCommandData extends APIApplicationCommandOptionBase<ApplicationCommandOptionType.Subcommand> {
type: ApplicationCommandOptionType.Subcommand;
options?: SernOptionsData[];
}

View File

@@ -24,17 +24,16 @@ import type {
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
Module,
Processed,
RoleSelectCommand,
SernEventCommand,
SlashCommand,
StringSelectCommand,
TextCommand,
UserSelectCommand,
} from './core-modules';
import { Args, Awaitable, Payload, SlashOptions } from './utility';
import { CommandType, Context, EventType, PluginType } from '../core';
} from './modules';
import { Args, Awaitable, Payload, SlashOptions } from '../../shared';
import { CommandType, Context, EventType, PluginType } from '../structures';
import { InitArgs, Processed } from '../../handler/types';
import {
ButtonInteraction,
ChannelSelectMenuInteraction,
@@ -51,10 +50,6 @@ import {
export type PluginResult = Awaitable<VoidResult>;
export type VoidResult = Result<void, void>;
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;

View File

@@ -1,22 +1,22 @@
import { ClientEvents } from 'discord.js';
import { CommandType, EventType, PluginType } from '../core/structures';
import type {
import {
AnyCommandPlugin,
AnyEventPlugin,
CommandArgs,
ControlPlugin,
EventArgs,
InitPlugin,
} from '../types/core-plugin';
import type {
} from '../core/types/plugins';
import {
CommandModule,
EventModule,
InputCommand,
InputEvent,
Module,
} from '../types/core-modules';
import { partitionPlugins } from './_internal';
import type { Awaitable } from '../types/utility';
} from '../core/types/modules';
import { partitionPlugins } from '../core/functions';
import { Awaitable } from '../shared';
/**
* @since 1.0.0 The wrapper function to define command modules for sern

View File

@@ -1,18 +1,17 @@
import { EventEmitter } from 'node:events';
import * as assert from 'node:assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import {
arrayifySource,
callPlugin,
isAutocomplete,
treeSearch,
SernError,
} from '../core/_internal';
import { createResultResolver } from './event-utils';
import { arrayifySource, callPlugin } from '../../core/operators';
import { createResultResolver } from './generic';
import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js';
import { CommandType, Context } from '../core';
import type { Args } from '../types/utility';
import type { BothCommand, CommandModule, Module, Processed } from '../types/core-modules';
import { treeSearch } from '../../core/functions';
import { SernError } from '../../core/structures/errors';
import { CommandType, Context } from '../../core';
import { isAutocomplete } from '../../core/predicates';
import { Processed } from '../types';
import { BothCommand, CommandModule, Module } from '../../core/types/modules';
import { Args } from '../../shared';
function dispatchInteraction<T extends CommandModule, V extends BaseInteraction | Message>(
payload: { module: Processed<T>; event: V },
@@ -36,14 +35,15 @@ function dispatchAutocomplete(payload: {
event: AutocompleteInteraction;
}) {
const option = treeSearch(payload.event, payload.module.options);
assert.ok(
option,
Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
if (option !== undefined) {
return {
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [payload.event],
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
return {
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [payload.event],
};
}
export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
@@ -93,11 +93,13 @@ export function createDispatcher(payload: {
module: Processed<CommandModule>;
event: BaseInteraction;
}) {
assert.ok(
CommandType.Text !== payload.module.type,
SernError.MismatchEvent + 'Found text command in interaction stream',
);
switch (payload.module.type) {
case CommandType.Text:
throw Error(
SernError.MismatchEvent +
' Found a text module in interaction stream. ' +
payload.module,
);
case CommandType.Slash:
case CommandType.Both: {
if (isAutocomplete(payload.event)) {

View File

@@ -12,50 +12,29 @@ import {
catchError,
finalize,
} from 'rxjs';
import {
Files,
Id,
callPlugin,
everyPluginOk,
filterMapTo,
handleError,
SernError,
VoidResult,
} from '../core/_internal';
import { Emitter, ErrorHandling, Logging, ModuleManager, useContainerRaw } from '../core';
import { ErrorHandling, Logging, ModuleManager, useContainerRaw } from '../../core';
import { SernError } from '../../core/structures/errors';
import { callPlugin, everyPluginOk, filterMap, filterMapTo, handleError } from '../../core/operators';
import { defaultModuleLoader } from '../../core/module-loading';
import { CommandModule, Module, AnyModule } from '../../core/types/modules';
import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
import { ObservableInput, pipe } from 'rxjs';
import { SernEmitter } from '../core';
import { Err, Ok, Result } from 'ts-results-es';
import type { Awaitable } from '../types/utility';
import type { ControlPlugin } from '../types/core-plugin';
import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules';
import type { ImportPayload } from '../types/core';
import { ObservableInput, pipe, switchMap } from 'rxjs';
import { SernEmitter } from '../../core';
import { errTap } from '../../core/operators';
import * as Files from '../../core/module-loading';
import { Err, Result } from 'ts-results-es';
import { fmt } from './messages';
import { ControlPlugin, VoidResult } from '../../core/types/plugins';
import { ImportPayload, Processed } from '../types';
import { Awaitable } from '../../shared';
import { createId, reconstructId } from '../id';
function createGenericHandler<Source, Narrowed extends Source, Output>(
source: Observable<Source>,
makeModule: (event: Narrowed) => Promise<Output>,
makeModule: (event: Narrowed) => Awaitable<Result<Output, unknown>>,
) {
return (pred: (i: Source) => i is Narrowed) =>
source.pipe(
filter(pred),
concatMap(makeModule));
return (pred: (i: Source) => i is Narrowed) => source.pipe(filter(pred), filterMap(makeModule));
}
/**
* 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);
}
/**
*
* Creates an RxJS observable that filters and maps incoming interactions to their respective modules.
@@ -67,18 +46,15 @@ export function createInteractionHandler<T extends Interaction>(
source: Observable<Interaction>,
mg: ModuleManager,
) {
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
return createGenericHandler<Interaction, T, ReturnType<typeof createDispatcher>>(
source,
async event => {
const fullPath = mg.get(Id.reconstruct(event));
if(!fullPath) {
return Err.EMPTY
}
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload =>
Ok(createDispatcher({ module: payload.module, event }))
);
event => {
const fullPath = mg.get(reconstructId(event as unknown as Interaction));
if (!fullPath)
return Err(SernError.UndefinedModule + ' No full path found in module store');
return defaultModuleLoader<Processed<CommandModule>>(fullPath).then(res =>
res.map(payload => createDispatcher({ module: payload.module, event })),
);
},
);
}
@@ -88,19 +64,16 @@ export function createMessageHandler(
defaultPrefix: string,
mg: ModuleManager,
) {
return createGenericHandler(source, async event => {
return createGenericHandler(source, event => {
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
const fullPath = mg.get(`${prefix}_A1`);
if(!fullPath) {
return Err('Possibly undefined behavior: could not find a static id to resolve ')
const fullPath = mg.get(`${prefix}_A0`);
if (fullPath === undefined) {
return Err(SernError.UndefinedModule + ' No full path found in module store');
}
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload => {
const args = contextArgs(event, rest);
return Ok(dispatchMessage(payload.module, args));
});
return defaultModuleLoader<Processed<CommandModule>>(fullPath).then(result => {
const args = contextArgs(event, rest);
return result.map(payload => dispatchMessage(payload.module, args));
});
});
}
/**
@@ -116,16 +89,36 @@ function assignDefaults<T extends Module>(
moduleManager.setMetadata(module, {
isClass: module.constructor.name === 'Function',
fullPath: absPath,
id: Id.create(module.name, module.type),
id: createId(module.name, module.type),
});
});
}
export function buildModules<T extends AnyModule>(
input: ObservableInput<string>,
sernEmitter: SernEmitter,
moduleManager: ModuleManager,
) {
return Files.buildModuleStream<Processed<T>>(input).pipe(assignDefaults(moduleManager));
return pipe(
switchMap(() => Files.buildModuleStream<T>(input)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
assignDefaults<T>(moduleManager),
);
}
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 isNonBot(prefix: string) {
return ({ author, content }: Message) => !author.bot && hasPrefix(prefix, content);
}
/**
@@ -137,7 +130,7 @@ export function buildModules<T extends AnyModule>(
* @param task the deferred execution which will be called
*/
export function executeModule(
emitter: Emitter,
emitter: SernEmitter,
{
module,
task,
@@ -194,20 +187,14 @@ export function createResultResolver<
* Calls a module's init plugins and checks for Err. If so, call { onStop } and
* ignore the module
*/
export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emitter) {
export function callInitPlugins<
T extends Processed<AnyModule>,
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)),
onStop: (module: T) => {
sernEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onNext: ({ module }) => {
sernEmitter.emit('module.register', SernEmitter.success(module));
return module;
},
...config,
}),
);
}
@@ -220,10 +207,7 @@ 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,
});
const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return concatMap(
createResultResolver({
onStop,
@@ -233,15 +217,16 @@ export function makeModuleExecutor<
);
}
export const handleCrash = (err: ErrorHandling, log?: Logging) =>
pipe(
catchError(handleError(err, log)),
export function handleCrash(
errorHandler: ErrorHandling,
logger?: Logging,
) {
return pipe(
catchError(handleError(errorHandler, logger)),
finalize(() => {
log?.info({
message: 'A stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
logger?.info({ message: 'A stream closed or reached end of lifetime' });
useContainerRaw()?.disposeAll()
.then(() => logger?.info({ message: 'Cleaning container and crashing' }));
})
);
}

View File

@@ -0,0 +1,26 @@
import { Interaction } from 'discord.js';
import { concatMap, merge } from 'rxjs';
import { SernError } from '../../core/structures/errors';
import { SernEmitter } from '../../core';
import { sharedObservable } from '../../core/operators';
import { isAutocomplete, isCommand, isMessageComponent, isModal } from '../../core/predicates';
import { createInteractionHandler, executeModule, makeModuleExecutor } from './generic';
import { DependencyList } from '../types';
export function makeInteractionHandler([emitter, , , modules, client]: DependencyList) {
const interactionStream$ = sharedObservable<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler(interactionStream$, modules);
const interactionHandler$ = merge(
handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal),
);
return interactionHandler$.pipe(
makeModuleExecutor(module => {
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(emitter, payload)),
);
}

View File

@@ -0,0 +1,42 @@
import { concatMap, EMPTY } from 'rxjs';
import { SernError } from '../../core/structures/errors';
import type { Message } from 'discord.js';
import { SernEmitter } from '../../core';
import { sharedObservable } from '../../core/operators';
import { createMessageHandler, executeModule, isNonBot, makeModuleExecutor } from './generic';
import { DependencyList } from '../types';
/**
* 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);
}
export function makeMessageHandler(
[emitter, , log, modules, client]: DependencyList,
defaultPrefix: string | undefined,
) {
if (!defaultPrefix) {
log?.debug({ message: 'No prefix found. message handler shutting down' });
return EMPTY;
}
const messageStream$ = sharedObservable<Message>(client, 'messageCreate');
const handler = createMessageHandler(messageStream$, defaultPrefix, modules);
const prefixedMessages$ = handler(isNonBot(defaultPrefix) as (m: Message) => m is Message);
return prefixedMessages$.pipe(
makeModuleExecutor(module => {
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(emitter, payload)),
);
}

View File

@@ -0,0 +1,55 @@
import { ObservableInput, fromEvent, take } from 'rxjs';
import { CommandType } from '../../core/structures';
import { SernError } from '../../core/structures/errors';
import { Result } from 'ts-results-es';
import { ModuleManager } from '../../core/contracts';
import { SernEmitter } from '../../core';
import { Processed, DependencyList } from '../types';
import { buildModules, callInitPlugins } from './generic';
import { AnyModule } from '../../core/types/modules';
import * as assert from 'node:assert';
export function startReadyEvent(
[sEmitter, , , moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
const ready$ = fromEvent(client!, 'ready').pipe(take(1));
return ready$
.pipe(
buildModules<Processed<AnyModule>>(allPaths, sEmitter, moduleManager),
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);
if (result.err) {
throw Error(SernError.InvalidModuleType + ' ' + result.val);
}
});
}
function registerModule<T extends Processed<AnyModule>>(
manager: ModuleManager,
module: T,
): Result<void, void> {
const { id, fullPath } = manager.getMetadata(module);
assert.ok(
module.type > 0 && module.type < 1 << 10,
`Found ${module.name} at ${fullPath}, which does not have a valid type`,
);
if (module.type === CommandType.Both || module.type === CommandType.Text) {
module.alias?.forEach(a => manager.set(`${a}_A0`, fullPath));
}
return Result.wrap(() => manager.set(id, fullPath));
}

View File

@@ -0,0 +1,59 @@
import { ObservableInput, catchError, finalize, map, mergeAll, of } from 'rxjs';
import type { CommandModule, EventModule } from '../../core/types/modules';
import { SernEmitter } from '../../core';
import { EventType } from '../../core/structures';
import { SernError } from '../../core/structures/errors';
import { eventDispatcher } from './dispatchers';
import { buildModules, callInitPlugins } from './generic';
import { handleError } from '../../core/operators';
import { Service, useContainerRaw } from '../../core/ioc';
import { DependencyList, Processed } from '../types';
export function makeEventsHandler(
[emitter, err, log, moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
//code smell
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch (e.type) {
case EventType.Sern:
return eventDispatcher(e, emitter);
case EventType.Discord:
return eventDispatcher(e, client);
case EventType.External:
return eventDispatcher(e, Service(e.emitter));
default:
throw Error(SernError.InvalidModuleType + ' while creating event handler');
}
};
of(null)
.pipe(
buildModules<Processed<EventModule>>(allPaths, emitter, moduleManager),
callInitPlugins({
onStop: module =>
emitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
),
onNext: ({ module }) => {
emitter.emit('module.register', SernEmitter.success(module));
return module;
},
}),
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();
}

View File

@@ -1,12 +1,12 @@
import { ApplicationCommandType, ComponentType, Interaction, InteractionType } from 'discord.js';
import { CommandType, EventType } from './structures';
import { Interaction, InteractionType } from 'discord.js';
import { CommandType, EventType } from '../core';
/**
* Construct unique ID for a given interaction object.
* @param event The interaction object for which to create an ID.
* @returns A unique string ID based on the type and properties of the interaction object.
*/
export function reconstruct<T extends Interaction>(event: T) {
export function reconstructId<T extends Interaction>(event: T) {
switch (event.type) {
case InteractionType.MessageComponent: {
return `${event.customId}_C${event.componentType}`;
@@ -21,27 +21,8 @@ export function reconstruct<T extends Interaction>(event: T) {
}
}
}
/**
*
* A magic number to represent any commandtype that is an ApplicationCommand.
*/
const appBitField = 0b000000001111;
// Each index represents the exponent of a CommandType.
// Every CommandType is a power of two.
export const CommandTypeDiscordApi = [
1, // CommandType.Text
ApplicationCommandType.ChatInput,
ApplicationCommandType.User,
ApplicationCommandType.Message,
ComponentType.Button,
ComponentType.StringSelect,
1, // CommandType.Modal
ComponentType.UserSelect,
ComponentType.RoleSelect,
ComponentType.MentionableSelect,
ComponentType.ChannelSelect,
];
const appBitField = 0b000000011111;
/*
* Generates a number based on CommandType.
* This corresponds to an ApplicationCommandType or ComponentType
@@ -49,15 +30,21 @@ export const CommandTypeDiscordApi = [
*/
function apiType(t: CommandType | EventType) {
if (t === CommandType.Both || t === CommandType.Modal) return 1;
return CommandTypeDiscordApi[Math.log2(t)];
const log = Math.log2(t);
return (appBitField & t) !== 0 ? log : log - 2;
}
/*
* Generates an id based on name and CommandType.
* Generates an id based on CommandType.
* A is for any ApplicationCommand. C is for any ComponentCommand
* Then, another number generated by apiType function is appended
*/
export function create(name: string, type: CommandType | EventType) {
const am = (appBitField & type) !== 0 ? 'A' : 'C';
return name + '_' + am + apiType(type);
function uniqueSuffix(t: CommandType | EventType) {
const am = (appBitField & t) !== 0 ? 'A' : 'C';
return am + apiType(t);
}
export function createId(name: string, type: CommandType | EventType) {
return name+"_"+uniqueSuffix(type)
}

89
src/handler/sern.ts Normal file
View File

@@ -0,0 +1,89 @@
import { makeEventsHandler } from './events/user-defined';
import { makeInteractionHandler } from './events/interactions';
import { startReadyEvent } from './events/ready';
import { makeMessageHandler } from './events/messages';
import { err, ok } from '../core/functions';
import { getFullPathTree } from '../core/module-loading';
import { merge } from 'rxjs';
import { Services } from '../core/ioc';
import { Wrapper } from '../shared';
import { handleCrash } from './events/generic';
/**
* @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({
* commands: 'dist/commands',
* events: 'dist/events',
* })
* ```
*/
export function init(wrapper: Wrapper) {
const startTime = performance.now();
const dependencies = useDependencies();
const logger = dependencies[2];
const errorHandler = dependencies[1];
const mode = debugModuleLoading(wrapper.mode ?? process.env.MODE);
if (wrapper.events !== undefined) {
makeEventsHandler(dependencies, getFullPathTree(wrapper.events, mode));
}
startReadyEvent(dependencies, getFullPathTree(wrapper.commands, mode))
.add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded' );
logger?.info({
message: `sern: registered all modules in ${time} s`,
});
});
const messages$ = makeMessageHandler(dependencies, wrapper.defaultPrefix);
const interactions$ = makeInteractionHandler(dependencies);
merge(messages$, interactions$)
.pipe(handleCrash(errorHandler, logger))
.subscribe();
}
function debugModuleLoading(mode: string | undefined) {
console.info(`Detected mode: "${mode}"`);
if (mode === undefined) {
console.info('No mode found in process.env, assuming DEV');
}
switch (mode) {
case 'PROD':
return false;
case 'DEV':
case undefined:
return true;
default: {
console.warn(mode + ' is not a valid. Should be PROD or DEV');
return false;
}
}
}
function useDependencies() {
return Services(
'@sern/emitter',
'@sern/errors',
'@sern/logger',
'@sern/modules',
'@sern/client',
);
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: ok,
stop: err,
};

24
src/handler/types.ts Normal file
View File

@@ -0,0 +1,24 @@
import { ErrorHandling, Logging, ModuleManager, SernEmitter } from '../core';
import { EventEmitter } from 'node:events';
import { Module } from '../core/types/modules';
export type Processed<T> = T & { name: string; description: string };
export type DependencyList = [
SernEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
EventEmitter,
];
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
export interface ImportPayload<T> {
module: T;
absPath: string;
[key: string]: unknown;
}

View File

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

View File

@@ -1,33 +0,0 @@
import { Interaction } from 'discord.js';
import { concatMap, merge } from 'rxjs';
import { SernEmitter } from '../core';
import {
isAutocomplete,
isCommand,
isMessageComponent,
isModal,
sharedEventStream,
SernError,
filterTap,
} from '../core/_internal';
import { createInteractionHandler, executeModule, makeModuleExecutor } from './_internal';
import type { DependencyList } from '../types/ioc';
export function interactionHandler([emitter, , , modules, client]: DependencyList) {
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler(interactionStream$, modules);
const interactionHandler$ = merge(
handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal),
);
return interactionHandler$
.pipe(
filterTap(e => emitter.emit('warning', SernEmitter.warning(e))),
makeModuleExecutor(module =>
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure))),
concatMap(payload => executeModule(emitter, payload)),
);
}

View File

@@ -1,47 +0,0 @@
import { concatMap, EMPTY } from 'rxjs';
import type { Message } from 'discord.js';
import { SernEmitter } from '../core';
import { sharedEventStream, SernError, filterTap } from '../core/_internal';
import { createMessageHandler, executeModule, makeModuleExecutor } from './_internal';
import type { DependencyList } from '../types/ioc';
/**
* 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 function messageHandler(
[emitter, , log, modules, client]: DependencyList,
defaultPrefix: string | undefined,
) {
if (!defaultPrefix) {
log?.debug({
message: 'No prefix found. message handler shutting down',
});
return EMPTY;
}
const messageStream$ = sharedEventStream<Message>(client, 'messageCreate');
const handle = createMessageHandler(messageStream$, defaultPrefix, modules);
const msgCommands$ = handle(isNonBot(defaultPrefix));
return msgCommands$.pipe(
filterTap((e) => emitter.emit('warning', SernEmitter.warning(e))),
makeModuleExecutor(module => {
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(emitter, payload)),
);
}

View File

@@ -1,48 +0,0 @@
import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe } from 'rxjs';
import { CommandType } from '../core/structures';
import { SernError } from '../core/_internal';
import { Result } from 'ts-results-es';
import { ModuleManager } from '../core/contracts';
import { buildModules, callInitPlugins } from './_internal';
import * as assert from 'node:assert';
import * as util from 'node:util';
import type { DependencyList } from '../types/ioc';
import type { AnyModule, Processed } from '../types/core-modules';
export function startReadyEvent(
[sEmitter, , , moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
const ready$ = fromEvent(client!, 'ready').pipe(once());
return concat(ready$, buildModules<AnyModule>(allPaths, moduleManager))
.pipe(callInitPlugins(sEmitter))
.subscribe(module => {
register(moduleManager, module).expect(
SernError.InvalidModuleType + ' ' + util.inspect(module),
);
});
}
const once = () => pipe(
first(),
ignoreElements()
)
function register<T extends Processed<AnyModule>>(
manager: ModuleManager,
module: T,
): Result<void, void> {
const { id, fullPath } = manager.getMetadata(module)!;
const validModuleType = module.type >= 0 && module.type <= 1 << 10;
assert.ok(
validModuleType,
`Found ${module.name} at ${fullPath}, which does not have a valid type`,
);
if (module.type === CommandType.Both || module.type === CommandType.Text) {
module.alias?.forEach(a => manager.set(`${a}_A1`, fullPath));
}
return Result.wrap(() => manager.set(id, fullPath));
}

View File

@@ -1,37 +0,0 @@
import { ObservableInput, map, mergeAll } from 'rxjs';
import { EventType } from '../core/structures';
import { SernError } from '../core/_internal';
import { buildModules, callInitPlugins, handleCrash, eventDispatcher } from './_internal';
import { Service } from '../core/ioc';
import type { DependencyList } from '../types/ioc';
import type { CommandModule, EventModule, Processed } from '../types/core-modules';
export function eventsHandler(
[emitter, err, log, moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
//code smell
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch (e.type) {
case EventType.Sern:
return eventDispatcher(e, emitter);
case EventType.Discord:
return eventDispatcher(e, client);
case EventType.External:
return eventDispatcher(e, Service(e.emitter));
default:
throw Error(SernError.InvalidModuleType + ' while creating event handler');
}
};
buildModules<EventModule>(allPaths, moduleManager)
.pipe(
callInitPlugins(emitter),
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
handleCrash(err, log),
)
.subscribe();
}

View File

@@ -1,53 +1,11 @@
export * as Sern from './sern';
export * as Sern from './handler/sern';
export * from './core';
export type {
CommandModule,
EventModule,
BothCommand,
ContextMenuMsg,
ContextMenuUser,
SlashCommand,
TextCommand,
ButtonCommand,
StringSelectCommand,
MentionableSelectCommand,
UserSelectCommand,
ChannelSelectCommand,
RoleSelectCommand,
ModalSubmitCommand,
DiscordEventCommand,
SernEventCommand,
ExternalEventCommand,
CommandModuleDefs,
EventModuleDefs,
SernAutocompleteData,
SernOptionsData,
SernSubCommandData,
SernSubCommandGroupData,
} from './types/core-modules';
export type {
Controller,
PluginResult,
InitPlugin,
ControlPlugin,
Plugin,
AnyEventPlugin,
AnyCommandPlugin,
} from './types/core-plugin';
export type { Wrapper } from './types/core';
export type { Args, SlashOptions, Payload, SernEventsMapping } from './types/utility';
export type { Singleton, Transient, CoreDependencies, Initializable } from './types/ioc';
export {
commandModule,
eventModule,
discordEvent,
EventExecutable,
CommandExecutable,
} from './core/modules';
export { controller } from './sern';
} from './handler/commands';
export { controller } from './handler/sern';
export type { Wrapper, Args } from './shared';

View File

@@ -1,75 +0,0 @@
import { handleCrash } from './handlers/_internal';
import { err, ok, Files } from './core/_internal';
import { merge } from 'rxjs';
import { Services } from './core/ioc';
import { Wrapper } from './types/core';
import { eventsHandler } from './handlers/user-defined-events';
import { startReadyEvent } from './handlers/ready-event';
import { messageHandler } from './handlers/message-event';
import { interactionHandler } from './handlers/interaction-event';
/**
* @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({
* commands: 'dist/commands',
* events: 'dist/events',
* })
* ```
*/
export function init(maybeWrapper: Wrapper | 'file') {
const startTime = performance.now();
const wrapper = Files.loadConfig(maybeWrapper);
const dependencies = useDependencies();
const logger = dependencies[2],
errorHandler = dependencies[1];
const mode = isDevMode(wrapper.mode ?? process.env.MODE);
if (wrapper.events !== undefined) {
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events, mode));
}
//Ready event: load all modules and when finished, time should be taken and logged
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands, mode)).add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded');
logger?.info({
message: `sern: registered all modules in ${time} s`,
});
});
const messages$ = messageHandler(dependencies, wrapper.defaultPrefix);
const interactions$ = interactionHandler(dependencies);
// listening to the message stream and interaction stream
merge(messages$, interactions$).pipe(handleCrash(errorHandler, logger)).subscribe();
}
function isDevMode(mode: string | undefined) {
console.info(`Detected mode: "${mode}"`);
if (mode === undefined) {
console.info('No mode found in process.env, assuming DEV');
}
return mode === 'DEV' || mode == undefined;
}
function useDependencies() {
return Services(
'@sern/emitter',
'@sern/errors',
'@sern/logger',
'@sern/modules',
'@sern/client',
);
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: ok,
stop: err,
};

View File

@@ -1,29 +1,52 @@
import { CommandInteractionOptionResolver } from 'discord.js';
import { PayloadType } from '../core';
import { AnyModule } from './core-modules';
import type {
CommandInteractionOptionResolver,
InteractionReplyOptions,
MessageReplyOptions,
} from 'discord.js';
import { PayloadType } from './core';
import { AnyModule } from './core/types/modules';
export type Awaitable<T> = PromiseLike<T> | T;
export type ReplyOptions =
| string
| Omit<InteractionReplyOptions, 'fetchReply'>
| MessageReplyOptions;
export type AnyFunction = (...args: unknown[]) => unknown;
// Thanks to @kelsny
type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
}[keyof T];
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
export interface SernEventsMapping {
'module.register': [Payload];
'module.activate': [Payload];
error: [Payload];
warning: [Payload];
modulesLoaded: [never?];
'modulesLoaded': [never?];
}
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
export type Awaitable<T> = PromiseLike<T> | T;
export interface Wrapper {
commands: string;
defaultPrefix?: string;
events?: string;
/**
* Overload to enable mode in case developer does not use a .env file.
*/
mode?: 'DEV' | 'PROD';
/*
* @deprecated
*/
containerConfig?: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
}
// Thanks to @kelsny
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'>;

View File

@@ -1,21 +0,0 @@
export interface ImportPayload<T> {
module: T;
absPath: string;
[key: string]: unknown;
}
export interface Wrapper {
commands: string;
defaultPrefix?: string;
events?: string;
/**
* Overload to enable mode in case developer does not use a .env file.
*/
mode?: 'DEV' | 'PROD';
/*
* @deprecated
*/
containerConfig?: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
}

View File

@@ -1,48 +0,0 @@
import { Container, UnpackFunction } from 'iti';
import * as Contracts from '../core/contracts';
/**
* Type to annotate that something is a singleton.
* T is created once and lazily.
*/
export type Singleton<T> = () => T;
/**
* Type to annotate that something is transient.
* Every time this is called, a new object is created
*/
export type Transient<T> = () => () => T;
/**
* Type to annotate that something is initializable.
* If T has an init method, this will be called.
*/
export type Initializable<T extends Contracts.Init> = T
export type DependencyList = [
Contracts.Emitter,
Contracts.ErrorHandling,
Contracts.Logging | undefined,
Contracts.ModuleManager,
Contracts.Emitter,
];
export interface CoreDependencies {
'@sern/client': () => Contracts.Emitter;
'@sern/logger'?: () => Contracts.Logging;
'@sern/emitter': () => Contracts.Emitter;
'@sern/store': () => Contracts.CoreModuleStore;
'@sern/modules': () => Contracts.ModuleManager;
'@sern/errors': () => Contracts.ErrorHandling;
}
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 DependencyConfiguration {
//@deprecated. Loggers will always be included in the future
exclude?: Set<'@sern/logger'>;
build: (
root: Container<Omit<CoreDependencies, '@sern/client'>, {}>,
) => Container<Dependencies, {}>;
}

View File

@@ -1,16 +1,14 @@
import { assertType, describe, it } from 'vitest';
import { ModuleStore } from '../../src';
import * as DefaultContracts from '../../src/core/structures/services';
import * as Contracts from '../../src/core/contracts/index.js';
import { assertType, describe, it, vi } from "vitest";
import * as DefaultContracts from '../../src/core/structures/services'
import * as Contracts from '../../src/core/contracts/index.js'
import { ModuleStore } from "../../src/core";
describe('default contracts', () => {
it('should satisfy contracts', () => {
assertType<Contracts.Logging>(new DefaultContracts.DefaultLogging());
assertType<Contracts.ErrorHandling>(new DefaultContracts.DefaultErrorHandling());
assertType<Contracts.ModuleManager>(
new DefaultContracts.DefaultModuleManager(new ModuleStore()),
);
assertType<Contracts.CoreModuleStore>(new ModuleStore());
});
});
assertType<Contracts.Logging>(new DefaultContracts.DefaultLogging())
assertType<Contracts.ErrorHandling>(new DefaultContracts.DefaultErrorHandling())
assertType<Contracts.ModuleManager>(new DefaultContracts.DefaultModuleManager(new ModuleStore()))
assertType<Contracts.CoreModuleStore>(new ModuleStore())
})
})

View File

@@ -1,32 +1,35 @@
import { describe, it, expect } from 'vitest';
import {
CommandControlPlugin,
CommandInitPlugin,
EventControlPlugin,
EventInitPlugin,
} from '../../src/core/create-plugins';
import { PluginType, controller } from '../../src';
import { describe, it, expect } from 'vitest'
import { CommandControlPlugin, CommandInitPlugin, EventControlPlugin, EventInitPlugin } from '../../src/core/create-plugins'
import { PluginType, controller } from '../../src/index'
describe('create-plugins', () => {
it('should make proper control plugins', () => {
const pl = EventControlPlugin(() => controller.next());
expect(pl).to.have.all.keys(['type', 'execute']);
expect(pl.type).toBe(PluginType.Control);
expect(pl.execute).an('function');
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 pl = EventControlPlugin(() => controller.next())
expect(pl)
.to.have.all.keys(['type', 'execute'])
expect(pl.type).toBe(PluginType.Control)
expect(pl.execute).an('function')
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')
})
})
const pl2 = CommandInitPlugin(() => controller.next());
expect(pl2).to.have.all.keys(['type', 'execute']);
expect(pl2.type).toBe(PluginType.Init);
expect(pl2.execute).an('function');
});
});

View File

@@ -1,404 +1,208 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PluginType, SernOptionsData, controller } from '../../src/index';
import { partitionPlugins, treeSearch } from '../../src/core/functions';
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';
import { ApplicationCommandOptionType, AutocompleteInteraction } from "discord.js";
vi.mock('discord.js', () => {
const Collection = Map;
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
const Collection = Map
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 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;
const AutocompleteInteraction = class {
type = 4;
option: string
constructor(s: string) {
this.option = s;
}
options = {
getFocused : vi.fn()
}
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
return {
Collection,
ComponentType: {
Button: 2
},
InteractionType : {
Ping: 1,
ApplicationCommand: 2,
MessageComponent: 3,
ApplicationCommandAutocomplete:4,
ModalSubmit: 5
},
ApplicationCommandOptionType : {
Subcommand : 1,
SubcommandGroup : 2,
String : 3,
Integer : 4,
Boolean : 5,
User : 6,
Channel : 7,
Role : 8,
Mentionable : 9,
Number : 10,
Attachment : 11
},
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction
};
};
})
return {
Collection,
ComponentType: {
Button: 2,
},
InteractionType: {
Ping: 1,
ApplicationCommand: 2,
MessageComponent: 3,
ApplicationCommandAutocomplete: 4,
ModalSubmit: 5,
},
ApplicationCommandOptionType: {
Subcommand: 1,
SubcommandGroup: 2,
String: 3,
Integer: 4,
Boolean: 5,
User: 6,
Channel: 7,
Role: 8,
Mentionable: 9,
Number: 10,
Attachment: 11,
},
ApplicationCommandType: {
ChatInput: 1,
User: 2,
Message: 3,
},
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
};
});
describe('functions', () => {
afterEach(() => {
vi.clearAllMocks();
});
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()),
}));
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 }),
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);
});
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[] = [
const options : SernOptionsData[] = [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command: { onEvent: [], execute: vi.fn() },
},
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command : { onEvent: [], execute:(a) => {} }
}
];
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'autocomplete',
value: faker.string.alpha(),
focused: true,
});
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', () => {
}),
it('should tree search depth 2', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
const options : SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
type: ApplicationCommandOptionType.Subcommand,
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(),
],
},
],
},
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,
});
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 ', () => {
})
it('should tree search depth n > 2', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
const options : SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
{
type: ApplicationCommandOptionType.Subcommand,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
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: () => {},
},
},
],
},
],
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[] = [
autocmpInteraction.options.getFocused.mockReturnValue(
{
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,
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
result = treeSearch(autocomplete, options);
}
expect(result).toBeTruthy();
});
});
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;
})
})

View File

@@ -1,11 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CoreContainer } from '../../src/core/ioc/container';
import { CoreDependencies } from '../../src/core/ioc';
import { EventEmitter } from 'events';
import { DefaultLogging, Init, Logging } from '../../src/core';
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CoreContainer } from '../../src/core/structures/container'
import { CoreDependencies } from "../../src/core/ioc";
import { EventEmitter } from "events";
import { DefaultLogging, Init, Logging } from "../../src/core";
describe('ioc container', () => {
let container: CoreContainer<{}>;
let container: CoreContainer<{}>;
let initDependency: Logging & Init;
beforeEach(() => {
initDependency = {
@@ -14,39 +15,38 @@ describe('ioc container', () => {
warning(): void {},
info(): void {},
debug(): void {},
};
container = new CoreContainer();
});
}
container = new CoreContainer()
})
it('should be ready after calling container.ready()', () => {
container.ready();
expect(container.isReady()).toBe(true);
});
it('should container all core dependencies', async () => {
const keys = [
'@sern/modules',
'@sern/emitter',
'@sern/logger',
'@sern/errors',
] satisfies (keyof CoreDependencies)[];
container.ready()
expect(container.isReady()).toBe(true)
})
it('should container all core dependencies', async () => {
const keys = ['@sern/modules', '@sern/emitter', '@sern/logger', '@sern/errors'] satisfies (keyof CoreDependencies)[]
container.add({
'@sern/logger': () => new DefaultLogging(),
'@sern/client': () => new EventEmitter(),
});
for (const k of keys) {
})
for(const k of keys) {
//@ts-expect-error typings for iti are strict
expect(() => container.get(k)).not.toThrow();
}
});
})
it('should init modules', () => {
container.upsert({ '@sern/logger': initDependency });
container.ready();
expect(initDependency.init).to.toHaveBeenCalledOnce();
});
container.upsert({ '@sern/logger': initDependency })
container.ready()
expect(initDependency.init).to.toHaveBeenCalledOnce()
})
it('should not lazy module', () => {
container.upsert({ '@sern/logger': () => initDependency });
container.ready();
container.upsert({ '@sern/logger': () => initDependency })
container.ready()
expect(initDependency.init).toHaveBeenCalledTimes(0);
});
});
})
})

View File

@@ -1,23 +0,0 @@
import { describe, it, expect } from 'vitest'
import { faker } from '@faker-js/faker'
import * as Files from '../../src/core/module-loading'
describe('module-loading', () => {
it('should properly extract filename from file, nested once', () => {
const extension = faker.system.fileExt()
const name = faker.system.fileName({ extensionCount: 0 })
const filename = Files.fmtFileName(name+'.'+extension);
expect(filename).toBe(name)
})
// todo: handle commands with multiple extensions
// it('should properly extract filename from file, nested multiple', () => {
// const extension = faker.system.fileExt()
// const extension2 = faker.system.fileExt()
// const name = faker.system.fileName({ extensionCount: 0 })
// const filename = Files.fmtFileName(name+'.'+extension+'.'+extension2);
// console.log(filename, name)
// expect(filename).toBe(name)
//
// })
})

View File

@@ -1,77 +1,80 @@
import { SpyInstance, afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { CoreContainer } from '../../src/core/ioc/container';
import { DefaultLogging } from '../../src/core';
import { faker } from '@faker-js/faker';
import { commandModule } from '../../src';
import * as Id from '../../src/core/id';
import { CommandMeta } from '../../src/types/core-modules';
import { SpyInstance, afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { CoreContainer } from '../../src/core/structures/container'
import { DefaultLogging } from "../../src/core";
import { faker } from '@faker-js/faker'
import { commandModule } from "../../src";
import { createId } from '../../src/handler/id'
import { CommandMeta } from "../../src/core/types/modules";
describe('services', () => {
//@ts-ignore
let container: CoreContainer<Dependencies>;
let consoleMock: SpyInstance;
let container: CoreContainer<Dependencies>;
let consoleMock : SpyInstance;
beforeEach(() => {
container = new CoreContainer();
container.add({ '@sern/logger': () => new DefaultLogging() });
container.ready();
consoleMock = vi.spyOn(container.get('@sern/logger'), 'error').mockImplementation(() => {});
});
container = new CoreContainer()
container.add({ '@sern/logger': () => new DefaultLogging() })
container.ready()
consoleMock = vi.spyOn(container.get('@sern/logger'), 'error').mockImplementation(() => {})
})
afterAll(() => {
consoleMock.mockReset();
consoleMock.mockReset()
});
it('module-store.ts', async () => {
function createRandomCommandModules() {
function createRandomCommandModules() {
return commandModule({
type: faker.number.int({ min: 1 << 0, max: 1 << 10 }),
type: faker.number.int({ min: 1<<0, max: 1<<10 }),
description: faker.string.alpha(),
name: faker.string.alpha(),
execute: () => {},
});
}
execute: ()=>{}
})
}
const modules = faker.helpers.multiple(createRandomCommandModules, {
count: 40,
});
const modules = faker.helpers.multiple(createRandomCommandModules, { count: 40 })
const paths = faker.helpers.multiple(faker.system.directoryPath, { count: 40 })
.map((path,i) => `${path}/${modules[i]}.js`);
const paths = faker.helpers
.multiple(faker.system.directoryPath, { count: 40 })
.map((path, i) => `${path}/${modules[i]}.js`);
const metadata: CommandMeta[] = modules.map((cm, i) => ({
id: Id.create(cm.name, cm.type),
isClass: false,
fullPath: `${paths[i]}/${cm.name}.js`,
}));
const moduleManager = container.get('@sern/modules');
let i = 0;
for (const m of modules) {
moduleManager.set(Id.create(m.name, m.type), paths[i]);
moduleManager.setMetadata(m, metadata[i]);
i++;
}
for (const m of modules) {
expect(moduleManager.getMetadata(m), 'module references do not exist').toBeDefined();
}
});
const metadata: CommandMeta[] = modules.map((cm, i) => ({
id: createId(cm.name, cm.type),
isClass: false,
fullPath: `${paths[i]}/${cm.name}.js`
}));
const moduleManager = container.get('@sern/modules');
let i =0;
for(const m of modules) {
moduleManager.set(createId(m.name,m.type), paths[i]);
moduleManager.setMetadata(m, metadata[i]);
i++
}
for(const m of modules) {
expect(moduleManager.getMetadata(m), "module references do not exist").toBeDefined()
}
})
//todo add more
it('error-handling', () => {
const errorHandler = container.get('@sern/errors');
const lifetime = errorHandler.keepAlive;
for (let i = 0; i < lifetime; i++) {
if (i == lifetime - 1) {
expect(() => errorHandler.updateAlive(new Error('poo'))).toThrowError();
for(let i = 0; i< lifetime; i++) {
if(i == lifetime-1) {
expect(() => errorHandler.updateAlive(new Error("poo"))).toThrowError();
} else {
expect(() => errorHandler.updateAlive(new Error('poo'))).not.toThrowError();
expect(() => errorHandler.updateAlive(new Error("poo"))).not.toThrowError();
}
}
});
})
//todo add more, spy on every instance?
it('logger', () => {
container.get('@sern/logger').error({ message: 'error' });
container.get('@sern/logger').error({ message: 'error' })
expect(consoleMock).toHaveBeenCalledOnce();
expect(consoleMock).toHaveBeenLastCalledWith({ message: 'error' });
});
});
expect(consoleMock).toHaveBeenLastCalledWith({ message: 'error' })
})
})

View File

@@ -1,45 +0,0 @@
import { beforeEach, describe, expect, vi, it } from 'vitest';
import { createResultResolver, eventDispatcher } from '../../src/handlers/_internal';
import { faker } from '@faker-js/faker';
import { Module } from '../../src/core/types/modules';
import { Processed } from '../../src/handlers/types';
import { CommandType } from '../../src/core';
import { EventEmitter } from 'events';
function createRandomModule(): Processed<Module> {
return {
type: faker.number.int({
min: CommandType.Text,
max: CommandType.ChannelSelect,
}),
description: faker.string.alpha(),
name: faker.string.alpha(),
onEvent: [],
plugins: [],
execute: vi.fn(),
};
}
describe('eventDispatcher standard', () => {
let m: Processed<Module>;
let ee: EventEmitter;
beforeEach(() => {
ee = new EventEmitter();
m = createRandomModule();
});
it('should throw', () => {
expect(() => eventDispatcher(m, 'not event emitter')).toThrowError();
});
it("Shouldn't throw", () => {
expect(() => eventDispatcher(m, ee)).not.toThrowError();
});
it('Should be called once', () => {
const s = eventDispatcher(m, ee);
s.subscribe();
ee.emit(m.name, faker.string.alpha());
expect(m.execute).toHaveBeenCalledOnce();
});
});

View File

@@ -1,72 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import * as Id from '../../src/core/id';
import { faker } from '@faker-js/faker';
import { CommandModule, CommandType, commandModule } from '../../src';
import { CommandTypeDiscordApi } from '../../src/core/id';
function createRandomCommandModules() {
const randomCommandType = [
CommandType.Text,
CommandType.Both,
CommandType.CtxMsg,
CommandType.CtxUser,
CommandType.Modal,
CommandType.ChannelSelect,
CommandType.RoleSelect,
CommandType.UserSelect,
CommandType.StringSelect,
CommandType.Button,
];
return commandModule({
type: randomCommandType[Math.floor(Math.random() * randomCommandType.length)],
description: faker.string.alpha(),
name: faker.string.alpha(),
execute: () => {},
});
}
function createMetadata(c: CommandModule) {
return {
fullPath: faker.system.filePath(),
id: Id.create(c.name, c.type),
isClass: Boolean(Math.floor(Math.random())),
};
}
const appBitField = 0b000000001111;
describe('id resolution', () => {
it('should resolve application commands correctly', () => {
const modules = faker.helpers.multiple(createRandomCommandModules, {
count: 20,
});
const metadata = modules.map(createMetadata);
metadata.forEach((meta, idx) => {
const associatedModule = modules[idx];
const am = (appBitField & associatedModule.type) !== 0 ? 'A' : 'C';
let uid = 0;
if (
associatedModule.type === CommandType.Both ||
associatedModule.type === CommandType.Modal
) {
uid = 1;
} else {
uid = CommandTypeDiscordApi[Math.log2(associatedModule.type)];
}
expect(meta.id).toBe(associatedModule.name + '_' + am + uid);
});
});
it('maps commands type to discord components or application commands', () => {
expect(CommandTypeDiscordApi[Math.log2(CommandType.Text)]).toBe(1);
expect(CommandTypeDiscordApi[1]).toBe(1);
expect(CommandTypeDiscordApi[Math.log2(CommandType.CtxUser)]).toBe(2);
expect(CommandTypeDiscordApi[Math.log2(CommandType.CtxMsg)]).toBe(3);
expect(CommandTypeDiscordApi[Math.log2(CommandType.Button)]).toBe(2);
expect(CommandTypeDiscordApi[Math.log2(CommandType.StringSelect)]).toBe(3);
expect(CommandTypeDiscordApi[Math.log2(CommandType.UserSelect)]).toBe(5);
expect(CommandTypeDiscordApi[Math.log2(CommandType.RoleSelect)]).toBe(6);
expect(CommandTypeDiscordApi[Math.log2(CommandType.MentionableSelect)]).toBe(7);
expect(CommandTypeDiscordApi[Math.log2(CommandType.ChannelSelect)]).toBe(8);
expect(CommandTypeDiscordApi[6]).toBe(1);
});
});

19
tsconfig-base.json Normal file
View File

@@ -0,0 +1,19 @@
{
"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,
"isolatedModules": true
},
"exclude": ["node_modules", "dist"],
"include": ["./src", "./src/**/*.d.ts"]
}

8
tsconfig-cjs.json Normal file
View File

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

8
tsconfig-esm.json Normal file
View File

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

View File

@@ -1,21 +1,3 @@
{
"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,
"isolatedModules": true,
"module": "esnext",
"target": "esnext"
},
"exclude": ["node_modules", "dist"],
"include": ["./src", "./src/**/*.d.ts"]
"extends": "./tsconfig-esm.json"
}

View File

@@ -1,4 +1,6 @@
import { defineConfig } from 'tsup';
import { writeFile } from 'fs/promises';
import ifdefPlugin from 'esbuild-ifdef';
const shared = {
entry: ['src/index.ts'],
external: ['discord.js', 'iti'],
@@ -10,40 +12,50 @@ const shared = {
correctVarValueBeforeDeclaration: true, //need this to treeshake esm discord.js empty import
annotations: true,
},
dts: false,
};
export default defineConfig([
{
format: ['esm', 'cjs'],
format: 'esm',
target: 'node18',
tsconfig: './tsconfig.json',
outDir: './dist',
tsconfig: './tsconfig-esm.json',
outDir: './dist/esm',
splitting: true,
dts: 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: 'node18',
// 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,
// },
// {
// dts: {
// only: true,
// },
// entry: ['src/index.ts'],
// outDir: 'dist',
// },
{
format: 'cjs',
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'cjs' }, verbose: true })],
splitting: false,
target: 'node18',
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,
},
{
dts: {
only: true,
},
entry: ['src/index.ts'],
outDir: 'dist',
},
]);

1667
yarn.lock

File diff suppressed because it is too large Load Diff