diff --git a/package.json b/package.json index da50b36..bf063ac 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,6 @@ ".": { "import": "./dist/index.js", "require": "./dist/index.js" - }, - "./internal": { - "import": "./dist/_internal.js", - "require": "./dist/_internal.js" } }, "scripts": { @@ -45,7 +41,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.1", - "@types/node": "^18.15.11", + "@types/node": "~18.17.11", "@typescript-eslint/eslint-plugin": "5.58.0", "@typescript-eslint/parser": "5.59.1", "discord.js": "^14.11.0", @@ -80,10 +76,7 @@ "allowTemplateLiterals": true } ], - "semi": [ - "error", - "always" - ], + "semi": [ "error", "always" ], "@typescript-eslint/no-empty-interface": 0, "@typescript-eslint/ban-types": 0, "@typescript-eslint/no-explicit-any": "off" @@ -94,7 +87,7 @@ "url": "git+https://github.com/sern-handler/handler.git" }, "engines": { - "node": ">= 18.16.x" + "node": ">= 18.17.x" }, "homepage": "https://sern.dev" } diff --git a/src/_internal.ts b/src/_internal.ts deleted file mode 100644 index fd40910..0000000 --- a/src/_internal.ts +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/core/_internal.ts b/src/core/_internal.ts index b7ab4f3..439c170 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -1,6 +1,5 @@ import type { Result } from 'ts-results-es' -export * from './operators'; export * from './functions'; export type _Module = { diff --git a/src/core/ioc/base.ts b/src/core/ioc/base.ts index 7542500..a33643b 100644 --- a/src/core/ioc/base.ts +++ b/src/core/ioc/base.ts @@ -72,8 +72,9 @@ async function composeRoot( //container should have no client or logger yet. const hasLogger = conf.exclude?.has('@sern/logger'); if (!hasLogger) { - __add_container('@sern/logger', new __Services.DefaultLogging); + __add_container('@sern/logger', new __Services.DefaultLogging()); } + __add_container('@sern/errors', new __Services.DefaultErrorHandling()); //Build the container based on the callback provided by the user conf.build(container as Container); @@ -97,6 +98,7 @@ export async function makeDependencies (conf: ValidDependencyConfig) { if(includeLogger) { __add_container('@sern/logger', new __Services.DefaultLogging); } + __add_container('@sern/errors', new __Services.DefaultErrorHandling()); await useContainerRaw().ready(); } else { await composeRoot(useContainerRaw(), conf); diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index 32bf7b5..cc7dc46 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -1,7 +1,9 @@ import path from 'node:path'; import { existsSync } from 'fs'; +import { readdir } from 'fs/promises'; import assert from 'node:assert'; - +import * as Id from './id' +import type { _Module } from './_internal'; export const parseCallsite = (site: string) => { const pathobj = path.parse(site.replace(/file:\\?/, "") @@ -27,7 +29,6 @@ export const shouldHandle = (pth: string, filenam: string) => { * commonjs, javascript : * ```js * exports = commandModule({ }) - * * //or * exports.default = commandModule({ }) * ``` @@ -37,16 +38,33 @@ export const shouldHandle = (pth: string, filenam: string) => { export async function importModule(absPath: string) { let fileModule = await import(absPath); - let commandModule = fileModule.default; + let commandModule: _Module = fileModule.default; assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`); if ('default' in commandModule) { - commandModule = commandModule.default; + commandModule = commandModule.default as _Module; } + const p = path.parse(absPath) + commandModule.name ??= p.name; commandModule.description ??= "..."; + commandModule.meta = { + //@ts-ignore + id: Id.create(commandModule.name, commandModule.type), + absPath, + }; return { module: commandModule } as T; } +export async function* readRecursive(dir: string): AsyncGenerator { + const files = await readdir(dir, { withFileTypes: true, recursive: true }); + for (const file of files) { + const fullPath = path.join(file.path, file.name); + if(!file.name.startsWith('!') && !file.isDirectory()) { + yield fullPath; + } + } +} + export const fmtFileName = (fileName: string) => path.parse(fileName).name; export const filename = (p: string) => fmtFileName(path.basename(p)); diff --git a/src/core/modules.ts b/src/core/modules.ts index a8c87c5..e2be1f4 100644 --- a/src/core/modules.ts +++ b/src/core/modules.ts @@ -7,29 +7,16 @@ import type { } from '../types/core-modules'; import { type _Module, partitionPlugins } from './_internal'; import type { Awaitable } from '../types/utility'; -import callsites, { type CallSite } from 'callsites'; -import * as Files from './module-loading' -import * as Id from './id' -const get_callsite = (css: CallSite[]) => { - return css.map(cs => cs.getFileName()).filter(Boolean) -} + /** * @since 1.0.0 The wrapper function to define command modules for sern * @param mod */ export function commandModule(mod: InputCommand): _Module { const [onEvent, plugins] = partitionPlugins(mod.plugins); - const initCallsite = get_callsite(callsites()).at(-2); - if(!initCallsite) throw Error("initCallsite is null"); - const { name, absPath } = Files.parseCallsite(initCallsite); - mod.name ??= name; mod.description ??= '...' //@ts-ignore return { ...mod, - meta: { - id: Id.create(mod.name, mod.type), - absPath - }, onEvent, plugins, }; @@ -41,17 +28,10 @@ export function commandModule(mod: InputCommand): _Module { */ export function eventModule(mod: InputEvent): _Module { const [onEvent, plugins] = partitionPlugins(mod.plugins); - const initCallsite = get_callsite(callsites()).at(-2); - if(!initCallsite) throw Error("initCallsite is null"); - const { name, absPath } = Files.parseCallsite(initCallsite); - mod.name ??= name; mod.description ??= '...' + //@ts-ignore return { ...mod, - meta: { - id: Id.create(mod.name, mod.type), - absPath - }, plugins, onEvent, }; diff --git a/src/core/structures/enums.ts b/src/core/structures/enums.ts index 663785e..ef398f1 100644 --- a/src/core/structures/enums.ts +++ b/src/core/structures/enums.ts @@ -48,16 +48,17 @@ export enum EventType { /** * The EventType for handling discord events */ - Discord = 1, + Discord, /** * The EventType for handling sern events */ - Sern = 2, + Sern, /** * The EventType for handling external events. * Could be for example, `process` events, database events */ - External = 3, + External, + Cron } /** diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index c199128..d77302b 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -14,13 +14,8 @@ import { pipe } from 'rxjs'; import { - callPlugin, - everyPluginOk, - filterMapTo, - handleError, type VoidResult, resultPayload, - arrayifySource, isAutocomplete, treeSearch, _Module, @@ -39,6 +34,7 @@ import { CommandType } from '../core/structures/enums' import type { Args } from '../types/utility'; import { inspect } from 'node:util' import { disposeAll } from '../core/ioc/base'; +import { arrayifySource, callPlugin, everyPluginOk, filterMapTo, handleError } from '../core/operators'; function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) { diff --git a/src/handlers/interaction.ts b/src/handlers/interaction.ts index d9b3c71..ffe9d2e 100644 --- a/src/handlers/interaction.ts +++ b/src/handlers/interaction.ts @@ -1,13 +1,12 @@ import type { Interaction } from 'discord.js'; import { mergeMap, merge, concatMap } from 'rxjs'; import { PayloadType } from '../core/structures/enums'; -import { filterTap } from '../core/operators' +import { filterTap, sharedEventStream } from '../core/operators' import { isAutocomplete, isCommand, isMessageComponent, isModal, - sharedEventStream, resultPayload, } from '../core/_internal'; import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils'; @@ -19,7 +18,7 @@ export function interactionHandler([emitter, err, log, client]: DependencyList) const handle = createInteractionHandler(interactionStream$, modules); const interactionHandler$ = merge(handle(isMessageComponent), - handle(isAutocomplete), + handle(isAutocomplete), handle(isCommand), handle(isModal)); return interactionHandler$ diff --git a/src/handlers/message.ts b/src/handlers/message.ts index bcc6a66..8046ac1 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -1,11 +1,11 @@ import { EMPTY, mergeMap, concatMap } from 'rxjs'; import type { Message } from 'discord.js'; -import { sharedEventStream } from '../core/_internal'; import type { DependencyList } from '../types/ioc'; import { createMessageHandler, executeModule, makeModuleExecutor } from './event-utils'; import { PayloadType, SernError } from '../core/structures/enums' import { resultPayload } from '../core/functions' -import { filterTap } from '../core/operators' +import { filterTap, sharedEventStream } from '../core/operators' + /** * Ignores messages from any person / bot except itself * @param prefix @@ -16,7 +16,7 @@ function isNonBot(prefix: string) { function hasPrefix(prefix: string, content: string) { const prefixInContent = content.slice(0, prefix.length); - return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent', }) === 0); + return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0); } export function messageHandler( diff --git a/src/handlers/ready-event.ts b/src/handlers/ready-event.ts index 188875d..22ea02a 100644 --- a/src/handlers/ready-event.ts +++ b/src/handlers/ready-event.ts @@ -1,13 +1,13 @@ -import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe, tap } from 'rxjs'; +import { concat, first, fromEvent, ignoreElements, pipe, tap } from 'rxjs'; import { _Module } from '../core/_internal'; import { Logging } from '../core/interfaces'; import type { DependencyList } from '../types/ioc'; import { callInitPlugins } from './event-utils'; -const once = (log: Logging | undefined) => pipe( - tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }), - first(), - ignoreElements()) +const once = (log: Logging | undefined) => + pipe(tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }), + first(), + ignoreElements()) export function readyHandler( [sEmitter, , log, client]: DependencyList, @@ -15,7 +15,8 @@ export function readyHandler( //Todo: add module manager on on ready const ready$ = fromEvent(client!, 'ready').pipe(once(log)); - return concat(ready$).pipe(callInitPlugins(sEmitter)).subscribe() + return concat(ready$).pipe(callInitPlugins(sEmitter)).subscribe(); + // const validModuleType = module.type >= 0 && module.type <= 1 << 10; // assert.ok(validModuleType, // `Found ${module.name} at ${module.meta.fullPath}, which does not have a valid type`); diff --git a/src/sern.ts b/src/sern.ts index 8d89de7..35875e9 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -27,8 +27,7 @@ interface Wrapper { * }) * ``` */ -export function init(wrapper?: Wrapper) { - wrapper ??= { commands: "./dist/commands", events: "./dist/events" }; +export function init(wrapper: Wrapper = { commands: "./dist/commands", events: "./dist/events" }) { const startTime = performance.now(); const dependencies = Services('@sern/emitter', '@sern/errors', @@ -52,7 +51,7 @@ export function init(wrapper?: Wrapper) { logger?.info({ message: `sern: registered in ${time} s`, }); if(presencePath.exists) { const setPresence = async (p: any) => { - return (dependencies[4] as Client).user?.setPresence(p); + return (dependencies[3] as Client).user?.setPresence(p); } presenceHandler(presencePath.path, setPresence).subscribe(); } diff --git a/src/types/core-modules.ts b/src/types/core-modules.ts index 2533a87..c594022 100644 --- a/src/types/core-modules.ts +++ b/src/types/core-modules.ts @@ -163,6 +163,8 @@ export interface EventModuleDefs { [EventType.Sern]: SernEventCommand; [EventType.Discord]: DiscordEventCommand; [EventType.External]: ExternalEventCommand; + //TODO + [EventType.Cron]: ExternalEventCommand; } export interface SernAutocompleteData diff --git a/src/types/core-plugin.ts b/src/types/core-plugin.ts index 5cff96f..331c9be 100644 --- a/src/types/core-plugin.ts +++ b/src/types/core-plugin.ts @@ -150,4 +150,8 @@ interface EventArgsMatrix { [PluginType.Control]: unknown[]; [PluginType.Init]: [InitArgs>]; }; + [EventType.Cron]: { + [PluginType.Control]: unknown[]; + [PluginType.Init]: [InitArgs>]; + }; } diff --git a/test/core/ioc.test.ts b/test/core/ioc.test.ts deleted file mode 100644 index 46cd0c0..0000000 --- a/test/core/ioc.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { CoreContainer } from '../../src/core/ioc/container'; -import { EventEmitter } from 'events'; -import { Disposable, Emitter, Init, Logging } from '../../src/core/interfaces'; -import * as __Services from '../../src/core/structures/default-services' -import { CoreDependencies } from '../../src/types/ioc'; - -describe('ioc container', () => { - let container: CoreContainer<{}> = new CoreContainer(); - let dependency: Logging & Init & Disposable; - let dependency2: Emitter - beforeEach(() => { - dependency = { - init: vi.fn(), - error(): void {}, - warning(): void {}, - info(): void {}, - debug(): void {}, - dispose: vi.fn() - }; - dependency2 = { - addListener: vi.fn(), - removeListener: vi.fn(), - emit: vi.fn() - }; - container = new CoreContainer(); - }); - const wait = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds)); - class DB implements Init, Disposable { - public connected = false - constructor() {} - async init() { - this.connected = true - await wait(10) - } - async dispose() { - await wait(20) - this.connected = false - } - } - 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/emitter', - '@sern/logger', - '@sern/errors', - ] satisfies (keyof CoreDependencies)[]; - container.add({ - '@sern/logger': () => new __Services.DefaultLogging(), - '@sern/client': () => new EventEmitter(), - }); - 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': dependency }); - container.ready(); - expect(dependency.init).to.toHaveBeenCalledOnce(); - }); - it('should dispose modules', async () => { - - container.upsert({ '@sern/logger': dependency }) - - container.ready(); - // We need to access the dependency at least once to be able to dispose of it. - container.get('@sern/logger' as never); - await container.disposeAll(); - expect(dependency.dispose).toHaveBeenCalledOnce(); - }); - - it('should init and dispose', async () => { - container.add({ db: new DB() }) - container.ready() - const db = container.get('db' as never) as DB - expect(db.connected).toBeTruthy() - - await container.disposeAll(); - - expect(db.connected).toBeFalsy() - }) - - it('should not lazy module', () => { - container.upsert({ '@sern/logger': () => dependency }); - container.ready(); - expect(dependency.init).toHaveBeenCalledTimes(0); - }); - - it('should init dependency depending on something else', () => { - container.add({ '@sern/client': dependency2 }); - container.upsert((cntr) => ({ '@sern/logger': dependency })); - container.ready(); - expect(dependency.init).toHaveBeenCalledTimes(1); - }) - - it('should detect a key already exists', () => { - container.add({ '@sern/client': dependency2 }); - expect(container.hasKey('@sern/client')).toBeTruthy() - }) - - - it('should detect a key already exists', () => { - container.add({ '@sern/client': () => dependency2 }); - expect(container.hasKey('@sern/client')).toBeTruthy() - }) - -}); diff --git a/test/core/services.test.ts b/test/core/services.test.ts deleted file mode 100644 index 09cea94..0000000 --- a/test/core/services.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SpyInstance, afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CoreContainer } from '../../src/core/ioc/container'; -import * as __Services from '../../src/core/structures/default-services'; -import { faker } from '@faker-js/faker'; -import { commandModule, CommandType } from '../../src'; - -function createRandomCommandModules() { - return commandModule({ - type: CommandType.Slash, - description: faker.string.alpha(), - name: faker.string.alpha({ length: { min: 5, max: 10 }}), - execute: vi.fn(), - }); -} -describe('services', () => { - //@ts-ignore - let container: CoreContainer; - let consoleMock: SpyInstance; - beforeEach(() => { - container = new CoreContainer(); - container.add({ '@sern/logger': () => new __Services.DefaultLogging() }); - container.ready(); - consoleMock = vi.spyOn(container.get('@sern/logger'), 'error').mockImplementation(() => {}); - }); - - afterAll(() => { - consoleMock.mockReset(); - }); - - //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(); - } else { - expect(() => errorHandler.updateAlive(new Error('poo'))).not.toThrowError(); - } - } - }); - //todo add more, spy on every instance? - it('logger', () => { - container.get('@sern/logger').error({ message: 'error' }); - - expect(consoleMock).toHaveBeenCalledOnce(); - expect(consoleMock).toHaveBeenLastCalledWith({ message: 'error' }); - }); - - - -}); diff --git a/yarn.lock b/yarn.lock index c380662..43cc46b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -235,7 +235,7 @@ __metadata: resolution: "@sern/handler@workspace:." dependencies: "@faker-js/faker": ^8.0.1 - "@types/node": ^18.15.11 + "@types/node": ~18.17.11 "@typescript-eslint/eslint-plugin": 5.58.0 "@typescript-eslint/parser": 5.59.1 callsites: ^3.1.0 @@ -262,10 +262,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.15.11": - version: 18.17.14 - resolution: "@types/node@npm:18.17.14" - checksum: f96ce1e588426a26cf82440193084f8bbab47bfb3c2e668cf174095f99ce808a20654b2137448c7e88cfd7b6c2b8521ffb6f714f521b3502ac595a0df0bff679 +"@types/node@npm:~18.17.11": + version: 18.17.19 + resolution: "@types/node@npm:18.17.19" + checksum: 6ab47127cd7534511aa199550659d56b44e5a6dbec9df054d0cde279926b4d43f0e6438f92c8392b039ab4e2a85aa0f698b95926430aff860e23bfc36c96576c languageName: node linkType: hard