feat: shrink package size, improve dev deps, esm and cjs support (#98)

* chore: fix tsc predicate type checking

* build: add tsup as bundler

* chore: revert text

* chore: fix predicates.ts, update dependencies, bump version

* docs: update example

* build: update dependencies

* fix: crash on collectors (#89)

* fix: crash on collectors

* feat: bump version for bug fix

* fix: crash on collectors pt

* docs: adding some documentation for docasaurus

* docs: add errors.ts comments

* docs: refactor comments

* docs: adding examples

* feat: refresh package-lock.json

* refactor: destructure and clean namespaces

* feat: add regen package.json script

* feat: add tsup, remove tsc, add scripts

* feat: update ts-results import style

* feat: readd typescript because idk if i should

* feat: breakup tsconfigs and add tsup config

* feat: add esm json tsconfig to git

* build: update dependencies and move to ts-result-es

* feat: remove unused function

* feat: update ts-results for esm/cjs interop!

* revert: remove version.txt

* build: goodbye tsc, hello tsup

* build: moving discord.js as dev dependency

* style: requested changes

* feat: add tsc back ( i missed you )

* build: bump version -> 1.0.0

* feat: syncing to main

* style: pretty

* feat: fix tsconfig issues with tsup

* revert: remove ExternallyUsed

* feat: update scripts

* build: update tsup and pkg-lock.json

* feat: refresh package-lock.json

* feat: test
This commit is contained in:
Jacob Nguyen
2022-08-06 15:51:19 -05:00
committed by GitHub
parent 0559fcca96
commit 74378f0f12
21 changed files with 1493 additions and 292 deletions

View File

@@ -1,5 +1,4 @@
src/
tsconfig.json
docs/
.gitignore
@@ -9,7 +8,6 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
@@ -106,4 +104,10 @@ CODE_OF_CONDUCT.md
babel.config.js
tests/
tsup.config.js
tsconfig-base.json
tsconfig.cjs.json
tsconfig.esm.json

1369
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,23 @@
{
"name": "@sern/handler",
"version": "1.1.9-beta",
"version": "1.0.0",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
"main": "dist/index.js",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"scripts": {
"compile": "tsc",
"watch": "tsc -w",
"watch": "tsup --watch --dts",
"clean-modules": "rimraf node_modules/ && npm install",
"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",
"publish": "npm run build && npm publish"
},
"keywords": [
"sern-handler",
@@ -21,16 +31,17 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"discord.js": "^14.1.2",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"ts-results": "^3.3.0"
"ts-results-es": "^3.5.0"
},
"devDependencies": {
"eslint": "8.20.0",
"@typescript-eslint/parser": "5.32.0",
"@typescript-eslint/eslint-plugin": "5.32.0",
"discord.js": "^14.1.2",
"prettier": "2.7.1",
"tsup": "^6.1.3",
"typescript": "4.7.4"
},
"repository": {

15
scripts/mkjson.mjs Normal file
View File

@@ -0,0 +1,15 @@
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

@@ -59,24 +59,24 @@ export default class InteractionHandler extends EventsHandler<{
override init() {
this.discordEvent.subscribe({
next: interaction => {
if (isMessageComponent(interaction)) {
const mod = Files.MessageCompCommands[interaction.componentType].get(
interaction.customId,
next: event => {
if (isMessageComponent(event)) {
const mod = Files.MessageCompCommands[event.componentType].get(
event.customId,
);
this.setState({ event: interaction, mod });
} else if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
this.setState({ event, mod });
} else if (isApplicationCommand(event) || isAutocomplete(event)) {
const mod =
Files.ApplicationCommands[interaction.commandType].get(
interaction.commandName,
) ?? Files.BothCommands.get(interaction.commandName);
this.setState({ event: interaction, mod });
} else if (isModalSubmit(interaction)) {
Files.ApplicationCommands[event.commandType].get(
event.commandName,
) ?? Files.BothCommands.get(event.commandName);
this.setState({ event, mod });
} else if (isModalSubmit(event)) {
/**
* maybe move modal submits into message component object maps?
*/
const mod = Files.ModalSubmitCommands.get(interaction.customId);
this.setState({ event: interaction, mod });
const mod = Files.ModalSubmitCommands.get(event.customId);
this.setState({ event, mod });
} else {
throw Error('This interaction is not supported yet');
}
@@ -96,31 +96,31 @@ export default class InteractionHandler extends EventsHandler<{
}
}
protected processModules(payload: { event: Interaction; mod: CommandModule }) {
return match(payload.mod)
protected processModules({ mod, event }: { event: Interaction; mod: CommandModule }) {
return match(mod)
.with(
{ type: P.union(CommandType.Slash, CommandType.Both) },
applicationCommandDispatcher(payload.event),
applicationCommandDispatcher(event),
)
.with(
{ type: CommandType.Modal },
modalCommandDispatcher(payload.event as ModalSubmitInteraction),
modalCommandDispatcher(event as ModalSubmitInteraction),
)
.with(
{ type: CommandType.Button },
buttonCommandDispatcher(payload.event as ButtonInteraction),
buttonCommandDispatcher(event as ButtonInteraction),
)
.with(
{ type: CommandType.MenuSelect },
selectMenuCommandDispatcher(payload.event as SelectMenuInteraction),
selectMenuCommandDispatcher(event as SelectMenuInteraction),
)
.with(
{ type: CommandType.MenuUser },
ctxMenuUserDispatcher(payload.event as UserContextMenuCommandInteraction),
ctxMenuUserDispatcher(event as UserContextMenuCommandInteraction),
)
.with(
{ type: CommandType.MenuMsg },
ctxMenuMsgDispatcher(payload.event as MessageContextMenuCommandInteraction),
ctxMenuMsgDispatcher(event as MessageContextMenuCommandInteraction),
)
.otherwise(() => {
throw Error(SernError.MismatchModule);

View File

@@ -2,31 +2,11 @@ import type { Message } from 'discord.js';
import { from, Observable, of, tap, throwError } from 'rxjs';
import { SernError } from '../structures/errors';
import type { Module, CommandModuleDefs, CommandModule } from '../structures/module';
import { correctModuleType } from '../utilities/predicates';
import type { Result } from 'ts-results';
import type { Result } from 'ts-results-es';
import type { CommandType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
import { PayloadType } from '../structures/enums';
export function filterCorrectModule<T extends keyof CommandModuleDefs>(cmdType: T) {
return (src: Observable<Module | undefined>) =>
new Observable<CommandModuleDefs[T]>(subscriber => {
return src.subscribe({
next(mod) {
if (mod === undefined) {
return throwError(() => SernError.UndefinedModule);
}
if (correctModuleType(mod, cmdType)) {
subscriber.next(mod!);
} else {
return throwError(() => SernError.MismatchModule);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
}
export function ignoreNonBot(prefix: string) {
return (src: Observable<Message>) =>

View File

@@ -11,7 +11,7 @@ import { processCommandPlugins } from './userDefinedEventsHandling';
import type { Awaitable } from 'discord.js';
import { SernError } from '../structures/errors';
import { match } from 'ts-pattern';
import { Err, Ok, type Result } from 'ts-results';
import { type Result, Err, Ok } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
export default class ReadyHandler extends EventsHandler<{
@@ -46,7 +46,7 @@ export default class ReadyHandler extends EventsHandler<{
if (allPluginsSuccessful) {
const res = registerModule(payload.mod);
if (res.err) {
throw Error(SernError.NonValidModuleType);
throw Error(SernError.InvalidModuleType);
}
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Success,

View File

@@ -7,14 +7,14 @@
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cooldowns, permissions, etc.
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { AutocompleteInteraction, Awaitable, Client, ClientEvents } from 'discord.js';
import type { Err, Ok, Result } from 'ts-results';
import type { CommandType, DefinitelyDefined, Override, SernEventsMapping } from '../..';
import { EventType, PluginType } from '../..';
import type { Result, Ok, Err } from 'ts-results-es';
import type { CommandType, DefinitelyDefined, Override, SernEventsMapping } from '../../index';
import { EventType, PluginType } from '../../index';
import type { BaseModule, CommandModuleDefs, EventModuleDefs } from '../structures/module';
import type { EventEmitter } from 'events';
import type {

View File

@@ -1,5 +1,5 @@
import type Wrapper from './structures/wrapper';
import { Err, Ok } from 'ts-results';
import { Err, Ok } from 'ts-results-es';
import { ExternalEventEmitters } from './utilities/readFile';
import type { EventEmitter } from 'events';
import { processEvents } from './events/userDefinedEventsHandling';
@@ -20,8 +20,16 @@ import MessageHandler from './events/messageHandler';
/**
*
* @param wrapper options to pass into sern.
* Function to start the handler up.
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
* ```ts title="src/index.ts"
* Sern.init({
* client,
* defaultPrefix: '!',
* commands: 'dist/commands',
* })
* ```
*/
export function init(wrapper: Wrapper) {
const { events } = wrapper;
@@ -40,20 +48,21 @@ export function init(wrapper: Wrapper) {
* As there are infinite possibilities to adding external event emitters,
* Most types aren't provided and are as narrow as possibly can.
* @example
* ```ts title="src/index.ts"
* //Add this before initiating Sern!
* Sern.addExternal(new Level())
* ```
* Sern.addExternal(new Level())
* @example
* ```ts title="events/level.ts"
* export default eventModule({
* emitter: 'Level',
* type : EventType.External,
* name: 'error',
* execute(args) {
* console.log(args)
* }
* })
* ```
* ```
* // events/level.ts
* export default eventModule({
* emitter: 'Level',
* type : EventType.External,
* name: 'error',
* execute(args) {
* console.log(args)
* }
* })
*
*/
export function addExternal<T extends EventEmitter>(emitter: T) {
if (ExternalEventEmitters.has(emitter.constructor.name)) {
@@ -62,11 +71,18 @@ export function addExternal<T extends EventEmitter>(emitter: T) {
ExternalEventEmitters.set(emitter.constructor.name, emitter);
}
/**
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: () => Ok.EMPTY,
stop: () => Err.EMPTY,
};
/**
* The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommandModule): CommandModule {
const onEvent: EventPlugin[] = [];
const plugins: CommandPlugin[] = [];
@@ -84,6 +100,10 @@ export function commandModule(mod: InputCommandModule): CommandModule {
plugins,
} as CommandModule;
}
/**
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEventModule): EventModule {
const onEvent: EventModuleEventPluginDefs[EventType][] = [];
const plugins: EventModuleCommandPluginDefs[EventType][] = [];

View File

@@ -11,9 +11,8 @@ import type {
TextBasedChannel,
User,
} from 'discord.js';
import { None, Option, Some } from 'ts-results';
import { type Option, None, Some } from 'ts-results-es';
import type { Nullish } from '../../types/handler';
import { ExternallyUsed } from '../utilities/externallyUsed';
import { SernError } from './errors';
function firstSome<T>(...args: Option<T>[]): Nullish<T> {
@@ -42,7 +41,6 @@ export default class Context {
* CommandType.Slash or the event fired in a Both command was
* ChatInputCommandInteraction
*/
@ExternallyUsed
public get message() {
return this.oMsg.expect(SernError.MismatchEvent);
}
@@ -51,12 +49,10 @@ export default class Context {
* CommandType.Text or the event fired in a Both command was
* Message
*/
@ExternallyUsed
public get interaction() {
return this.oInterac.expect(SernError.MismatchEvent);
}
@ExternallyUsed
public get id(): Snowflake {
return firstSome(
this.oInterac.map(i => i.id),
@@ -64,7 +60,6 @@ export default class Context {
)!;
}
@ExternallyUsed
public get channel(): Nullish<TextBasedChannel> {
return firstSome(
this.oMsg.map(m => m.channel),
@@ -72,7 +67,6 @@ export default class Context {
);
}
@ExternallyUsed
public get user(): User {
return firstSome(
this.oMsg.map(m => m.author),
@@ -80,7 +74,6 @@ export default class Context {
)!;
}
@ExternallyUsed
public get createdTimestamp(): number {
return firstSome(
this.oMsg.map(m => m.createdTimestamp),
@@ -88,7 +81,6 @@ export default class Context {
)!;
}
@ExternallyUsed
public get guild(): Guild {
return firstSome(
this.oMsg.map(m => m.guild),
@@ -96,7 +88,6 @@ export default class Context {
)!;
}
@ExternallyUsed
public get guildId(): Snowflake {
return firstSome(
this.oMsg.map(m => m.guildId),
@@ -107,7 +98,6 @@ export default class Context {
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
@ExternallyUsed
public get member(): Nullish<GuildMember | APIGuildMember> {
return firstSome(
this.oMsg.map(m => m.member),
@@ -115,7 +105,6 @@ export default class Context {
);
}
@ExternallyUsed
public get client(): Client {
return firstSome(
this.oMsg.map(m => m.client),
@@ -123,7 +112,6 @@ export default class Context {
)!;
}
@ExternallyUsed
public get inGuild(): boolean {
return firstSome(
this.oMsg.map(m => m.inGuild()),
@@ -138,12 +126,10 @@ export default class Context {
return new Context(Some(wrappable), None);
}
@ExternallyUsed
public isEmpty() {
return this.oMsg.none && this.oInterac.none;
}
//Make queueable
@ExternallyUsed
public reply(
content: string | Omit<InteractionReplyOptions, 'fetchReply'> | ReplyMessageOptions,
) {

View File

@@ -1,30 +1,119 @@
/**
* @enum { number };
* @enum { number }
* @example
* ```ts
* export default commandModule({
* // highlight-next-line
* type : CommandType.Text,
* name : 'a text command'
* execute(message) {
* console.log(message.content)
* }
* })
* ```
*/
export enum CommandType {
/**
* The CommandType for text commands
*/
Text = 0b00000000001,
/**
* The CommandType for slash commands
*/
Slash = 0b00000000010,
/**
* The CommandType for hybrid commands, text and slash
*/
Both = 0b0000011,
/**
* The CommandType for UserContextMenuInteraction commands
*/
MenuUser = 0b00000000100,
/**
* The CommandType for MessageContextMenuInteraction commands
*/
MenuMsg = 0b0000001000,
/**
* The CommandType for ButtonInteraction commands
*/
Button = 0b00000010000,
/**
* The CommandType for SelectMenuInteraction commands
*/
MenuSelect = 0b00000100000,
/**
* The CommandType for ModalSubmitInteraction commands
*/
Modal = 0b00001000000,
}
/**
* @enum { number }
* @example
* ```ts
* export default eventModule({
* //highlight-next-line
* type : EventType.Discord,
* name : 'guildMemberAdd'
* execute(member : GuildMember) {
* console.log(member)
* }
* })
* ```
*/
export enum EventType {
/**
* The EventType for handling discord events
*/
Discord = 0b01,
/**
* The EventType for handling sern events
*/
Sern = 0b10,
/**
* The EventType for handling external events.
* Could be for example, `process` events, database events
*/
External = 0b11,
}
/**
* @enum { number }
* @example
* ```ts
* export default function myPlugin() : EventPlugin<CommandType.Text> {
* //highlight-next-line
* type : PluginType.Event,
* execute([ctx, args], controller) {
* return controller.next();
* }
* }
* ```
*/
export enum PluginType {
/**
* The PluginType for CommandPlugins
*/
Command = 0b01,
/**
* The PluginType for EventPlugins
*/
Event = 0b10,
}
/**
* @enum { string }
*/
export enum PayloadType {
/**
* The PayloadType for a SernEmitter success event
*/
Success = 'success',
/**
* The PayloadType for a SernEmitter failure event
*/
Failure = 'failure',
/**
* The PayloadType for a SernEmitter warning event
*/
Warning = 'warning',
}

View File

@@ -1,9 +1,34 @@
/**
* @enum { string }
*/
export enum SernError {
NonValidModuleType = 'Detected an unknown module type',
UndefinedModule = `A module could not be detected at`,
/**
* 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`,
}

View File

@@ -11,6 +11,9 @@ import type {
import type { Awaitable, ClientEvents } from 'discord.js';
import type { EventType } from './enums';
/*
* Mapped type to generate all sern event modules
*/
export type SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping> =
Override<
BaseModule,
@@ -22,6 +25,9 @@ export type SernEventCommand<T extends keyof SernEventsMapping = keyof SernEvent
execute(...args: SernEventsMapping[T]): Awaitable<void | unknown>;
}
>;
/*
* Mapped type to generate all discord event modules
*/
export type DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents> = Override<
BaseModule,
{
@@ -32,7 +38,9 @@ export type DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvent
execute(...args: ClientEvents[T]): Awaitable<void | unknown>;
}
>;
/*
* Type for any event emitter that can be handled by sern
*/
export type ExternalEventCommand = Override<
BaseModule,
{

View File

@@ -30,7 +30,6 @@ export interface BaseModule {
execute: (ctx: Context, args: Args) => Awaitable<void | unknown>;
}
//possible refactoring types into interfaces and not types
export type TextCommand = Override<
BaseModule,
{
@@ -118,7 +117,6 @@ export type ModalSubmitCommand = Override<
// Autocomplete commands are a little different
// They can't have command plugins as they are
// in conjunction with chat input commands
// TODO: possibly in future, allow Autocmp commands in separate files?
export type AutocompleteCommand = Override<
BaseModule,
{
@@ -178,7 +176,7 @@ export type SernAutocompleteData = Override<
>;
/**
* Type that just uses SernAutocompleteData and not regular autocomplete
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type BaseOptions =
| ApplicationCommandChoicesData

View File

@@ -3,13 +3,8 @@ import type SernEmitter from '../sernEmitter';
import type { EventModule } from './module';
/**
* An object to be passed into Sern.Handler constructor.
* An object to be passed into Sern#init() function.
* @typedef {object} Wrapper
* @property {readonly Client} client
* @prop { readonly SernEmitter } sernEmitter
* @property {readonly string} defaultPrefix
* @property {readonly string} commands
* @prop { readonly DiscordEvent[] } events
*/
interface Wrapper {
readonly client: Client;

View File

@@ -1,10 +1,10 @@
import { ApplicationCommandType, ComponentType } from 'discord.js';
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { from, Observable } from 'rxjs';
import { type Observable, from, concatAll } from 'rxjs';
import type { CommandModule } from '../structures/module';
import { SernError } from '../structures/errors';
import { Err, Ok, type Result } from 'ts-results';
import { type Result, Err, Ok } from 'ts-results-es';
import type { EventEmitter } from 'events';
//Maybe move this? this probably doesnt belong in utlities/
@@ -63,15 +63,19 @@ export function buildData<T>(commandDir: string): Observable<
>
> {
const commands = getCommands(commandDir);
return from(
commands.map(absPath => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = <T | undefined>require(absPath).default;
return from(Promise.all(commands.map(async absPath => {
let mod : T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod = require(absPath).default;
} catch {
mod = (await import(`file:///` +absPath)).default;
}
if (mod !== undefined) {
return Ok({ mod, absPath });
} else return Err(SernError.UndefinedModule);
}),
);
})
)).pipe(concatAll());
}
export function getCommands(dir: string): string[] {

View File

@@ -1,10 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"resolveJsonModule": true,
"target": "esnext",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
@@ -14,6 +9,7 @@
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
},
"exclude": ["node_modules", "dist"],

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"
}
}

38
tsup.config.js Normal file
View File

@@ -0,0 +1,38 @@
import { defineConfig } from 'tsup';
export default defineConfig([
{
entry: ['src/index.ts'],
format : 'esm',
sourcemap: false,
target: 'node16',
tsconfig : './tsconfig-esm.json',
outDir : './dist/esm',
platform: 'node',
external: ['discord.js'],
clean: true,
treeshake: true,
outExtension() {
return {
js : '.mjs'
};
}
},
{
entry: ['src/index.ts'],
format : 'cjs',
splitting: false,
sourcemap: false,
external: ['discord.js'],
clean: true,
target: 'node16',
tsconfig : './tsconfig-cjs.json',
outDir : './dist/cjs',
platform: 'node',
outExtension() {
return {
js : '.cjs'
};
}
}
]);

View File

@@ -1 +0,0 @@
1.1.0-beta