meocord
Version:
Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.
113 lines (110 loc) • 3.71 kB
JavaScript
import 'reflect-metadata';
import { Container, injectable } from 'inversify';
import { MetadataKey } from '../enum/metadata-key.enum.js';
function isValueProvider(p) {
return 'useValue' in p;
}
/**
* Resolved test module. Retrieve instances via `.get()`.
*/ class TestingModule {
get(token) {
return this.container.get(token);
}
constructor(container){
this.container = container;
}
}
/**
* Builder returned by `MeoCordTestingModule.create()`.
* Call `.compile()` to get the resolved `TestingModule`.
*/ class TestingModuleBuilder {
overrideProvider(token) {
return {
useValue: (value)=>{
this.overrides.set(token, {
provide: token,
useValue: value
});
return this;
}
};
}
overrideGuard(guard) {
return {
useValue: (stub)=>{
this.guardOverrides.set(guard, stub);
return this;
}
};
}
compile() {
const container = new Container();
// Merge explicit providers with overrides (overrides win)
const providers = new Map();
for (const p of this.options.providers ?? []){
providers.set(p.provide, p);
}
for (const [token, override] of this.overrides){
providers.set(token, override);
}
// Bind explicit providers
for (const provider of providers.values()){
if (isValueProvider(provider)) {
container.bind(provider.provide).toConstantValue(provider.useValue);
} else {
const cls = provider.useClass;
if (!Reflect.hasMetadata(MetadataKey.Injectable, cls)) {
injectable()(cls);
}
container.bind(provider.provide).to(cls).inSingletonScope();
}
}
// Bind guard overrides — prevents inversify from auto-wiring guard dependencies
for (const [guardClass, stub] of this.guardOverrides){
container.bind(guardClass).toConstantValue(stub);
}
// Recursively bind controllers and their dependencies, skipping already-bound tokens
const bindClass = (cls)=>{
if (container.isBound(cls)) return;
if (!Reflect.hasMetadata(MetadataKey.Injectable, cls)) {
injectable()(cls);
}
container.bind(cls).toSelf().inSingletonScope();
const deps = Reflect.getMetadata(MetadataKey.ParamTypes, cls) || [];
for (const dep of deps){
bindClass(dep);
}
};
for (const ctrl of this.options.controllers ?? []){
bindClass(ctrl);
// Stamp container on controller class so @UseGuard works in tests too
Reflect.defineMetadata(MetadataKey.Container, container, ctrl);
}
return new TestingModule(container);
}
constructor(options){
this.options = options;
this.overrides = new Map();
this.guardOverrides = new Map();
}
}
/**
* Entry point for building isolated test modules.
*
* @example
* ```typescript
* const module = MeoCordTestingModule.create({
* controllers: [PingController],
* providers: [
* { provide: PingService, useValue: { handlePing: jest.fn().mockResolvedValue('pong') } },
* ],
* }).compile()
*
* const controller = module.get(PingController)
* ```
*/ class MeoCordTestingModule {
static create(options) {
return new TestingModuleBuilder(options);
}
}
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder };