Compare commits

...

7 Commits

Author SHA1 Message Date
github-actions[bot]
2fd7697300 chore(main): release 2.6.1 (#258)
* chore(main): release 2.6.1

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-17 16:37:20 -05:00
Jacob Nguyen
f9609ce6cd chore: release 2.6.1
Release-As: 2.6.1
2023-03-17 16:33:46 -05:00
Jacob Nguyen
a3064aa915 chore: audit & remove tspattern (#256)
* chore: move import

* build: remove ts pattern

* fix: forgot to convert to switch

* fix workflow

* refactor: lift function out of readyHandler

* refactor: clean up errTap signature

* fix: sern emitter emitting wrong payload

* wa

* style: space

* chore: remove old errTap

* chore:bump discord.js

* chore: eslint format
2023-03-17 16:30:27 -05:00
github-actions[bot]
0a53a48521 style: pretty please (#255)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-03-15 21:16:58 -05:00
Jacob Nguyen
05037b5315 build: prettier ignore 2023-03-15 21:15:33 -05:00
Jacob Nguyen
06a3e69210 feat: prettier ignore 2023-03-15 21:13:42 -05:00
Jacob Nguyen
74c4b77d4b build: refactor/building (#252)
* refactor: conditional compilation of loading esm/cjs modules

* refactor: move file loading file

* refactor: add conditional compilation for building modules

* refactor: add conditional compilation for building modules

* perf: decrease build times

* test

* revert: typo and clean code

* build: smaller build

* chore:cleanscripts

* chore:refactor readme

* build:automerge lockfile

* chore: remove build and upgrade readme

* fix: dropdown

* chore: fix

* chore: more docs

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-03-15 21:08:27 -05:00
20 changed files with 2163 additions and 2610 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- recursive: true - recursive: true
args: [--strict-peer-dependencies] args: [--strict-peer-dependencies]
- run: pnpm install - run: pnpm install
- run: pnpm build - run: pnpm build:prod
- uses: JS-DevTools/npm-publish@v1 - uses: JS-DevTools/npm-publish@v1
with: with:
token: ${{ secrets.NPM_TOKEN }} token: ${{ secrets.NPM_TOKEN }}

View File

@@ -52,6 +52,7 @@ typings/
.node_repl_history .node_repl_history
# Output of 'npm pack' # Output of 'npm pack'
*.tgz *.tgz
# Yarn Integrity file # Yarn Integrity file
@@ -108,6 +109,8 @@ tsup.config.js
tsconfig-base.json tsconfig-base.json
tsconfig.cjs.json tsconfig-cjs.json
tsconfig.esm.json tsconfig-esm.json
renovate.json

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## [2.6.1](https://github.com/sern-handler/handler/compare/v2.6.0...v2.6.1) (2023-03-17)
### Miscellaneous Chores
* release 2.6.1 ([f9609ce](https://github.com/sern-handler/handler/commit/f9609ce6cd777fa0eb595d8c48d57905bbce5966))
## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09) ## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09)

View File

@@ -16,7 +16,7 @@
## Why? ## Why?
- Most handlers don't support discord.js 14.7+ - Most handlers don't support discord.js 14.7+
- Customizable commands - Customizable, composable commands
- Plug and play or customize to your liking - Plug and play or customize to your liking
- Embraces reactive programming for consistent and reliable backend - Embraces reactive programming for consistent and reliable backend
- Customizable logger, error handling, and more - Customizable logger, error handling, and more
@@ -44,27 +44,54 @@ pnpm add @sern/handler
``` ```
## 👶 Basic Usage ## 👶 Basic Usage
<details open><summary>ping.ts</summary>
#### ` index.js (CommonJS)` ```ts
export default commandModule({
```js type: CommandType.Slash,
// Import the discord.js Client and GatewayIntentBits //Installed plugin to publish to discord api and allow access to owners only.
const { Client, GatewayIntentBits } = require('discord.js'); plugins: [publish(), ownerOnly()],
description: 'A ping pong command',
// Import Sern namespace execute(ctx) {
const { Sern, single } = require('@sern/handler'); ctx.reply('Hello owner of the bot');
}
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages
]
}); });
export const useContainer = Sern.makeDependencies({ ```
</details>
<details open><summary>modal.ts</summary>
```ts
export default commandModule({
type: CommandType.Modal,
//Installed a plugin to make sure modal fields pass a validation.
plugins : [
assertFields({
fields: {
name: /^([^0-9]*)$/
},
failure: (errors, modal) => modal.reply('your submission did not pass the validations')
})
],
execute : (modal) => {
modal.reply('thanks for the submission!');
}
})
```
</details>
<details open><summary>index.ts</summary>
```ts
import { Client, GatewayIntentBits } from 'discord.js';
import { Sern, single, type Dependencies } from '@sern/handler';
//client has been declared previously
interface MyDependencies extends Dependencies {
'@sern/client': Singleton<Client>;
}
export const useContainer = Sern.makeDependencies<MyDependencies>({
build: root => root build: root => root
.add({ '@sern/client': single(() => client) }) .add({ '@sern/client': single(() => client) })
.upsert({ '@sern/logger': single(() => new DefaultLogging()) })
}); });
//View docs for all options //View docs for all options
@@ -73,26 +100,13 @@ Sern.init({
commands: 'src/commands', commands: 'src/commands',
// events: 'src/events' (optional), // events: 'src/events' (optional),
containerConfig : { containerConfig : {
get: useContainer get: useContainer
} }
}); });
client.login("YOUR_BOT_TOKEN_HERE"); client.login("YOUR_BOT_TOKEN_HERE");
``` ```
</details>
#### ` ping.js (CommonJS)`
```js
const { CommandType, commandModule } = require('@sern/handler');
exports.default = commandModule({
type: CommandType.Slash,
description: 'A ping pong command',
execute(ctx) {
ctx.reply('pong!');
}
});
```
## 🤖 Bots Using sern ## 🤖 Bots Using sern
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord). - [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
@@ -112,9 +126,7 @@ It is **highly encouraged** to use the [command line interface](https://github.c
- [Support Server](https://sern.dev/discord) - [Support Server](https://sern.dev/discord)
## 👋 Contribute ## 👋 Contribute
- Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.github/CONTRIBUTING.md) carefully - Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.github/CONTRIBUTING.md) carefully
- Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs - Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs
- All kinds of contributions are welcomed. - All kinds of contributions are welcomed.

View File

@@ -1,8 +1,8 @@
{ {
"name": "@sern/handler", "name": "@sern/handler",
"packageManager": "pnpm@7.28.0", "packageManager": "pnpm@7.28.0",
"version": "2.6.0", "version": "2.6.1",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.", "description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "dist/cjs/index.cjs", "main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs", "module": "dist/esm/index.mjs",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -13,12 +13,13 @@
} }
}, },
"scripts": { "scripts": {
"watch": "tsup --dts --watch", "watch": "tsup --watch",
"clean-modules": "rimraf node_modules/ && npm install", "clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix", "format": "eslint src/**/*.ts --fix",
"build": "tsup && node scripts/mkjson.mjs dist/cjs dist/esm && tsup --dts-only --outDir dist", "build:dev": "tsup && tsup --dts-only --outDir dist",
"publish": "npm run build && npm publish", "build:prod": "tsup --minify && tsup --dts-only --outDir dist",
"publish": "npm run build:prod && npm publish",
"pretty": "prettier --write ." "pretty": "prettier --write ."
}, },
"keywords": [ "keywords": [
@@ -35,17 +36,17 @@
"dependencies": { "dependencies": {
"iti": "^0.6.0", "iti": "^0.6.0",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
"ts-pattern": "^4.1.4",
"ts-results-es": "^3.5.0" "ts-results-es": "^3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "5.54.0", "@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.54.0", "@typescript-eslint/parser": "5.54.0",
"discord.js": "^14.7.1", "discord.js": "^14.8.0",
"esbuild-ifdef": "^0.2.0",
"eslint": "8.30.0", "eslint": "8.30.0",
"prettier": "2.8.4", "prettier": "2.8.4",
"tsup": "^6.6.3", "typescript": "4.9.5",
"typescript": "4.9.5" "tsup": "^6.6.3"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

4361
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"schedule": ["every weekend"], "schedule": ["every weekend"],
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
"automerge": false "automerge": true
} }
} }

View File

@@ -1,13 +0,0 @@
import { writeFile } from 'fs/promises';
import { join } from 'path';
// A quick script to regenerate package.jsons for each cjs and esm after tsup cleans distributions
const locations = process.argv;
locations.shift();
locations.shift();
for (const loc of locations) {
if (loc.endsWith('cjs')) {
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'commonjs' }));
} else {
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'module' }));
}
}

View File

@@ -1,6 +1,7 @@
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import type { Logging } from './logging'; import type { Logging } from './logging';
import util from 'util'; import util from 'util';
export interface ErrorHandling { export interface ErrorHandling {
/** /**
* Number of times the process should throw an error until crashing and exiting * Number of times the process should throw an error until crashing and exiting

View File

@@ -13,7 +13,6 @@ import {
pipe, pipe,
} from 'rxjs'; } from 'rxjs';
import { CommandType, type ModuleStore, SernError } from '../structures'; import { CommandType, type ModuleStore, SernError } from '../structures';
import { match, P } from 'ts-pattern';
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers'; import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
import { executeModule, makeModuleExecutor } from './observableHandling'; import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module'; import type { CommandModule } from '../../types/module';
@@ -38,14 +37,15 @@ function makeInteractionProcessor(
); );
return of({ module, event }); return of({ module, event });
} else if (event.isCommand() || event.isAutocomplete()) { } else if (event.isCommand() || event.isAutocomplete()) {
const commandName = event.commandName;
const module = get( const module = get(
ms => ms =>
/** /**
* try to fetch from ApplicationCommands, if nothing, try BothCommands * try to fetch from ApplicationCommands, if nothing, try BothCommands
* exists on the API but not sern * exists on the API but not sern
*/ */
ms.ApplicationCommands[event.commandType].get(event.commandName) ?? ms.ApplicationCommands[event.commandType].get(commandName) ??
ms.BothCommands.get(event.commandName), ms.BothCommands.get(commandName),
); );
return of({ module, event }); return of({ module, event });
} else if (event.isModalSubmit()) { } else if (event.isModalSubmit()) {
@@ -95,28 +95,21 @@ function createDispatcher({
event: Interaction; event: Interaction;
module: Processed<CommandModule>; module: Processed<CommandModule>;
}) { }) {
return ( switch(module.type) {
match(module) case CommandType.Text:
.with({ type: CommandType.Text }, () => { throw Error(SernError.MismatchEvent);
throw Error(SernError.MismatchEvent); case CommandType.Slash: case CommandType.Both : {
}) if(event.isAutocomplete()) {
//P.union = either CommandType.Slash or CommandType.Both /**
.with({ type: P.union(CommandType.Slash, CommandType.Both) }, module => { * Autocomplete is a special case that
if (event.isAutocomplete()) { * must be handled separately, since it's
/** * too different from regular command modules
* Autocomplete is a special case that */
* must be handled separately, since it's return dispatchAutocomplete(module, event);
* too different from regular command modules } else {
*/ return dispatchCommand(module, contextArgs(event));
return dispatchAutocomplete(module, event); }
} else { }
return dispatchCommand(module, contextArgs(event)); default : return dispatchCommand(module, interactionArg(event));
} }
})
/**
* Every other command module takes a one argument parameter, its corresponding interaction
* this makes this usage safe
*/
.otherwise(mod => dispatchCommand(mod, interactionArg(event)))
);
} }

View File

@@ -28,7 +28,7 @@ const createMessageProcessor = (
//This concatMap checks if module is undefined, and if it is, do not continue. //This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet // Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => { concatMap(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix); const [prefix, ...rest] = fmt(message.content, defaultPrefix);
const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix)); const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
if (module === undefined) { if (module === undefined) {
return EMPTY; return EMPTY;

View File

@@ -1,9 +1,7 @@
import type { Awaitable, Message } from 'discord.js'; import type { Awaitable, Message } from 'discord.js';
import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs'; import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs';
import type { SernError } from '../structures';
import { Result } from 'ts-results-es'; import { Result } from 'ts-results-es';
import type { AnyModule, CommandModule, EventModule, Module } from '../../types/module'; import type { CommandModule, EventModule, Module } from '../../types/module';
import { _const as i } from '../utilities/functions';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import { callPlugin, everyPluginOk, filterMapTo } from './operators'; import { callPlugin, everyPluginOk, filterMapTo } from './operators';
import type { Processed } from '../../types/handler'; import type { Processed } from '../../types/handler';
@@ -31,27 +29,6 @@ export function ignoreNonBot<T extends Message>(prefix: string) {
}); });
} }
/**
* 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<T extends AnyModule>(cb: (err: SernError) => void) {
return (src: Observable<Result<{ module: T; absPath: string }, SernError>>) =>
new Observable<{ module: T; absPath: string }>(subscriber => {
return src.subscribe({
next(value) {
if (value.err) {
cb(value.val);
} else {
subscriber.next(value.val);
}
},
});
});
}
/** /**
* Wraps the task in a Result as a try / catch. * Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty * if the task is ok, an event is emitted and the stream becomes empty
@@ -78,7 +55,7 @@ export function executeModule(
emitter.emit('module.activate', SernEmitter.success(module)); emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY; return EMPTY;
} else { } else {
return throwError(i(SernEmitter.failure(module, result.val))); return throwError(() => SernEmitter.failure(module, result.val));
} }
}), }),
); );
@@ -120,7 +97,7 @@ export function createResultResolver<
* Calls a module's init plugins and checks for Err. If so, call { onFailure } and * Calls a module's init plugins and checks for Err. If so, call { onFailure } and
* ignore the module * ignore the module
*/ */
export function scanModule< export function callInitPlugins<
T extends Processed<CommandModule | EventModule>, T extends Processed<CommandModule | EventModule>,
Args extends { module: T; absPath: string }, Args extends { module: T; absPath: string },
>(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) { >(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) {

View File

@@ -10,6 +10,8 @@ import { nameOrFilename } from '../../utilities/functions';
import type { PluginResult, VoidResult } from '../../../types/plugin'; import type { PluginResult, VoidResult } from '../../../types/plugin';
import { guayin } from '../../plugins'; import { guayin } from '../../plugins';
import { controller } from '../../sern'; import { controller } from '../../sern';
import { SernError } from '../../structures';
import { Result } from 'ts-results-es';
/** /**
* if {src} is true, mapTo V, else ignore * if {src} is true, mapTo V, else ignore
@@ -48,7 +50,7 @@ export function callPlugin(args: unknown): OperatorFunction<
* operator function that fill the defaults for a module * operator function that fill the defaults for a module
*/ */
export function defineAllFields<T extends AnyModule>() { export function defineAllFields<T extends AnyModule>() {
const fillFields = ({ absPath, module }: { absPath: string; module: T }) => ({ const fillFields = ({ module, absPath }: { module: T; absPath: string }) => ({
absPath, absPath,
module: { module: {
name: nameOrFilename(module.name, absPath), name: nameOrFilename(module.name, absPath),
@@ -59,6 +61,27 @@ export function defineAllFields<T extends AnyModule>() {
return pipe(map(fillFields)); return pipe(map(fillFields));
} }
/**
* 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<T extends AnyModule>(
cb: (err: SernError) => void
): OperatorFunction<Result<{ module: T; absPath: string}, SernError>, { module: T; absPath: string }> {
return pipe(
concatMap(result => {
if(result.ok) {
return of(result.val);
} else {
cb(result.val);
return EMPTY;
}
})
);
}
/** /**
* Checks if the stream of results is all ok. * Checks if the stream of results is all ok.
*/ */

View File

@@ -1,18 +1,30 @@
import { fromEvent, pipe, switchMap, take } from 'rxjs'; import { fromEvent, pipe, switchMap, take } from 'rxjs';
import * as Files from '../utilities/readFile'; import * as Files from '../module-loading/readFile';
import { errTap, scanModule } from './observableHandling'; import { callInitPlugins } from './observableHandling';
import { CommandType, type ModuleStore, SernError } from '../structures'; import { CommandType, type ModuleStore, SernError } from '../structures';
import { match } from 'ts-pattern';
import { Result } from 'ts-results-es'; import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js'; import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module'; import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler'; import type { Processed } from '../../types/handler';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts'; import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
import { _const, err, ok } from '../utilities/functions'; import { err, ok } from '../utilities/functions';
import { defineAllFields } from './operators'; import { defineAllFields, errTap } from './operators';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import type { EventEmitter } from 'node:events'; import type { EventEmitter } from 'node:events';
function buildCommandModules(
commandDir: string,
sernEmitter: SernEmitter
) {
return pipe(
switchMap(() => Files.buildData<CommandModule>(commandDir)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
defineAllFields(),
);
}
export function makeReadyEvent( export function makeReadyEvent(
[sEmitter, client, errorHandler, , moduleManager]: [ [sEmitter, client, errorHandler, , moduleManager]: [
SernEmitter, SernEmitter,
@@ -24,24 +36,17 @@ export function makeReadyEvent(
commandDir: string, commandDir: string,
) { ) {
const readyOnce$ = fromEvent(client, 'ready').pipe(take(1)); const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
const parseCommandModules = pipe(
switchMap(() => Files.buildData<CommandModule>(commandDir)),
errTap(error => {
sEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
defineAllFields(),
);
return readyOnce$ return readyOnce$
.pipe( .pipe(
parseCommandModules, buildCommandModules(commandDir, sEmitter),
scanModule({ callInitPlugins({
onFailure: module => { onFailure: module => {
sEmitter.emit( sEmitter.emit(
'module.register', 'module.register',
SernEmitter.failure(module, SernError.PluginFailure), SernEmitter.failure(module, SernError.PluginFailure),
); );
}, },
onSuccess: ({ module }) => { onSuccess: ({ module }) => {
sEmitter.emit('module.register', SernEmitter.success(module)); sEmitter.emit('module.register', SernEmitter.success(module));
return module; return module;
}, },
@@ -61,45 +66,38 @@ function registerModule<T extends Processed<CommandModule>>(
): Result<void, void> { ): Result<void, void> {
const name = mod.name; const name = mod.name;
const insert = (cb: (ms: ModuleStore) => void) => { const insert = (cb: (ms: ModuleStore) => void) => {
const set = Result.wrap(_const(manager.set(cb))); const set = Result.wrap(() => manager.set(cb));
return set.ok ? ok() : err(); return set.ok ? ok() : err();
}; };
return match(mod as Processed<CommandModule>) switch(mod.type) {
.with({ type: CommandType.Text }, mod => { case CommandType.Text: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod))); mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod)); return insert(ms => ms.TextCommands.set(name, mod));
}) }
.with({ type: CommandType.Slash }, mod => case CommandType.Slash:
insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod)), return insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod));
) case CommandType.Both: {
.with({ type: CommandType.Both }, mod => {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod))); mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.BothCommands.set(name, mod)); return insert(ms => ms.BothCommands.set(name, mod));
}) }
.with({ type: CommandType.CtxUser }, mod => case CommandType.CtxUser:
insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod)), return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
) case CommandType.CtxMsg:
.with({ type: CommandType.CtxMsg }, mod => return insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod));
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod)), case CommandType.Button:
) return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
.with({ type: CommandType.Button }, mod => case CommandType.StringSelect:
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod)), return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
) case CommandType.MentionableSelect:
.with({ type: CommandType.StringSelect }, mod => return insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod));
insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod)), case CommandType.UserSelect:
) return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
.with({ type: CommandType.MentionableSelect }, mod => case CommandType.ChannelSelect:
insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod)), return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
) case CommandType.RoleSelect:
.with({ type: CommandType.ChannelSelect }, mod => return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod)), case CommandType.Modal:
) return insert(ms => ms.ModalSubmit.set(name, mod));
.with({ type: CommandType.UserSelect }, mod => default: return err();
insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod)), }
)
.with({ type: CommandType.RoleSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod)),
)
.with({ type: CommandType.Modal }, mod => insert(ms => ms.ModalSubmit.set(name, mod)))
.otherwise(err);
} }

View File

@@ -1,16 +1,15 @@
import { catchError, finalize, map, tap, mergeAll } from 'rxjs'; import { catchError, finalize, map, mergeAll } from 'rxjs';
import { buildData } from '../utilities/readFile'; import { buildData } from '../module-loading/readFile';
import type { Dependencies, Processed } from '../../types/handler'; import type { Dependencies, Processed } from '../../types/handler';
import { errTap, scanModule } from './observableHandling'; import { callInitPlugins } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module'; import type { CommandModule, EventModule } from '../../types/module';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import { match } from 'ts-pattern';
import type { ErrorHandling, Logging } from '../contracts'; import type { ErrorHandling, Logging } from '../contracts';
import { SernError, EventType, type Wrapper } from '../structures'; import { SernError, EventType, type Wrapper } from '../structures';
import { eventDispatcher } from './dispatchers'; import { eventDispatcher } from './dispatchers';
import { handleError } from '../contracts/errorHandling'; import { handleError } from '../contracts/errorHandling';
import { defineAllFields } from './operators'; import { defineAllFields, errTap } from './operators';
import { useContainerRaw } from '../dependencies'; import { useContainerRaw } from '../dependencies';
export function makeEventsHandler( export function makeEventsHandler(
@@ -23,20 +22,22 @@ export function makeEventsHandler(
const eventCreation$ = eventStream$.pipe( const eventCreation$ = eventStream$.pipe(
defineAllFields(), defineAllFields(),
scanModule({ callInitPlugins({
onFailure: module => s.emit('module.register', SernEmitter.success(module)), onFailure: module => s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onSuccess: ({ module }) => { onSuccess: ({ module }) => {
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)); s.emit('module.register', SernEmitter.success(module));
return module; return module;
}, },
}), }),
); );
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
match(e) switch(e.type) {
.with({ type: EventType.Sern }, m => eventDispatcher(m, s)) case EventType.Sern: return eventDispatcher(e, s);
.with({ type: EventType.Discord }, m => eventDispatcher(m, client)) case EventType.Discord: return eventDispatcher(e, client);
.with({ type: EventType.External }, m => eventDispatcher(m, lazy(m.emitter))) case EventType.External: return eventDispatcher(e, lazy(e.emitter));
.otherwise(() => err.crash(Error(SernError.InvalidModuleType))); default: err.crash(Error(SernError.InvalidModuleType + ' while creating event handler'));
}
};
eventCreation$ eventCreation$
.pipe( .pipe(
map(intoDispatcher), map(intoDispatcher),

View File

@@ -1,6 +1,6 @@
import { readdirSync, statSync } from 'fs'; import { readdirSync, statSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { type Observable, from, concatAll } from 'rxjs'; import { type Observable, from, mergeAll } from 'rxjs';
import { SernError } from '../structures/errors'; import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es'; import { type Result, Err, Ok } from 'ts-results-es';
@@ -18,16 +18,13 @@ function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
return arrayOfFiles; return arrayOfFiles;
} }
export const fmtFileName = (n: string) => n.substring(0, n.length - 3); export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/** /**
* a directory string is converted into a stream of modules. * a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init * starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files * @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir * @param commandDir
*/ */
export function buildData<T>(commandDir: string): Observable< export function buildData<T>(commandDir: string): Observable<
Result< Result<
{ {
@@ -41,13 +38,14 @@ export function buildData<T>(commandDir: string): Observable<
return from( return from(
Promise.all( Promise.all(
commands.map(async absPath => { commands.map(async absPath => {
let module: T | undefined; // prettier-ignore
try { let module: T | undefined
// eslint-disable-next-line @typescript-eslint/no-var-requires /// #if MODE === 'esm'
module = require(absPath).default; = (await import(`file:///` + absPath)).default
} catch { /// #elif MODE === 'cjs'
module = (await import(`file:///` + absPath)).default; = require(absPath).default; // eslint-disable-line
} /// #endif
if (module === undefined) { if (module === undefined) {
return Err(SernError.UndefinedModule); return Err(SernError.UndefinedModule);
} }
@@ -57,7 +55,7 @@ export function buildData<T>(commandDir: string): Observable<
return Ok({ module, absPath }); return Ok({ module, absPath });
}), }),
), ),
).pipe(concatAll()); ).pipe(mergeAll());
} }
export function getCommands(dir: string): string[] { export function getCommands(dir: string): string[] {

View File

@@ -1,4 +1,4 @@
import * as Files from './readFile'; import * as Files from '../module-loading/readFile';
import { basename } from 'path'; import { basename } from 'path';
import { Err, Ok } from 'ts-results-es'; import { Err, Ok } from 'ts-results-es';
/** /**

View File

@@ -1,5 +1,3 @@
import type { Message } from 'discord.js';
/** /**
* Removes the first character(s) _[depending on prefix length]_ of the message * Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg * @param msg
@@ -10,6 +8,6 @@ import type { Message } from 'discord.js';
* console.log(fmt(message, '!')); * console.log(fmt(message, '!'));
* // [ 'ping' ] * // [ 'ping' ]
*/ */
export function fmt(msg: Message, prefix: string): string[] { export function fmt(msg: string, prefix: string): string[] {
return msg.content.slice(prefix.length).trim().split(/\s+/g); return msg.slice(prefix.length).trim().split(/\s+/g);
} }

View File

@@ -19,13 +19,13 @@ import type {
MentionableSelectMenuInteraction, MentionableSelectMenuInteraction,
RoleSelectMenuInteraction, RoleSelectMenuInteraction,
StringSelectMenuInteraction, StringSelectMenuInteraction,
UserSelectMenuInteraction
} from 'discord.js'; } from 'discord.js';
import { CommandType } from '../handler/structures/enums'; import { CommandType } from '../handler/structures/enums';
import type { Args, SlashOptions } from './handler'; import type { Args, SlashOptions } from './handler';
import type Context from '../handler/structures/context'; import type Context from '../handler/structures/context';
import type { InitPlugin, ControlPlugin } from './plugin'; import type { InitPlugin, ControlPlugin } from './plugin';
import { EventType } from '../handler/structures/enums'; import { EventType } from '../handler/structures/enums';
import type { UserSelectMenuInteraction } from 'discord.js';
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin'; import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
import type { SernEventsMapping } from './handler'; import type { SernEventsMapping } from './handler';
import type { ClientEvents } from 'discord.js'; import type { ClientEvents } from 'discord.js';

View File

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