├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ ├── bug_report.yml │ └── regression.yml ├── workflows │ └── test.yml ├── PULL_REQUEST_TEMPLATE.md └── labels.yml ├── .husky ├── pre-commit ├── commit-msg └── check-gpg ├── pnpm-workspace.yaml ├── packages ├── core │ ├── context │ │ ├── index.ts │ │ └── context.ts │ ├── adapter │ │ ├── index.ts │ │ └── adapter.abstract.ts │ ├── types │ │ ├── index.ts │ │ └── gland-broker.type.ts │ ├── application │ │ ├── index.ts │ │ ├── application-initial.ts │ │ ├── application-lifecycle.ts │ │ └── application-binder.ts │ ├── hooks │ │ ├── index.ts │ │ ├── lifecycle.interface.ts │ │ └── lifecycle.scanner.ts │ ├── injector │ │ ├── container │ │ │ ├── index.ts │ │ │ ├── module-container.ts │ │ │ └── container.ts │ │ ├── scanner │ │ │ ├── index.ts │ │ │ ├── metadata-scanner.ts │ │ │ └── dependencies-scanner.ts │ │ ├── index.ts │ │ ├── instance-wrapper.ts │ │ ├── module.ts │ │ ├── graph.ts │ │ ├── discovery-service.ts │ │ └── explorer.ts │ ├── index.ts │ ├── tsconfig.json │ ├── gland-broker.ts │ ├── gland-factory.ts │ ├── package.json │ └── README.md ├── common │ ├── types │ │ ├── index.ts │ │ └── modules.type.ts │ ├── decorators │ │ ├── core │ │ │ ├── index.ts │ │ │ └── controller.decorator.ts │ │ ├── modules │ │ │ ├── index.ts │ │ │ └── module.decorator.ts │ │ ├── events │ │ │ ├── index.ts │ │ │ ├── channel.decorator.ts │ │ │ └── on.decorator.ts │ │ └── index.ts │ ├── interfaces │ │ ├── index.ts │ │ ├── gland-events.interfaces.ts │ │ └── modules.interfaces.ts │ ├── utils │ │ ├── index.ts │ │ ├── shared.util.ts │ │ ├── load-pkg.util.ts │ │ └── uuid.util.ts │ ├── constant.ts │ ├── tsconfig.json │ ├── index.ts │ ├── package.json │ └── README.md └── tsconfig.json ├── .npmrc ├── samples └── 01-simple │ ├── .gitignore │ ├── package.json │ ├── src │ ├── main.ts │ ├── shared │ │ └── events.interface.ts │ ├── modules │ │ └── product │ │ │ ├── product.module.ts │ │ │ ├── product.channel.ts │ │ │ └── product.controller.ts │ ├── common │ │ └── db.channel.ts │ └── app.module.ts │ └── tsconfig.json ├── .prettierignore ├── .env.example ├── test ├── integration │ └── lazy-modules │ │ └── src │ │ ├── app.module.ts │ │ ├── main.ts │ │ └── global.module.ts └── unit │ └── injector.spec.ts ├── .prettierrc ├── .npmignore ├── .changeset └── config.json ├── .gitignore ├── commitlint.config.cjs ├── LICENSE ├── tsconfig.json ├── docs ├── CHANGELOG.md ├── SECURITY.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── package.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: gland 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | npm run lint-staged -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /packages/core/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /packages/common/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules.type'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | npx --no -- commitlint --edit ${1} -------------------------------------------------------------------------------- /packages/core/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter.abstract'; 2 | -------------------------------------------------------------------------------- /packages/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gland-broker.type'; 2 | -------------------------------------------------------------------------------- /packages/core/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application-initial'; 2 | -------------------------------------------------------------------------------- /samples/01-simple/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | # dist 4 | /dist 5 | -------------------------------------------------------------------------------- /packages/core/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lifecycle.scanner'; 2 | export * from './lifecycle.interface'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/**/*.d.ts 2 | packages/**/*.js 3 | packages/**/*.json 4 | pnpm-lock.yaml 5 | .github/* 6 | -------------------------------------------------------------------------------- /packages/common/decorators/core/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './controller.decorator'; 3 | -------------------------------------------------------------------------------- /packages/common/decorators/modules/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './module.decorator'; 3 | -------------------------------------------------------------------------------- /packages/core/injector/container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './container'; 2 | export * from './module-container'; 3 | -------------------------------------------------------------------------------- /packages/common/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules.interfaces'; 2 | export * from './gland-events.interfaces'; 3 | -------------------------------------------------------------------------------- /packages/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uuid.util'; 2 | export * from './shared.util'; 3 | export * from './load-pkg.util'; 4 | -------------------------------------------------------------------------------- /packages/common/decorators/events/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './on.decorator'; 3 | export * from './channel.decorator'; 4 | -------------------------------------------------------------------------------- /packages/common/decorators/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './core'; 3 | export * from './events'; 4 | export * from './modules'; 5 | -------------------------------------------------------------------------------- /packages/core/injector/scanner/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './dependencies-scanner'; 3 | export * from './metadata-scanner'; 4 | -------------------------------------------------------------------------------- /packages/common/constant.ts: -------------------------------------------------------------------------------- 1 | export const PATH_METADATA = 'path'; 2 | export const METHOD_METADATA = 'method'; 3 | export const MODULE_METADATA = '__module__'; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Enable debug mode for GlandJS core 2 | # Set to 'true' to show detailed logs (e.g. internal bindings, module resolution, etc.) 3 | GLAND_DEBUG=true 4 | -------------------------------------------------------------------------------- /packages/core/injector/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | export * from './container'; 3 | export * from './scanner'; 4 | export * from './explorer'; 5 | export * from './discovery-service'; 6 | -------------------------------------------------------------------------------- /packages/core/types/gland-broker.type.ts: -------------------------------------------------------------------------------- 1 | import type { GlandEvents } from '@glandjs/common'; 2 | import type { Broker } from '@glandjs/events'; 3 | 4 | export type TGlandBroker = Broker; 5 | -------------------------------------------------------------------------------- /test/integration/lazy-modules/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@glandjs/common'; 2 | import { GlobalModule } from './global.module'; 3 | 4 | @Module({ 5 | imports: [GlobalModule], 6 | }) 7 | export class AppModule {} 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/*.ts 2 | *.ts 3 | 4 | !**/*.d.ts 5 | !*.d.ts 6 | 7 | tsconfig.json 8 | tsconfig.tsbuildinfo 9 | .prettierrc 10 | .prettierignore 11 | .gitignore 12 | .env 13 | pnpm-workspace.yaml 14 | pnpm-lock.yaml 15 | .husky 16 | bin 17 | docs 18 | .changeset 19 | .github 20 | exmaples -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "outDir": ".", 6 | "rootDir": ".", 7 | "paths": {} 8 | }, 9 | "include": ["**/*.ts"], 10 | "exclude": ["node_modules"], 11 | "references": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Gland Common 3 | * Copyright(c) 2024 - 2025 Mahdi 4 | * MIT Licensed 5 | */ 6 | import 'reflect-metadata'; 7 | export * from './decorators'; 8 | export * from './utils'; 9 | export * from './types'; 10 | export * from './interfaces'; 11 | export * from './constant'; 12 | -------------------------------------------------------------------------------- /packages/core/injector/container/module-container.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '../module'; 2 | 3 | export class ModulesContainer extends Map { 4 | public getByToken(token: string): Module | undefined { 5 | return [...this.values()].find((moduleRef) => moduleRef.token === token); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/decorators/core/controller.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PATH_METADATA } from '../../constant'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function Controller(path?: string): ClassDecorator { 7 | return (target: object) => { 8 | Reflect.defineMetadata(PATH_METADATA, path, target); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/common/decorators/events/channel.decorator.ts: -------------------------------------------------------------------------------- 1 | import { PATH_METADATA } from '../../constant'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function Channel(event?: string): ClassDecorator { 7 | return (target: Function) => { 8 | Reflect.defineMetadata(PATH_METADATA, event, target); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/common/types/modules.type.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule } from '../interfaces'; 2 | import { Constructor } from '@medishn/toolkit'; 3 | export type ImportableModule = Constructor | DynamicModule | Promise; 4 | 5 | export type InjectionToken = string | symbol | Constructor | Function; 6 | -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Gland Core Module 3 | * Copyright(c) 2024 - 2025 Mahdi 4 | * MIT Licensed 5 | */ 6 | import 'reflect-metadata'; 7 | 8 | export * from './injector'; 9 | export * from './adapter'; 10 | export * from './context'; 11 | export * from './hooks'; 12 | export * from './gland-factory'; 13 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/decorators/events/on.decorator.ts: -------------------------------------------------------------------------------- 1 | import { METHOD_METADATA } from '../../constant'; 2 | 3 | /** 4 | * @publicApi 5 | */ 6 | export function On(event: string): MethodDecorator { 7 | return (target: object, key: string | symbol) => { 8 | Reflect.defineMetadata(METHOD_METADATA, event, target, key); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /.husky/check-gpg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | last_hash=$(git rev-parse HEAD) 3 | sig_status=$(git verify-commit "$last_hash" 2>&1 || true) 4 | 5 | if [[ $sig_status != *"Good signature"* ]]; then 6 | echo "🔒 Error: Your last commit is not GPG-signed." 7 | echo " Please sign it: git commit -S" 8 | exit 1 9 | fi 10 | 11 | echo "✅ GPG signature verified." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/*/package-lock.json 2 | tsconfig.tsb* 3 | # dependencies 4 | node_modules/ 5 | 6 | # bundle 7 | packages/**/**/*.d.ts 8 | packages/**/**/*.js 9 | 10 | # Environment variables 11 | .env 12 | 13 | # misc 14 | .DS_Store 15 | .idea/ 16 | .vscode/ 17 | /packages/**/.npmignore 18 | /packages/**/LICENSE 19 | 20 | bin/* 21 | .nyc_output/* 22 | coverage.md 23 | tags* 24 | -------------------------------------------------------------------------------- /packages/common/utils/shared.util.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule } from '../interfaces'; 2 | 3 | export const normalizePath = (path?: string): string => (path ? (path.startsWith('/') ? ('/' + path.replace(/\/+$/, '')).replace(/\/+/g, '/') : '/' + path.replace(/\/+$/, '')) : '/'); 4 | 5 | export function isDynamicModule(module: any): module is DynamicModule { 6 | return !!module?.module; 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/lazy-modules/src/main.ts: -------------------------------------------------------------------------------- 1 | import { GlandFactory } from '@glandjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ExpressBroker } from '@glandjs/express'; 4 | 5 | async function bootstrap() { 6 | const app = await GlandFactory.create(AppModule); 7 | const express = app.connectTo(ExpressBroker); 8 | express.listen(3000); 9 | } 10 | void bootstrap(); 11 | -------------------------------------------------------------------------------- /packages/common/decorators/modules/module.decorator.ts: -------------------------------------------------------------------------------- 1 | import { MODULE_METADATA } from '../../constant'; 2 | import { ModuleMetadata } from '../../interfaces'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export function Module(metadata: ModuleMetadata): ClassDecorator { 8 | return (target: Function) => { 9 | Reflect.defineMetadata(MODULE_METADATA, metadata, target); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/packages/common" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/packages/core" 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: "npm" 12 | directory: "/packages/events" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /packages/common/interfaces/gland-events.interfaces.ts: -------------------------------------------------------------------------------- 1 | interface GlandRoute { 2 | path: string; 3 | method: string; 4 | action: (ctx) => void; 5 | meta: { 6 | path: string; 7 | method: string; 8 | }; 9 | } 10 | interface GlandChannel {} 11 | export interface GlandEvents { 12 | 'gland:define:route': GlandRoute; 13 | [key: `gland:define:channel:${string}:${string}`]: GlandChannel; 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Support Questions 4 | url: https://stackoverflow.com/questions/tagged/gland 5 | about: 'Please ask and answer support questions here instead of opening an issue.' 6 | - name: 💬 Community Discussions 7 | url: https://github.com/glandjs/gland/discussions 8 | about: 'For feature discussions and general feedback, use GitHub Discussions.' 9 | -------------------------------------------------------------------------------- /test/integration/lazy-modules/src/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Channel, Module, On } from '@glandjs/common'; 2 | import type { HttpContext } from '@glandjs/http'; 3 | 4 | @Channel() 5 | class GlobalChannel { 6 | OnInit() {} 7 | 8 | @On('send') 9 | send(ctx: HttpContext) { 10 | ctx.send('Hello World'); 11 | } 12 | } 13 | 14 | @Module({ 15 | channels: [GlobalChannel], 16 | exports: [GlobalChannel], 17 | }) 18 | export class GlobalModule {} 19 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": ".", 5 | "rootDir": ".", 6 | "paths": { 7 | "@glandjs/common": ["../common"], 8 | "@glandjs/common/*": ["../common/*"], 9 | }, 10 | "types": ["node"] 11 | }, 12 | "include": ["**/*.ts"], 13 | "exclude": ["node_modules"], 14 | "references": [ 15 | { 16 | "path": "../common/tsconfig.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/hooks/lifecycle.interface.ts: -------------------------------------------------------------------------------- 1 | export interface OnModuleInit { 2 | onModuleInit(): Promise | void; 3 | } 4 | export interface OnModuleDestroy { 5 | onModuleDestroy(): Promise | void; 6 | } 7 | 8 | export interface OnAppBootstrap { 9 | onAppBootstrap(): Promise | void; 10 | } 11 | export interface OnAppShutdown { 12 | onAppShutdown(signal?: string): Promise | void; 13 | } 14 | export interface OnChannelInit { 15 | onChannelInit(): Promise | void; 16 | } 17 | -------------------------------------------------------------------------------- /samples/01-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gland-samples-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "license": "MIT", 6 | "scripts": {}, 7 | "dependencies": { 8 | "@glandjs/events": "1.1.0", 9 | "@glandjs/common": "^1.0.3-beta", 10 | "@glandjs/core": "^1.0.3-beta", 11 | "@glandjs/express": "^1.0.0-beta", 12 | "@glandjs/http": "^1.0.0-beta" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "22.14.1", 16 | "typescript": "5.7.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/01-simple/src/main.ts: -------------------------------------------------------------------------------- 1 | import { GlandFactory } from '@glandjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ExpressBroker } from '@glandjs/express'; 4 | import type { EventTypes } from './shared/events.interface'; 5 | async function bootstrap() { 6 | const app = await GlandFactory.create(AppModule); 7 | const express = app.connectTo(ExpressBroker); 8 | express.json(); 9 | express.urlencoded({ extended: true }); 10 | express.listen(3000); 11 | } 12 | void bootstrap(); 13 | -------------------------------------------------------------------------------- /packages/core/injector/instance-wrapper.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from '@medishn/toolkit'; 2 | 3 | export class InstanceWrapper { 4 | constructor( 5 | public readonly token: Constructor, 6 | private readonly instance?: T, 7 | ) {} 8 | 9 | get id(): string { 10 | return this.token?.toString() || this.token?.name || 'unknown'; 11 | } 12 | 13 | getInstance(): T { 14 | if (!this.instance) { 15 | throw new Error(`Instance ${this.id} not initialized`); 16 | } 17 | return this.instance; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/01-simple/src/shared/events.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ExpressContext } from '@glandjs/express'; 2 | import { IOEvent } from '@glandjs/events'; 3 | export interface Product { 4 | id: string; 5 | name: string; 6 | price: number; 7 | stock: number; 8 | } 9 | 10 | export interface EventTypes { 11 | // Product events 12 | 'product:viewed': { id: string; ctx: ExpressContext }; 13 | 14 | /// database events \\ 15 | 16 | 'db:product:create': IOEvent, Promise>; 17 | 'db:product:all-products': IOEvent<{}, Promise>; 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/adapter/adapter.abstract.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from '@medishn/toolkit'; 2 | import type { TGlandBroker } from '../types'; 3 | import { EventRecord } from '@glandjs/events'; 4 | export abstract class BrokerAdapter { 5 | public broker: TEvents & TGlandBroker; 6 | public instance: TApp; 7 | public abstract initialize(): TApp; 8 | constructor(protected readonly options?: TOptions) {} 9 | } 10 | export type BrokerAdapterClass = Constructor>; 11 | -------------------------------------------------------------------------------- /samples/01-simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "useDefineForClassFields": false, 15 | "incremental": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/common/utils/load-pkg.util.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@medishn/toolkit'; 2 | 3 | const MISSING_REQUIRED_DEPENDENCY = (name: string, reason: string) => `The "${name}" package is missing. Please, make sure to install it to take advantage of ${reason}.`; 4 | 5 | const logger = new Logger({ context: 'PackageLoader' }); 6 | 7 | export function loadPackage(packageName: string, context: string, loaderFn?: Function) { 8 | try { 9 | return loaderFn ? loaderFn() : require(packageName); 10 | } catch (e) { 11 | logger.error(MISSING_REQUIRED_DEPENDENCY(packageName, context)); 12 | process.exit(1); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/interfaces/modules.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '@medishn/toolkit'; 2 | import { ImportableModule, InjectionToken } from '../types'; 3 | 4 | export interface ModuleMetadata { 5 | imports?: ImportableModule[]; 6 | controllers?: Constructor[]; 7 | channels?: Constructor[]; 8 | } 9 | 10 | export interface DynamicModule { 11 | controllers?: Constructor[]; 12 | channels?: Constructor[]; 13 | imports?: ImportableModule[]; 14 | exports?: InjectionToken[]; 15 | module: Constructor; 16 | /** 17 | * @default false 18 | */ 19 | global?: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /samples/01-simple/src/modules/product/product.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@glandjs/common'; 2 | import { ProductController } from './product.controller'; 3 | import { ProductChannel } from './product.channel'; 4 | import type { OnModuleDestroy, OnModuleInit } from '@glandjs/core'; 5 | @Module({ 6 | controllers: [ProductController], 7 | channels: [ProductChannel], 8 | }) 9 | export class ProductModule implements OnModuleInit, OnModuleDestroy { 10 | onModuleInit(): void { 11 | console.log('[ProductModule] Module initialized'); 12 | } 13 | onModuleDestroy(): void { 14 | console.log('[ProductModule] Module destroyed'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/01-simple/src/modules/product/product.channel.ts: -------------------------------------------------------------------------------- 1 | import { Channel, On } from '@glandjs/common'; 2 | import type { EventTypes } from '../../shared/events.interface'; 3 | import { Database } from '../../common/db.channel'; 4 | import type { OnChannelInit } from '@glandjs/core'; 5 | 6 | @Channel('product') 7 | export class ProductChannel implements OnChannelInit { 8 | onChannelInit(): void { 9 | console.log('[ProductChannel] Channel initialized'); 10 | } 11 | @On('viewed') 12 | async onProductViewed(payload: EventTypes['product:viewed']) { 13 | console.log(`Product ${payload.id} was viewed!`); 14 | return { tracked: true }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/injector/scanner/metadata-scanner.ts: -------------------------------------------------------------------------------- 1 | export class MetadataScanner { 2 | scanFromPrototype(prototype: object, handler: (methodName: string) => R): R[] { 3 | const methodNames = this.getAllFilteredMethodNames(prototype); 4 | return methodNames.map(handler); 5 | } 6 | getAllFilteredMethodNames(prototype: object): string[] { 7 | const methodNames = Object.getOwnPropertyNames(prototype); 8 | return methodNames.filter((method) => { 9 | const descriptor = Object.getOwnPropertyDescriptor(prototype, method); 10 | return descriptor && typeof descriptor.value === 'function' && method !== 'constructor'; 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [2, 'always', 200], 5 | 'footer-max-line-length': [2, 'always', 200], 6 | 'header-max-length': [2, 'always', 200], 7 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], 8 | 'subject-empty': [2, 'never'], 9 | 'subject-full-stop': [2, 'never', '.'], 10 | 'type-case': [2, 'always', 'lower-case'], 11 | 'type-empty': [2, 'never'], 12 | 'type-enum': [2, 'always', ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test']], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/gland-broker.ts: -------------------------------------------------------------------------------- 1 | import { EventBroker, EventRecord } from '@glandjs/events'; 2 | import type { BrokerAdapterClass } from './adapter'; 3 | import type { TGlandBroker } from './types/gland-broker.type'; 4 | 5 | export class GlandBroker { 6 | public readonly broker: TGlandBroker; 7 | 8 | constructor() { 9 | this.broker = new EventBroker({ name: '@glandjs/core' }) as TGlandBroker; 10 | } 11 | 12 | connectTo(AdapterClass: BrokerAdapterClass, options?: TOptions): TApp { 13 | const adapter = new AdapterClass(options); 14 | const broker = adapter.broker as any; 15 | broker.connectTo(this.broker); 16 | return adapter.initialize(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/gland-factory.ts: -------------------------------------------------------------------------------- 1 | import { Logger, type Constructor } from '@medishn/toolkit'; 2 | import { ApplicationInitial } from './application'; 3 | import { GlandBroker } from './gland-broker'; 4 | 5 | export class GlandFactory { 6 | private gland = new GlandBroker(); 7 | private readonly logger = new Logger({ 8 | context: 'Gland', 9 | }); 10 | get debugMode(): boolean { 11 | return !!process.env.GLAND_DEBUG; 12 | } 13 | 14 | static async create(root: Constructor): Promise { 15 | const instance = new GlandFactory(); 16 | 17 | const broker = instance.gland.broker; 18 | const initial = new ApplicationInitial(broker, instance.logger, instance.debugMode); 19 | initial.initialize(root); 20 | return instance.gland; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "removeComments": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "useUnknownInCatchVariables": false, 12 | "allowJs": false, 13 | "composite": true, 14 | "declaration": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": true, 17 | "types": ["node"], 18 | "noImplicitAny": false, 19 | "noUnusedLocals": false, 20 | "noLib": false, 21 | "sourceMap": false, 22 | "strictPropertyInitialization": false 23 | }, 24 | "references": [ 25 | { 26 | "path": "./common/tsconfig.json" 27 | }, 28 | { 29 | "path": "./core/tsconfig.json" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /samples/01-simple/src/common/db.channel.ts: -------------------------------------------------------------------------------- 1 | import { Channel, On } from '@glandjs/common'; 2 | import type { OnChannelInit } from '@glandjs/core'; 3 | import type { Product } from '../shared/events.interface'; 4 | 5 | @Channel('db') 6 | export class Database implements OnChannelInit { 7 | onChannelInit(): void { 8 | console.log('[DatabaseChannel] Channel initialized'); 9 | } 10 | private products: Map = new Map(); 11 | @On('product:create') 12 | createProduct(product: Omit): Product { 13 | const id = Math.random().toString(36).substring(2, 9); 14 | const newProduct = { id, ...product }; 15 | this.products.set(id, newProduct); 16 | 17 | return newProduct || {}; 18 | } 19 | 20 | @On('product:all-products') 21 | async getAllProducts(): Promise { 22 | const result = Array.from(this.products.values()); 23 | return result; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/01-simple/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@glandjs/common'; 2 | import { ProductModule } from './modules/product/product.module'; 3 | import { Database } from './common/db.channel'; 4 | import type { OnAppBootstrap, OnAppShutdown, OnModuleDestroy, OnModuleInit } from '@glandjs/core/hooks'; 5 | 6 | @Module({ 7 | imports: [ProductModule], 8 | channels: [Database], 9 | }) 10 | export class AppModule implements OnModuleInit, OnModuleDestroy, OnAppBootstrap, OnAppShutdown { 11 | onModuleInit(): void { 12 | console.log('[AppModule] Module initialized'); 13 | } 14 | 15 | onAppBootstrap(): void { 16 | console.log('[AppModule] Application has bootstrapped'); 17 | } 18 | 19 | onAppShutdown(signal?: string): void { 20 | console.log(`[AppModule] Application is shutting down due to signal: ${signal}`); 21 | } 22 | 23 | onModuleDestroy(): void { 24 | console.log('[AppModule] Module is being destroyed'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/injector/module.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@glandjs/common'; 2 | import { Constructor } from '@medishn/toolkit'; 3 | import { InstanceWrapper } from './instance-wrapper'; 4 | 5 | export class Module { 6 | public readonly imports = new Set(); 7 | public readonly controllers = new Map>(); 8 | public readonly channels = new Map>(); 9 | constructor( 10 | public readonly token: string, 11 | public readonly metatype: Constructor, 12 | ) {} 13 | public addImports(imports: Module[]) { 14 | imports.forEach((imp) => this.imports.add(imp)); 15 | } 16 | 17 | public addController(controller: Constructor, instance?: any): void { 18 | const wrapper = new InstanceWrapper(controller, instance); 19 | this.controllers.set(controller, wrapper); 20 | } 21 | 22 | public addChannel(channel: Constructor, instance?: any): void { 23 | const wrapper = new InstanceWrapper(channel, instance); 24 | 25 | this.channels.set(channel, wrapper); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Medishn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/01-simple/src/modules/product/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@glandjs/common'; 2 | import type { ExpressContext } from '@glandjs/express'; 3 | import { Get, Post } from '@glandjs/http'; 4 | import type { EventTypes } from '../../shared/events.interface'; 5 | 6 | @Controller('products') 7 | export class ProductController { 8 | @Get() 9 | async getAllProducts(ctx: ExpressContext) { 10 | const products = await ctx.call('db:product:all-products', {}); 11 | return ctx.send({ products }); 12 | } 13 | 14 | @Get(':id') 15 | async getProductById(ctx: ExpressContext) { 16 | const { id } = ctx.params || {}; 17 | ctx.emit('product:viewed', { id, ctx }); 18 | 19 | return ctx.send({ params: ctx.params }); 20 | } 21 | 22 | @Post() 23 | async createProduct(ctx: ExpressContext) { 24 | const { name, price, stock } = ctx.body || {}; 25 | 26 | if (!name || !price) { 27 | return ctx.send({ error: 'Name and price are required' }, 400); 28 | } 29 | 30 | const product = await ctx.call('db:product:create', { name, price, stock: stock || 0 }); 31 | return ctx.send({ product }, 201); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@glandjs/common", 3 | "version": "1.0.3-beta", 4 | "description": "Glands is a web framework for Node.js (@common)", 5 | "author": "Mahdi ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/glandjs/gland#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/glandjs/gland.git" 11 | }, 12 | "engines": { 13 | "node": ">= 20", 14 | "typescript": ">=5" 15 | }, 16 | "scripts": { 17 | "build": "tsc", 18 | "clean": "rm -rf dist *.d.ts *.js **/*.js **/*.d.ts */**/*.js */**/*.d.ts", 19 | "typecheck": "tsc --noEmit", 20 | "dev": "tsc --watch" 21 | }, 22 | "keywords": [ 23 | "gland", 24 | "glandjs", 25 | "common", 26 | "nodejs", 27 | "typescript", 28 | "decorators", 29 | "interfaces", 30 | "utilities", 31 | "modular" 32 | ], 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "dependencies": { 37 | "@medishn/toolkit": "^1.0.4", 38 | "tslib": "2.8.1" 39 | }, 40 | "peerDependencies": { 41 | "reflect-metadata": "^0.2.2" 42 | }, 43 | "devDependencies": { 44 | "typescript": "^5.5.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "removeComments": true, 8 | "composite": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "strictNullChecks": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "useUnknownInCatchVariables": false, 17 | "allowJs": false, 18 | "skipLibCheck": true, 19 | "moduleResolution": "node", 20 | "rootDir": ".", 21 | "baseUrl": ".", 22 | "types": ["node"], 23 | "paths": { 24 | "@glandjs/common": ["./packages/common"], 25 | "@glandjs/common/*": ["./packages/common/*"], 26 | "@glandjs/core": ["./packages/core"], 27 | "@glandjs/core/*": ["./packages/core/*"] 28 | }, 29 | "noImplicitAny": false, 30 | "noUnusedLocals": false, 31 | "strictPropertyInitialization": false, 32 | "noLib": false 33 | }, 34 | "include": ["packages/**/*"], 35 | "compileOnSave": true, 36 | "buildOnSave": true, 37 | "preserveSymlinks": true, 38 | "exclude": ["node_modules"] 39 | } 40 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.0-alpha.1] - 2025-03-28 4 | 5 | ### Added 6 | 7 | - **Core Framework**: Initial alpha release of the Gland framework with core packages: 8 | - `@glandjs/core`: Dependency Injection (DI), basic framework setup. 9 | - `@glandjs/http`: HTTP server handling (routes, controllers). 10 | - `@glandjs/events`: Event-driven architecture with support for various protocols. 11 | - `@glandjs/common`: Common decorators and utilities (like `@Module`, `@Controller`, `@Get`, etc.). 12 | - **Example project**: Added a small example showcasing the use of Gland framework, including routing and event handling. 13 | - Example: [Very Simple Example](https://github.com/glandjs/gland/blob/main/examples/very-simple.ts) 14 | 15 | ### Changes 16 | 17 | - **Package name**: Renamed from `gland` to `glandjs` due to an existing namespace on npm. All the packages have been published under `@glandjs`. 18 | 19 | ### Known Issues 20 | 21 | - **HTTP Static Methods**: Some static methods in the HTTP package are currently non-functional. 22 | - **Minor bugs**: As this is an alpha release, there might be bugs and instability in some of the features. 23 | 24 | ### New Contributors 25 | 26 | - @xDefyingGravity 27 | - @masih-developer 28 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@glandjs/core", 3 | "version": "1.0.3-beta", 4 | "description": "Glands is a web framework for Node.js (@core)", 5 | "author": "Mahdi ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/glandjs/gland#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/glandjs/gland.git", 11 | "directory": "packages/core" 12 | }, 13 | "engines": { 14 | "node": ">=20.0.0", 15 | "typescript": ">=5.0.0" 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "clean": "rm -rf dist *.d.ts *.js **/*.js **/*.d.ts */**/*.js */**/*.d.ts", 20 | "typecheck": "tsc --noEmit", 21 | "dev": "tsc --watch" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": ["gland", "glandjs", "core", "framework", "nodejs", "typescript", "dependency-injection", "event-driven", "event-bus", "event-broker", "architecture", "modular"], 27 | "dependencies": { 28 | "@glandjs/events": "latest", 29 | "@medishn/toolkit": "latest", 30 | "tslib": "2.8.1" 31 | }, 32 | "peerDependencies": { 33 | "@glandjs/common": "workspace:*", 34 | "reflect-metadata": "^0.2.2" 35 | }, 36 | "devDependencies": { 37 | "@glandjs/common": "workspace:*", 38 | "typescript": "^5.5.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/injector/graph.ts: -------------------------------------------------------------------------------- 1 | export interface DependencyNode { 2 | id: string; 3 | type: 'module' | 'controller' | 'channel'; 4 | metadata?: Record; 5 | instance?: T; 6 | parentId?: string; 7 | children: string[]; 8 | } 9 | 10 | export class DependencyGraph { 11 | private readonly nodes = new Map>(); 12 | 13 | public addNode(id: string, type: 'module' | 'controller' | 'channel', parentId?: string, metadata?: Record, instance?: T): DependencyNode { 14 | if (this.nodes.has(id)) { 15 | const existingNode = this.nodes.get(id)!; 16 | existingNode.type = type; 17 | existingNode.metadata = { ...existingNode.metadata, ...metadata }; 18 | 19 | if (instance !== undefined) { 20 | existingNode.instance = instance; 21 | } 22 | 23 | if (parentId && !existingNode.parentId) { 24 | existingNode.parentId = parentId; 25 | 26 | const parentNode = this.getNode(parentId); 27 | if (parentNode) { 28 | parentNode.children.push(id); 29 | } 30 | } 31 | 32 | return existingNode; 33 | } 34 | 35 | const node: DependencyNode = { 36 | id, 37 | type, 38 | metadata, 39 | instance, 40 | parentId, 41 | children: [], 42 | }; 43 | 44 | this.nodes.set(id, node); 45 | 46 | if (parentId) { 47 | const parentNode = this.getNode(parentId); 48 | if (parentNode) { 49 | parentNode.children.push(id); 50 | } 51 | } 52 | 53 | return node; 54 | } 55 | 56 | public getNode(id: string): DependencyNode | undefined { 57 | return this.nodes.get(id); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Run Tests 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [20.x, 21.x, 22.x] 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup PNPM 23 | uses: pnpm/action-setup@v3 24 | with: 25 | version: 9.0.6 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: 'pnpm' 32 | 33 | - name: Install dependencies 34 | run: pnpm install 35 | 36 | - name: Run Unit Tests 37 | run: pnpm test:unit 38 | 39 | - name: Upload test coverage 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: test-results-${{ matrix.node-version }} 43 | path: | 44 | coverage/ 45 | test-results/ 46 | retention-days: 5 47 | 48 | lint: 49 | name: Lint and Style Check 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | - name: Checkout code 54 | uses: actions/checkout@v4 55 | 56 | - name: Setup PNPM 57 | uses: pnpm/action-setup@v3 58 | with: 59 | version: 9.0.6 60 | 61 | - name: Use Node.js 20.x 62 | uses: actions/setup-node@v4 63 | with: 64 | node-version: 20.x 65 | cache: 'pnpm' 66 | 67 | - name: Install dependencies 68 | run: pnpm install 69 | 70 | - name: Run Prettier Check 71 | run: pnpm exec prettier --check . 72 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # PR Template 2 | 3 | ## Description 4 | 5 | Please provide a concise description of your changes and the motivation behind them. 6 | 7 | ## Type of Change 8 | 9 | 10 | 11 | Please check the option(s) that apply to this PR: 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] Feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature causing existing functionality to change) 16 | - [ ] Refactoring (no functional changes, no API changes) 17 | - [ ] Documentation update 18 | - [ ] Build or CI/CD related changes 19 | - [ ] Performance improvement 20 | - [ ] Other (please describe): 21 | 22 | ## Checklist 23 | 24 | 25 | 26 | - [ ] I have read the [CONTRIBUTING](CONTRIBUTING.md) guidelines 27 | - [ ] My code follows the event-driven architecture of Gland 28 | - [ ] I have added tests that prove my fix/feature works 29 | - [ ] New and existing unit tests pass locally with my changes 30 | - [ ] I have made necessary documentation updates 31 | - [ ] My changes generate no new warnings or errors 32 | - [ ] I have updated examples if applicable 33 | - [ ] My branch is up-to-date with the base branch 34 | 35 | **Breaking Changes?** 36 | 37 | - [ ] Yes (please describe the impact and migration path) 38 | - [ ] No 39 | 40 | **Documentation Impact** 41 | 42 | - [ ] Requires updates to event flow diagrams 43 | - [ ] New channel API docs needed 44 | - [ ] No changes required 45 | 46 | ## Current Behavior 47 | 48 | Describe the current behavior before this PR. If applicable, include issue numbers and links. 49 | 50 | ## New Behavior 51 | 52 | Describe the new behavior introduced by this PR. Explain how it improves the framework or fixes an issue. 53 | 54 | **Additional Information** 55 | 56 | 57 | -------------------------------------------------------------------------------- /packages/core/injector/discovery-service.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined, type Logger } from '@medishn/toolkit'; 2 | import type { ModulesContainer } from './container'; 3 | import { InstanceWrapper } from './instance-wrapper'; 4 | export class DiscoveryService { 5 | private readonly logger?: Logger; 6 | constructor( 7 | private readonly modulesContainer: ModulesContainer, 8 | logger?: Logger, 9 | ) { 10 | this.logger = logger?.child('Discovery'); 11 | } 12 | private getByMetadata(metadataKey: string, metadataValue: any, selector: (moduleRef: any) => Map, type: 'controller' | 'channel'): InstanceWrapper[] { 13 | const items: InstanceWrapper[] = []; 14 | 15 | for (const [moduleName, moduleRef] of this.modulesContainer.entries()) { 16 | const wrappers = selector(moduleRef); 17 | this.logger?.debug(`Scanning module: ${moduleName} for ${type}s`); 18 | for (const [token, wrapper] of wrappers.entries()) { 19 | const meta = Reflect.getMetadata(metadataKey, wrapper.token); 20 | if (isUndefined(meta)) { 21 | this.logger?.debug(` - Skipped ${type} (no ${metadataKey})`); 22 | continue; 23 | } 24 | if (!isUndefined(metadataValue) && meta !== metadataValue) { 25 | this.logger?.debug(` - Skipped ${type} (metadata mismatch: expected "${metadataValue}", got "${meta}")`); 26 | continue; 27 | } 28 | this.logger?.debug(` - Matched ${type} with metadata "${meta}"`); 29 | items.push(wrapper); 30 | } 31 | } 32 | this.logger?.debug(`Found ${items.length} ${type}(s) matching criteria.`); 33 | this.logger?.debug(`- Done.`); 34 | return items; 35 | } 36 | public getControllers(metadataKey: string, metadataValue?: any): InstanceWrapper[] { 37 | return this.getByMetadata(metadataKey, metadataValue, (m) => m.controllers, 'controller'); 38 | } 39 | 40 | public getChannels(metadataKey: string, metadataValue?: any): InstanceWrapper[] { 41 | return this.getByMetadata(metadataKey, metadataValue, (m) => m.channels, 'channel'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/application/application-initial.ts: -------------------------------------------------------------------------------- 1 | import { Constructor, Logger } from '@medishn/toolkit'; 2 | import { DependenciesScanner, Explorer } from '../injector'; 3 | import type { TGlandBroker } from '../types'; 4 | import { ApplicationBinder } from './application-binder'; 5 | import { ApplicationLifecycle } from './application-lifecycle'; 6 | 7 | export class ApplicationInitial { 8 | private readonly dependenciesScanner: DependenciesScanner; 9 | private lifecycle: ApplicationLifecycle; 10 | 11 | constructor( 12 | private broker: TGlandBroker, 13 | private logger: Logger, 14 | private mode: boolean, 15 | ) { 16 | this.dependenciesScanner = new DependenciesScanner(this.getLogger()); 17 | } 18 | getLogger(): Logger | undefined { 19 | return this.mode ? this.logger : undefined; 20 | } 21 | public async initialize(root: Constructor): Promise { 22 | const logger = this.logger.child('Initial'); 23 | try { 24 | logger.info('Scanning module dependencies'); 25 | await this.dependenciesScanner.scan(root); 26 | 27 | logger.info('Initializing dependency injector'); 28 | const explorer = new Explorer(this.dependenciesScanner.modules, this.getLogger()); 29 | 30 | this.lifecycle = new ApplicationLifecycle(this.dependenciesScanner.modules, this.getLogger()); 31 | 32 | this.logger.info('Running module initialization hooks'); 33 | await this.lifecycle.init(); 34 | 35 | logger.info('Binding application components'); 36 | const appBinder = new ApplicationBinder(explorer, this.broker, this.getLogger()); 37 | appBinder.bind(); 38 | 39 | this.logger.info('Running channel initialization hooks'); 40 | await this.lifecycle.initChannels(); 41 | 42 | this.logger.info('Bootstrapping application'); 43 | await this.lifecycle.bootstrap(); 44 | 45 | this.logger.info('Application initialized successfully'); 46 | } catch (error) { 47 | logger.error(`Application initialization failed: ${error.message}`); 48 | throw error; 49 | } 50 | } 51 | public async shutdown(signal?: string): Promise { 52 | if (this.lifecycle) { 53 | await this.lifecycle.shutdown(signal); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@glandjs/core", 3 | "version": "1.0.3-beta", 4 | "description": "Glands is a web framework for Node.js", 5 | "author": "Mahdi", 6 | "license": "MIT", 7 | "homepage": "https://github.com/glandjs/gland#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/glandjs/gland.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/glandjs/gland/issues" 14 | }, 15 | "keywords": ["gland", "glandjs", "framework", "nodejs", "di", "dependency-injection", "event-bus", "event-driven", "typescript", "core", "architecture", "modular"], 16 | "engines": { 17 | "node": ">=20.0.0", 18 | "typescript": ">=5.0.0" 19 | }, 20 | "collective": { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/gland", 23 | "donation": { 24 | "text": "Become a partner" 25 | } 26 | }, 27 | "workspaces": ["packages/*"], 28 | "files": ["packages/common", "packages/core"], 29 | "scripts": { 30 | "dev": "pnpm -r dev", 31 | "watch": "tsc --watch", 32 | "build": "pnpm -r build", 33 | "prebuild": "pnpm typecheck", 34 | "clean": "pnpm -r clean", 35 | "typecheck": "tsc --noEmit", 36 | "lint": "prettier --check .", 37 | "lint:fix": "prettier --write .", 38 | "lint-staged": "lint-staged", 39 | "test": "pnpm test:unit", 40 | "test:unit": "tsx ./node_modules/mocha/bin/mocha test/unit/**/*.spec.ts", 41 | "test:integration": "tsx ./node_modules/mocha/bin/mocha test/integration/**/*.spec.ts", 42 | "coverage": "nyc --reporter=text-summary pnpm test && nyc report --reporter=text-summary > coverage.md", 43 | "version": "changeset version", 44 | "prepare": "husky", 45 | "release": "pnpm build && pnpm changeset publish", 46 | "release:alpha": "pnpm build && pnpm changeset publish --tag alpha", 47 | "release:beta": "pnpm build && pnpm changeset publish --tag beta", 48 | "release:next": "pnpm build && changeset publish --tag next", 49 | "mode:dev": "", 50 | "mode:prod": "" 51 | }, 52 | "lint-staged": { 53 | "**/*.ts": "prettier --ignore-path ./.prettierignore --write" 54 | }, 55 | "devDependencies": { 56 | "@changesets/cli": "^2.28.1", 57 | "@commitlint/cli": "^19.8.0", 58 | "@commitlint/config-conventional": "^19.8.0", 59 | "@types/chai": "^4.3.19", 60 | "@types/mocha": "^10.0.7", 61 | "@types/node": "^22.0.0", 62 | "@types/sinon": "^17.0.3", 63 | "chai": "^4.3.7", 64 | "husky": "^9.1.7", 65 | "lint-staged": "^15.5.0", 66 | "mocha": "^10.7.3", 67 | "nyc": "^17.1.0", 68 | "prettier": "^3.5.3", 69 | "sinon": "^19.0.0", 70 | "tsx": "^4.19.4", 71 | "typescript": "^5.5.4" 72 | }, 73 | "dependencies": { 74 | "@glandjs/events": "latest", 75 | "@medishn/toolkit": "latest", 76 | "reflect-metadata": "^0.2.2", 77 | "tslib": "^2.8.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/common/utils/uuid.util.ts: -------------------------------------------------------------------------------- 1 | import { randomFillSync, randomUUID } from 'node:crypto'; 2 | 3 | export type UUID = string; 4 | type BufferLike = Uint8Array | Buffer; 5 | type UUIDOptions = { 6 | buffer?: BufferLike; 7 | offset?: number; 8 | random?: BufferLike; 9 | rng?: () => BufferLike; 10 | }; 11 | export class CryptoUUID { 12 | private static readonly VERSION = 0x40; 13 | private static readonly VARIANT = 0x80; 14 | private static readonly POOL_SIZE = 256; 15 | private static pool = new Uint8Array(this.POOL_SIZE); 16 | private static poolPtr = this.POOL_SIZE; 17 | 18 | static generate(options?: UUIDOptions): UUID; 19 | static generate(options: UUIDOptions, buffer: T): T; 20 | static generate(options?: UUIDOptions, buffer?: BufferLike): UUID | BufferLike { 21 | if (buffer && !options) { 22 | return randomUUID(); 23 | } 24 | 25 | const rnds = this.getEntropy(options); 26 | 27 | rnds[6] = (rnds[6] & 0x0f) | this.VERSION; 28 | rnds[8] = (rnds[8] & 0x3f) | this.VARIANT; 29 | 30 | if (buffer) { 31 | const offset = options?.offset || 0; 32 | this.validateBuffer(buffer, offset); 33 | buffer.set(rnds, offset); 34 | return buffer; 35 | } 36 | 37 | return this.format(rnds); 38 | } 39 | 40 | /** Validate UUID structure and version/variant bits */ 41 | static validate(uuid: string | BufferLike): boolean { 42 | if (typeof uuid === 'string') { 43 | return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid); 44 | } 45 | 46 | return this.isValidBuffer(uuid); 47 | } 48 | 49 | private static getEntropy(options: UUIDOptions = {}): BufferLike { 50 | if (options.random?.length! >= 16) return options.random!; 51 | if (options.rng) return options.rng(); 52 | 53 | if (this.poolPtr > this.POOL_SIZE - 16) { 54 | randomFillSync(this.pool); 55 | this.poolPtr = 0; 56 | } 57 | 58 | return this.pool.slice(this.poolPtr, (this.poolPtr += 16)); 59 | } 60 | 61 | private static format(bytes: BufferLike): UUID { 62 | return [ 63 | this.hex(bytes, 0, 4), // time_low 64 | this.hex(bytes, 4, 6), // time_mid 65 | this.hex(bytes, 6, 8), // time_hi_and_version 66 | this.hex(bytes, 8, 10), // clock_seq_hi_and_reserved + clock_seq_low 67 | this.hex(bytes, 10, 16), // node 68 | ].join('-'); 69 | } 70 | 71 | private static hex(bytes: BufferLike, start: number, end: number): string { 72 | return Buffer.from(bytes.slice(start, end) as Uint8Array).toString('hex'); 73 | } 74 | 75 | private static validateBuffer(buffer: BufferLike, offset: number): void { 76 | if (offset < 0 || offset + 16 > buffer.length) { 77 | throw new RangeError(`Invalid buffer range: ${offset} to ${offset + 16}`); 78 | } 79 | } 80 | 81 | private static isValidBuffer(buffer: BufferLike): boolean { 82 | return buffer[6] === (this.VERSION & 0x0f) && (buffer[8] & 0xc0) === this.VARIANT; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/core/application/application-lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@medishn/toolkit'; 2 | import { ModulesContainer } from '../injector/container/module-container'; 3 | import { LifecycleScanner } from '../hooks'; 4 | 5 | export class ApplicationLifecycle { 6 | private readonly lifecycleScanner: LifecycleScanner; 7 | private isBootstrapped = false; 8 | private isShuttingDown = false; 9 | private shutdownSignals = ['SIGTERM', 'SIGINT', 'SIGHUP']; 10 | private readonly logger?: Logger; 11 | constructor(modulesContainer: ModulesContainer, logger?: Logger) { 12 | this.logger = logger?.child('ApplicationLifecycle'); 13 | this.lifecycleScanner = new LifecycleScanner(modulesContainer, logger); 14 | this.setupShutdownHooks(); 15 | } 16 | 17 | private setupShutdownHooks(): void { 18 | if (typeof process !== 'undefined' && process) { 19 | for (const signal of this.shutdownSignals) { 20 | process.on(signal, async () => { 21 | await this.shutdown(signal); 22 | process.exit(0); 23 | }); 24 | } 25 | 26 | process.on('uncaughtException', async (error) => { 27 | this.logger?.error(`Uncaught exception: ${error.message}`); 28 | this.logger?.error(error.stack); 29 | await this.shutdown('uncaughtException'); 30 | process.exit(1); 31 | }); 32 | 33 | process.on('unhandledRejection', async (reason) => { 34 | this.logger?.error(`Unhandled rejection: ${reason}`); 35 | await this.shutdown('unhandledRejection'); 36 | process.exit(1); 37 | }); 38 | } 39 | } 40 | 41 | public async init(): Promise { 42 | this.logger?.info('Initializing modules'); 43 | this.lifecycleScanner.scanForHooks(); 44 | await this.lifecycleScanner.onModuleInit(); 45 | this.logger?.info('Modules initialized'); 46 | } 47 | public async initChannels(): Promise { 48 | this.logger?.info('Initializing channels'); 49 | await this.lifecycleScanner.onChannelInit(); 50 | this.logger?.info('Channels initialized'); 51 | } 52 | 53 | public async bootstrap(): Promise { 54 | if (this.isBootstrapped) { 55 | return; 56 | } 57 | 58 | this.logger?.info('Bootstrapping application'); 59 | await this.lifecycleScanner.onAppBootstrap(); 60 | this.isBootstrapped = true; 61 | this.logger?.info('Application bootstrapped successfully'); 62 | } 63 | 64 | public async shutdown(signal?: string): Promise { 65 | if (this.isShuttingDown) { 66 | return; 67 | } 68 | 69 | this.isShuttingDown = true; 70 | this.logger?.info(`Shutting down application${signal ? ` (signal: ${signal})` : ''}`); 71 | 72 | try { 73 | await this.lifecycleScanner.onAppShutdown(signal); 74 | await this.lifecycleScanner.onModuleDestroy(); 75 | this.logger?.info('Application shutdown complete'); 76 | } catch (error) { 77 | this.logger?.error(`Error during shutdown: ${error.message}`); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/application/application-binder.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from '@medishn/toolkit'; 2 | import type { Explorer } from '../injector'; 3 | import type { TGlandBroker } from '../types'; 4 | 5 | export class ApplicationBinder { 6 | private readonly logger?: Logger; 7 | private readonly map: Map< 8 | string, 9 | { 10 | namespace: string; 11 | event: string; 12 | fullEvent: string; 13 | } 14 | > = new Map(); 15 | constructor( 16 | private explorer: Explorer, 17 | private broker: TGlandBroker, 18 | logger?: Logger, 19 | ) { 20 | this.logger = logger?.child('Binder'); 21 | } 22 | bind(): void { 23 | this.logger?.debug('Starting controller/channel binding...'); 24 | this.bindChannel(); 25 | this.bindControllers(); 26 | this.logger?.info('- Done.'); 27 | } 28 | bindChannel() { 29 | const channels = this.explorer.exploreChannels(); 30 | for (const channel of channels) { 31 | const { instance, event, namespace, target } = channel; 32 | const fullEvent = `gland:define:channel:${namespace}:${event}` as `gland:define:channel:${string}:${string}`; 33 | 34 | this.broker.on(fullEvent, async (...args: any[]) => { 35 | this.logger?.debug(`[Broker] Invoking channel handler [${namespace}:${event}]`); 36 | return await target.apply(instance, args); 37 | }); 38 | this.map.set(fullEvent, { 39 | namespace, 40 | event, 41 | fullEvent, 42 | }); 43 | this.logger?.info(`Channel bound: namespace="${namespace}", event="${event}"`); 44 | } 45 | } 46 | public bindControllers(): void { 47 | const controllers = this.explorer.exploreControllers(); 48 | for (const controller of controllers) { 49 | const { method, route, controller: ctr } = controller; 50 | const fullPath = this.combinePaths(ctr.path, route); 51 | this.logger?.debug(`Binding route [${method.toUpperCase()} ${fullPath}] to method "${ctr.methodName}"`); 52 | 53 | this.broker.broadcast('gland:define:route', { 54 | path: route, 55 | meta: { 56 | method: ctr.methodName, 57 | path: fullPath, 58 | }, 59 | method, 60 | action: (ctx) => { 61 | this.logger?.debug(`[Broker] Executing action for ${method.toUpperCase()} ${fullPath}`); 62 | ctx.state = ctx._state || {}; 63 | ctx.state.brokerId = this.broker.id; 64 | ctx.state.channel = Array.from(this.map.values()); 65 | return ctr.target(ctx); 66 | }, 67 | }); 68 | this.logger?.info(`Controller route bound: [${method.toUpperCase()}] ${fullPath}`); 69 | } 70 | } 71 | private combinePaths(basePath: string, methodPath: string): string { 72 | basePath = basePath.startsWith('/') ? basePath : `/${basePath}`; 73 | basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; 74 | methodPath = methodPath.startsWith('/') ? methodPath : `/${methodPath}`; 75 | return `${basePath}${methodPath}`; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: '🚀 Feature Request' 2 | description: 'Suggest an idea or enhancement for Gland' 3 | labels: ['enhancement', 'needs triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## 💡 Feature Request 9 | 10 | Thank you for suggesting an enhancement for Gland! We value your input in making our event-driven framework even better. 11 | 12 | Before submitting your feature request, please: 13 | - Check existing issues to see if your idea has already been proposed 14 | - Consider if your idea aligns with Gland's event-driven architecture and philosophy 15 | 16 | For general questions, please use: 17 | - [Stack Overflow](https://stackoverflow.com/questions/tagged/gland) with the `gland` tag 18 | - Community channels (coming soon) 19 | 20 | --- 21 | 22 | - type: checkboxes 23 | attributes: 24 | label: 'Prerequisites' 25 | description: "Please confirm you've completed the following" 26 | options: 27 | - label: 'I have searched for similar feature requests' 28 | required: true 29 | - label: "I have read the documentation to confirm this feature doesn't already exist" 30 | required: true 31 | 32 | - type: textarea 33 | validations: 34 | required: true 35 | attributes: 36 | label: 'Problem Statement' 37 | description: 'Is your feature request related to a problem? Please describe.' 38 | placeholder: "I'm trying to accomplish X, but there's no way to do it because..." 39 | 40 | - type: textarea 41 | validations: 42 | required: true 43 | attributes: 44 | label: 'Proposed Solution' 45 | description: "Describe the solution you'd like to see implemented" 46 | placeholder: 'I would like Gland to...' 47 | 48 | - type: textarea 49 | validations: 50 | required: true 51 | attributes: 52 | label: 'Use Cases' 53 | description: 'Describe specific use cases this feature would enable' 54 | placeholder: 'This feature would allow developers to...' 55 | 56 | - type: textarea 57 | attributes: 58 | label: 'Alternatives Considered' 59 | description: "Describe any alternative solutions or features you've considered" 60 | placeholder: "I've tried working around this by..." 61 | 62 | - type: textarea 63 | attributes: 64 | label: 'Example Implementation' 65 | description: 'If you can, provide a conceptual example of how you envision this feature working' 66 | value: | 67 | ```typescript 68 | // Conceptual example 69 | ``` 70 | 71 | - type: textarea 72 | attributes: 73 | label: 'Additional Context' 74 | description: 'Add any other context or screenshots about the feature request here' 75 | 76 | - type: dropdown 77 | attributes: 78 | label: 'Impact' 79 | description: 'How would you categorize the impact of this feature?' 80 | options: 81 | - 'Critical functionality (blocking my usage of Gland)' 82 | - 'Major improvement (would significantly enhance my use of Gland)' 83 | - 'Nice to have (would be convenient but not essential)' 84 | - 'Just an idea (throwing it out there for consideration)' 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug Report' 2 | description: 'Report an issue or unexpected behavior in Gland' 3 | labels: ['bug', 'needs triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## 🚨 Before Submitting a Bug Report 9 | 10 | Thank you for taking the time to file a bug report! Please help us address the issue efficiently by: 11 | 12 | - Checking if a similar issue already exists in the [issue tracker](../issues) 13 | - Making sure your Gland and Node.js versions are up-to-date 14 | - Providing as much detail as possible about the problem 15 | 16 | For general questions about using Gland, please use: 17 | - [Stack Overflow](https://stackoverflow.com/questions/tagged/gland) with the `gland` tag 18 | - Join our community discussions (coming soon) 19 | 20 | --- 21 | 22 | - type: checkboxes 23 | attributes: 24 | label: 'Prerequisites' 25 | description: "Please confirm you've completed the following steps before submitting your issue" 26 | options: 27 | - label: "I have searched for similar issues and couldn't find a solution" 28 | required: true 29 | - label: 'I have tried to reproduce this issue with the latest version of Gland' 30 | required: true 31 | - label: 'I have checked the documentation and found no answer' 32 | required: true 33 | 34 | - type: textarea 35 | validations: 36 | required: true 37 | attributes: 38 | label: 'Current Behavior' 39 | description: 'A clear description of what actually happens' 40 | placeholder: 'When I perform X action, the framework does Y instead of Z...' 41 | 42 | - type: textarea 43 | validations: 44 | required: true 45 | attributes: 46 | label: 'Expected Behavior' 47 | description: 'A clear description of what you expected to happen' 48 | placeholder: 'I expected the framework to...' 49 | 50 | - type: input 51 | attributes: 52 | label: 'Reproduction Link' 53 | description: 'A link to a minimal repository that reproduces the issue' 54 | placeholder: 'https://github.com/username/repo' 55 | 56 | - type: textarea 57 | attributes: 58 | label: 'Steps to Reproduce' 59 | description: 'Step-by-step instructions to reproduce the behavior' 60 | placeholder: | 61 | 1. Initialize Gland with config '...' 62 | 2. Set up event '...' 63 | 3. Trigger event '...' 64 | 4. See error '...' 65 | 66 | - type: textarea 67 | attributes: 68 | label: 'Minimal Code Example' 69 | description: 'If you cannot create a reproduction repository, please provide the smallest code sample that demonstrates the issue' 70 | value: | 71 | ```typescript 72 | // Your code here 73 | ``` 74 | 75 | - type: dropdown 76 | attributes: 77 | label: 'Which component of Gland is affected?' 78 | multiple: true 79 | options: 80 | - 'Core' 81 | - 'Event System' 82 | - 'Channel' 83 | - 'Middleware' 84 | - 'Configuration' 85 | - 'Utilities' 86 | - 'Documentation' 87 | - 'Not sure/Other' 88 | 89 | - type: textarea 90 | attributes: 91 | label: 'Additional Context' 92 | description: 'Add any other context about the problem here (logs, screenshots, etc.)' 93 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | - name: Bug 3 | color: 'd73a4a' 4 | description: "Something isn't working" 5 | - name: Enhancement 6 | color: 'a2eeef' 7 | description: 'New feature or request' 8 | - name: Documentation 9 | color: '0075ca' 10 | description: 'Improvements or additions to documentation' 11 | - name: Question 12 | color: 'd876e3' 13 | description: 'Further information requested' 14 | - name: Discussion 15 | color: 'cfd3d7' 16 | description: 'Open-ended discussion' 17 | 18 | - name: area:core 19 | color: '5319e7' 20 | description: 'Issues in @glandjs/core package' 21 | - name: area:common 22 | color: '0e8a16' 23 | description: 'Issues in @glandjs/common package' 24 | - name: area:events 25 | color: 'd4c5f9' 26 | description: 'Issues in @glandjs/events package' 27 | - name: area:http 28 | color: '1d76db' 29 | description: 'Issues in @glandjs/http package' 30 | - name: area:emitter 31 | color: 'f9d0c4' 32 | description: 'Issues in @glandjs/emitter package' 33 | - name: area:docs 34 | color: '006b75' 35 | description: 'Documentation and guides' 36 | 37 | - name: P0 38 | color: 'b60205' 39 | description: 'Critical: immediate attention required' 40 | - name: P1 41 | color: 'd93f0b' 42 | description: 'High: important but not blocking' 43 | - name: P2 44 | color: 'f9d0c4' 45 | description: 'Medium: non-critical enhancements or bugs' 46 | - name: P3 47 | color: '0e8a16' 48 | description: 'Low: minor improvements or tasks' 49 | 50 | - name: status:triaged 51 | color: '0e8a16' 52 | description: 'Issue has been reviewed and prioritized' 53 | - name: status:in progress 54 | color: 'fbca04' 55 | description: 'Work is underway' 56 | - name: status:blocked 57 | color: 'e99695' 58 | description: 'Work is blocked by external factors' 59 | - name: status:review needed 60 | color: '0052cc' 61 | description: 'Ready for review' 62 | - name: status:done 63 | color: '1d76db' 64 | description: 'Completed and merged' 65 | 66 | - name: good first issue 67 | color: '7057ff' 68 | description: 'Good for newcomers' 69 | - name: help wanted 70 | color: '008672' 71 | description: 'Extra attention is needed' 72 | 73 | - name: CI 74 | color: '006b75' 75 | description: 'Automation and CI failures' 76 | - name: dependencies 77 | color: '2f6f44' 78 | description: 'Dependency update or vulnerability fix' 79 | 80 | - name: duplicate 81 | color: 'cccccc' 82 | description: 'This issue or PR already exists' 83 | - name: wontfix 84 | color: 'ffffff' 85 | description: 'This will not be worked on' 86 | - name: invalid 87 | color: 'e4e669' 88 | description: "This doesn't seem right" 89 | 90 | - name: architecture 91 | color: 'c5def5' 92 | description: 'Proposals around core architecture/design' 93 | - name: RFC 94 | color: '5319e7' 95 | description: 'Request for Comments (significant changes)' 96 | 97 | - name: performance 98 | color: 'd876e3' 99 | description: 'Performance improvements needed' 100 | - name: security 101 | color: 'b60205' 102 | description: 'Vulnerability or security issue' 103 | 104 | - name: release 105 | color: '0e8a16' 106 | description: 'Issues or PRs to include in next release' 107 | - name: roadmap 108 | color: '5319e7' 109 | description: 'Feature on the project roadmap' 110 | -------------------------------------------------------------------------------- /packages/core/context/context.ts: -------------------------------------------------------------------------------- 1 | import type { Broker, CallMethod, EmitMethod, EventOptions, EventPayload, EventRecord, EventReturn, Events, EventType, Listener, OffMethod, OnceMethod, OnMethod } from '@glandjs/events'; 2 | import { Dictionary } from '@medishn/toolkit'; 3 | 4 | export class Context implements CallMethod, OnMethod, EmitMethod, OnceMethod, OffMethod { 5 | private _state: Dictionary = {}; 6 | public error?: any; 7 | constructor(protected broker: Broker) {} 8 | get state() { 9 | return this._state; 10 | } 11 | 12 | set state(data: Dictionary) { 13 | this._state = Object.assign(this._state, data); 14 | } 15 | 16 | public emit>(event: K, payload: EventPayload, options?: EventOptions): this { 17 | const channels = this.state?.channel || [{}]; 18 | 19 | const brokerId = this.state?.brokerId; 20 | const listener = event?.includes(':') ? event?.split(/:(.+)/) : event; 21 | for (const channel of channels) { 22 | if (channel.event === listener[1]) { 23 | this.broker.emitTo(brokerId, channel.fullEvent, payload, options); 24 | return this; 25 | } 26 | } 27 | return this; 28 | } 29 | public on>(event: K, listener: Listener, void>, options?: EventOptions): this; 30 | public on>(event: K, listener: null | Listener, void>, options: EventOptions & { watch: true }): Promise>; 31 | public on>(event: K, listener: Listener, void> | null, options?: EventOptions & { watch?: boolean }): this | Promise> { 32 | // @ts-ignore 33 | const result = this.broker.on(event, listener, options); 34 | if (result instanceof Promise) { 35 | return result; 36 | } 37 | return this; 38 | } 39 | public off>(event: K, listener?: Listener, void>): this { 40 | this.broker.off(event, listener); 41 | return this; 42 | } 43 | 44 | public once>(event: K, listener: Listener, void>): this; 45 | public once>(event: K, listener: Listener, void> | null, options: EventOptions & { watch: true }): Promise>; 46 | public once>(event: K, listener: Listener, void> | null, options?: EventOptions & { watch?: boolean }): this | Promise> { 47 | // @ts-ignore 48 | const result = this.broker.once(event, listener, options); 49 | if (result instanceof Promise) { 50 | return result; 51 | } 52 | return this; 53 | } 54 | 55 | // @ts-ignore 56 | public call>(event: K, data: EventPayload): EventReturn; 57 | public call>(event: K, data: EventPayload, strategy: 'all'): EventReturn[]; 58 | public call>(event: K, data: EventPayload, strategy?: 'all') { 59 | const channels = this.state?.channel || [{}]; 60 | const brokerId = this.state?.brokerId; 61 | const listener = event?.includes(':') ? event?.split(/:(.+)/) : event; 62 | for (const channel of channels) { 63 | if (channel.event === listener[1]) { 64 | return this.broker.callTo(brokerId, channel.fullEvent, data, strategy!); 65 | } 66 | } 67 | return strategy === 'all' ? [] : (undefined as any); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

2 | Gland Logo 3 |

4 | 5 |

6 | NPM Version 7 | Package License 8 | NPM Downloads 9 |

10 | 11 |

Gland

12 | 13 |

A progressive, event-driven Node.js framework for building efficient and scalable server-side applications.

14 | 15 | ## Description 16 | 17 | **Gland** is a lightweight, extensible web framework built for modern JavaScript and TypeScript applications. With its unique event-driven architecture (EDS), it offers unparalleled flexibility in creating modular, scalable server-side applications. 18 | 19 | Inspired by frameworks like Angular and NestJS, Gland integrates an object-oriented design pattern, minimalistic dependency injection (DI), and powerful event-driven communication, allowing developers to efficiently build and maintain complex applications. 20 | 21 | ## Philosophy 22 | 23 | Rather than relying on predefined conventions or imposing rigid structures, Gland offers an approach where the developer can focus on the core problem domain without being hindered by unnecessary constraints. By using an event-driven approach, Gland ensures that communication between components remains straightforward and flexible, while also maintaining the ability to easily extend the system as requirements evolve. 24 | 25 | The simplicity of Gland lies not in the absence of features, but in how it allows developers to shape their applications with minimal friction and clear intentions. It strives to be a framework that adapts to the developer's needs, not the other way around. Through this approach, Gland provides the foundation for building applications that are both effective and maintainable, without forcing an unnatural design pattern upon the developer. 26 | 27 | ## Why Gland? 28 | 29 | Gland is designed with flexibility and scalability in mind. Whether you're building small APIs or large-scale applications, Gland provides the tools to help you structure your codebase efficiently and maintainably. Its event-driven approach helps in decoupling components and improving testability, while its object-oriented philosophy ensures clear and consistent code organization. 30 | 31 | ## Documentation 32 | 33 | For full documentation on how to use Gland, including guides, examples, and API references, check out the following resources: 34 | 35 | - [Official Documentation](#) 36 | - [API Reference](#/api) 37 | - [Contributing Guide](https://github.com/glandjs/gland/blob/main/docs/CONTRIBUTING.md) 38 | 39 | ## Contributing 40 | 41 | We welcome contributions to help improve Gland and shape it into a robust, production-ready framework. Here's how you can get involved: 42 | 43 | 1. Fork the repository. 44 | 2. Create a new branch for your feature or bug fix. 45 | 3. Write tests to cover your changes. 46 | 4. Submit a pull request with a detailed description of your changes. 47 | 48 | Please review our [CONTRIBUTING.md](https://github.com/glandjs/gland/blob/main/docs/CONTRIBUTING.md) guidelines before starting. 49 | 50 | ## Security 51 | 52 | For details on our security practices and how to report vulnerabilities, please visit [SECURITY.md](https://github.com/glandjs/gland/blob/main/docs/SECURITY). 53 | 54 | ## License 55 | 56 | Gland is licensed under the MIT License. See the [LICENSE](https://github.com/glandjs/gland/blob/main/LICENSE) file for details. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Gland Logo 3 |

4 | 5 |

6 | NPM Version 7 | Package License 8 | NPM Downloads 9 |

10 | 11 |

Gland

12 | 13 |

A progressive, event-driven Node.js framework for building efficient and scalable server-side applications.

14 | 15 | ## Description 16 | 17 | > What if every interaction was an event? Welcome to Gland. 18 | 19 | **Gland** is a lightweight, extensible web framework built for modern JavaScript and TypeScript applications. With its unique event-driven architecture (EDS), it offers unparalleled flexibility in creating modular, scalable server-side applications. 20 | 21 | Inspired by frameworks like Angular and NestJS, Gland integrates an object-oriented design pattern, minimalistic dependency injection (DI), and powerful event-driven communication, allowing developers to efficiently build and maintain complex applications. 22 | 23 | ## Philosophy 24 | 25 | Rather than relying on predefined conventions or imposing rigid structures, Gland offers an approach where the developer can focus on the core problem domain without being hindered by unnecessary constraints. By using an event-driven approach, Gland ensures that communication between components remains straightforward and flexible, while also maintaining the ability to easily extend the system as requirements evolve. 26 | 27 | The simplicity of Gland lies not in the absence of features, but in how it allows developers to shape their applications with minimal friction and clear intentions. It strives to be a framework that adapts to the developer's needs, not the other way around. Through this approach, Gland provides the foundation for building applications that are both effective and maintainable, without forcing an unnatural design pattern upon the developer. 28 | 29 | ## Why Gland? 30 | 31 | Gland is designed with flexibility and scalability in mind. Whether you're building small APIs or large-scale applications, Gland provides the tools to help you structure your codebase efficiently and maintainably. Its event-driven approach helps in decoupling components and improving testability, while its object-oriented philosophy ensures clear and consistent code organization. 32 | 33 | ## Documentation 34 | 35 | For full documentation on how to use Gland, including guides, examples, and API references, check out the following resources: 36 | 37 | - [Official Documentation](#) 38 | - [API Reference](#/api) 39 | - [Contributing Guide](./docs/CONTRIBUTING.md) 40 | 41 | ## Contributing 42 | 43 | We welcome contributions to help improve Gland and shape it into a robust, production-ready framework. Here's how you can get involved: 44 | 45 | 1. Fork the repository. 46 | 2. Create a new branch for your feature or bug fix. 47 | 3. Write tests to cover your changes. 48 | 4. Submit a pull request with a detailed description of your changes. 49 | 50 | Please review our [CONTRIBUTING.md](./docs/CONTRIBUTING.md) guidelines before starting. 51 | 52 | ## Security 53 | 54 | For details on our security practices and how to report vulnerabilities, please visit [SECURITY.md](./docs/SECURITY). 55 | 56 | ## License 57 | 58 | Gland is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 59 | --- 60 | 61 | Gland doesn’t tell you how to build. 62 | It asks: _what if everything was just a message?_ 63 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 |

2 | Gland Logo 3 |

4 | 5 |

6 | NPM Version 7 | Package License 8 | NPM Downloads 9 |

10 | 11 |

Gland

12 | 13 |

A progressive, event-driven Node.js framework for building efficient and scalable server-side applications.

14 | 15 | ## Description 16 | 17 | **Gland** is a lightweight, extensible web framework built for modern JavaScript and TypeScript applications. With its unique event-driven architecture (EDS), it offers unparalleled flexibility in creating modular, scalable server-side applications. 18 | 19 | Inspired by frameworks like Angular and NestJS, Gland integrates an object-oriented design pattern, minimalistic dependency injection (DI), and powerful event-driven communication, allowing developers to efficiently build and maintain complex applications. 20 | 21 | ## Philosophy 22 | 23 | Rather than relying on predefined conventions or imposing rigid structures, Gland offers an approach where the developer can focus on the core problem domain without being hindered by unnecessary constraints. By using an event-driven approach, Gland ensures that communication between components remains straightforward and flexible, while also maintaining the ability to easily extend the system as requirements evolve. 24 | 25 | The simplicity of Gland lies not in the absence of features, but in how it allows developers to shape their applications with minimal friction and clear intentions. It strives to be a framework that adapts to the developer's needs, not the other way around. Through this approach, Gland provides the foundation for building applications that are both effective and maintainable, without forcing an unnatural design pattern upon the developer. 26 | 27 | ## Why Gland? 28 | 29 | Gland is designed with flexibility and scalability in mind. Whether you're building small APIs or large-scale applications, Gland provides the tools to help you structure your codebase efficiently and maintainably. Its event-driven approach helps in decoupling components and improving testability, while its object-oriented philosophy ensures clear and consistent code organization. 30 | 31 | ## Documentation 32 | 33 | For full documentation on how to use Gland, including guides, examples, and API references, check out the following resources: 34 | 35 | - [Official Documentation](#) 36 | - [API Reference](#/api) 37 | - [Contributing Guide](https://github.com/glandjs/gland/blob/main/docs/CONTRIBUTING.md) 38 | 39 | ## Contributing 40 | 41 | We welcome contributions to help improve Gland and shape it into a robust, production-ready framework. Here's how you can get involved: 42 | 43 | 1. Fork the repository. 44 | 2. Create a new branch for your feature or bug fix. 45 | 3. Write tests to cover your changes. 46 | 4. Submit a pull request with a detailed description of your changes. 47 | 48 | Please review our [CONTRIBUTING.md](https://github.com/glandjs/gland/blob/main/docs/CONTRIBUTING.md) guidelines before starting. 49 | 50 | ## Security 51 | 52 | For details on our security practices and how to report vulnerabilities, please visit [SECURITY.md](https://github.com/glandjs/gland/blob/main/docs/SECURITY). 53 | 54 | ## License 55 | 56 | Gland is licensed under the MIT License. See the [LICENSE](https://github.com/glandjs/gland/blob/main/LICENSE) file for details. 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/regression.yml: -------------------------------------------------------------------------------- 1 | name: '💥 Regression' 2 | description: 'Report functionality that worked in a previous version but is now broken' 3 | labels: ['regression', 'needs triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## 🔄 Regression Report 9 | 10 | Please use this template to report functionality that worked correctly in a previous version of Gland but is now broken or behaving differently. 11 | 12 | Detailed information about when and how the behavior changed will help us identify and fix the regression quickly. 13 | 14 | For general questions, please use: 15 | - [Stack Overflow](https://stackoverflow.com/questions/tagged/gland) with the `gland` tag 16 | - Community channels (coming soon) 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: 'Prerequisites' 23 | description: 'Please confirm the following before submitting your regression report' 24 | options: 25 | - label: 'I have searched existing issues for similar reports' 26 | required: true 27 | - label: 'I have verified the issue occurs consistently in the new version but not in the previous version' 28 | required: true 29 | 30 | - type: input 31 | validations: 32 | required: true 33 | attributes: 34 | label: 'Version Information' 35 | description: 'Which versions are you comparing?' 36 | placeholder: 'Previously working: v1.2.0 → Now broken: v1.3.0' 37 | 38 | - type: textarea 39 | validations: 40 | required: true 41 | attributes: 42 | label: 'Regression Description' 43 | description: 'A clear description of what functionality changed between versions' 44 | placeholder: 'In the previous version, X would happen when I did Y. Now, Z happens instead.' 45 | 46 | - type: textarea 47 | validations: 48 | required: true 49 | attributes: 50 | label: 'Previous Behavior' 51 | description: 'How did it work in the previous version?' 52 | 53 | - type: textarea 54 | validations: 55 | required: true 56 | attributes: 57 | label: 'Current Behavior' 58 | description: 'How does it work (or not work) in the current version?' 59 | 60 | - type: textarea 61 | attributes: 62 | label: 'Steps to Reproduce' 63 | description: 'Provide steps to reproduce the regression' 64 | placeholder: | 65 | 1. Set up Gland with configuration X 66 | 2. Register event handler for Y 67 | 3. Dispatch event Z 68 | 4. Observe the incorrect behavior 69 | 70 | - type: input 71 | attributes: 72 | label: 'Reproduction Link' 73 | description: 'Link to a minimal repository that demonstrates the regression' 74 | placeholder: 'https://github.com/username/repo' 75 | 76 | - type: textarea 77 | attributes: 78 | label: 'Minimal Code Example' 79 | description: 'If you cannot create a reproduction repository, please provide the smallest code sample that demonstrates the issue' 80 | value: | 81 | ```typescript 82 | // Code that works in previous version but fails in current version 83 | ``` 84 | 85 | - type: input 86 | attributes: 87 | label: 'Potential Cause' 88 | description: 'If you have any insights into what changed to cause this regression, please share' 89 | placeholder: 'Possibly related to PR #123 or the changes to X feature' 90 | 91 | - type: textarea 92 | validations: 93 | required: true 94 | attributes: 95 | label: 'Environment' 96 | description: 'Please provide your environment details' 97 | value: | 98 | - Gland Version (previous): 99 | - Gland Version (current): 100 | 101 | - type: textarea 102 | attributes: 103 | label: 'Additional Context' 104 | description: 'Add any other context about the regression here (logs, screenshots, etc.)' 105 | -------------------------------------------------------------------------------- /packages/core/injector/explorer.ts: -------------------------------------------------------------------------------- 1 | import { METHOD_METADATA, PATH_METADATA } from '@glandjs/common'; 2 | import { DiscoveryService } from './discovery-service'; 3 | import { MetadataScanner } from './scanner'; 4 | import type { ModulesContainer } from './container'; 5 | import type { Constructor, Logger } from '@medishn/toolkit'; 6 | export interface RouteMetadata { 7 | method: string; 8 | route: string; 9 | controller: { 10 | path: string; 11 | instance: T; 12 | methodName: string; 13 | target: Function; 14 | }; 15 | } 16 | 17 | export interface ChannelMetadata { 18 | instance: T; 19 | token: Constructor; 20 | event: string; 21 | namespace: string; 22 | target: Function; 23 | } 24 | export class Explorer { 25 | private readonly metadataScanner: MetadataScanner; 26 | private readonly discoveryService: DiscoveryService; 27 | private readonly logger?: Logger; 28 | constructor(modulesContainer: ModulesContainer, logger?: Logger) { 29 | this.logger = logger?.child('Explorer'); 30 | this.metadataScanner = new MetadataScanner(); 31 | this.discoveryService = new DiscoveryService(modulesContainer, logger); 32 | } 33 | 34 | public exploreControllers(): RouteMetadata[] { 35 | this.logger?.debug('Starting controller exploration...'); 36 | 37 | const controllers = this.discoveryService.getControllers(PATH_METADATA); 38 | const result: RouteMetadata[] = []; 39 | 40 | for (const wrapper of controllers) { 41 | const instance = wrapper.getInstance(); 42 | 43 | const controllerType = wrapper.token; 44 | const prototype = Object.getPrototypeOf(instance); 45 | const controllerPath = Reflect.getMetadata(PATH_METADATA, controllerType) || ''; 46 | this.logger?.debug(`Scanning controller: ${controllerType.name}, path: "${controllerPath}"`); 47 | 48 | this.metadataScanner.scanFromPrototype(prototype, (methodName) => { 49 | const target = prototype[methodName]; 50 | const method = Reflect.getMetadata(METHOD_METADATA, target); // http method(GET,POST,...etc) 51 | const route = Reflect.getMetadata(PATH_METADATA, target) || '/'; 52 | if (method) { 53 | this.logger?.debug(` - Found route handler: ${method.toUpperCase()} ${controllerPath}${route} -> ${methodName}`); 54 | result.push({ 55 | controller: { 56 | instance, 57 | target, 58 | methodName, 59 | path: controllerPath, 60 | }, 61 | method, 62 | route, 63 | }); 64 | } 65 | }); 66 | } 67 | 68 | this.logger?.debug(`Completed controller exploration. Total: ${result.length}`); 69 | this.logger?.debug(`- Done.`); 70 | return result; 71 | } 72 | public exploreChannels(): ChannelMetadata[] { 73 | this.logger?.debug('Starting channel exploration...'); 74 | 75 | const channels = this.discoveryService.getChannels(PATH_METADATA); 76 | 77 | const result: ChannelMetadata[] = []; 78 | 79 | for (const wrapper of channels) { 80 | const instance = wrapper.getInstance(); 81 | const metatype = wrapper.token; 82 | const prototype = Object.getPrototypeOf(instance); 83 | const channelName = Reflect.getMetadata(PATH_METADATA, metatype); 84 | 85 | this.logger?.debug(`Scanning channel: ${metatype.name}, namespace: "${channelName}"`); 86 | this.metadataScanner.scanFromPrototype(prototype, (method) => { 87 | const target = prototype[method]; 88 | 89 | const methodMetadata = Reflect.getMetadata(METHOD_METADATA, prototype, method); 90 | 91 | if (methodMetadata) { 92 | this.logger?.debug(` - Found event handler: ${channelName}.${methodMetadata} -> ${method}`); 93 | result.push({ 94 | instance, 95 | token: metatype, 96 | event: methodMetadata, 97 | namespace: channelName, 98 | target, 99 | }); 100 | } 101 | }); 102 | } 103 | this.logger?.debug(`Completed channel exploration. Total: ${result.length}`); 104 | this.logger?.debug(`- Done.`); 105 | return result; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | Thought for a few seconds 2 | 3 | Below is a comprehensive security policy tailored for both the `@glandjs/core` and `@glandjs/common` packages. It follows GitHub’s recommended SECURITY.md structure and covers supported versions, general security considerations, best practices, and responsible disclosure guidelines. 4 | 5 | --- 6 | 7 | ## Summary 8 | 9 | This document outlines GlandJS’s security policy for its core architectural packages—`@glandjs/core` and `@glandjs/common` (currently at version **1.0.1‑alpha**). It describes which versions are supported, highlights key security considerations specific to dependency injection and shared utilities, prescribes best practices for safe usage, and defines a clear, private reporting and response process for vulnerabilities. All contributors and consumers should follow these guidelines to ensure GlandJS remains robust against potential threats. 10 | 11 | ## Supported Versions 12 | 13 | | Package | Version | Supported | 14 | | ----------------- | ----------- | :-------: | 15 | | `@glandjs/core` | 1.0.1‑alpha | ✅ Yes | 16 | | `@glandjs/common` | 1.0.1‑alpha | ✅ Yes | 17 | 18 | We provide security fixes only for the above versions. Versions older than **1.0.1‑alpha** are no longer maintained and users are strongly encouraged to upgrade. 19 | 20 | --- 21 | 22 | ## Security Considerations 23 | 24 | Both `@glandjs/core` (the protocol-agnostic DI + event‑sync engine) and `@glandjs/common` (shared decorators, interfaces, utilities) live at the heart of GlandJS’s modular architecture. While neither package performs network I/O directly, misuse or misconfiguration can still introduce vulnerabilities. 25 | 26 | ### Trust Boundaries 27 | 28 | - **Core vs. External Layers** 29 | Keep your application’s external entry points (HTTP controllers, WebSocket handlers, RPC adapters) separate from internal event flows. Only wire trusted modules into the `@glandjs/core` broker channels. 30 | - **Decorator Usage** 31 | Only apply decorators from `@glandjs/common` on classes and methods you control. Avoid dynamically constructing modules or injecting third‑party classes without explicit validation. 32 | 33 | ### Input Handling & Validation 34 | 35 | - **Sanitize All Inputs** 36 | Any payload flowing through event channels (e.g. data emitted via `EventBroker`) must be validated or sanitized—even if it originates from internal code—before passing it to other listeners or modules. 37 | - **Immutable Data Patterns** 38 | Treat event payloads and shared data objects as immutable. If a listener needs to transform data, clone or map it rather than mutating the original object to avoid side‑effects. 39 | 40 | ### Error Management 41 | 42 | - **Centralized Error Hook** 43 | Use a dedicated error‑handling channel in `@glandjs/core` (e.g. an `"error"` namespace) to catch and log exceptions from asynchronous listeners. 44 | - **Avoid Silent Failures** 45 | If a listener throws, propagate the error to the central broker or to the caller so that you never lose sight of critical failures. 46 | 47 | ### Resource Isolation 48 | 49 | - **Module Scoping** 50 | Keep modules and channels small and single‑purpose. Do not share state between unrelated modules unless explicitly intended. 51 | - **Minimal Privileges** 52 | Only grant each module access to the broker channels and utilities it requires—do not expose the entire `@glandjs/common` toolkit to untrusted code. 53 | 54 | ## Reporting a Vulnerability 55 | 56 | We value responsible disclosure. If you find a security issue in either `@glandjs/core` or `@glandjs/common`, please: 57 | 58 | 1. **Do not create a public issue.** 59 | 2. **Email us privately** at [bitsgenix@gmail.com](mailto:bitsgenix@gmail.com) with: 60 | 61 | - **Subject:** `[SECURITY] @glandjs/{core|common}` 62 | - **Details:** 63 | 64 | - Package name and version 65 | - Description of the vulnerability 66 | - Steps to reproduce (minimal repro if possible) 67 | - Impact assessment 68 | - Suggested fix (optional) 69 | 70 | We will acknowledge receipt within 48 hours and keep you updated on our progress toward a patch and public advisory. 71 | 72 | ## Response & Disclosure 73 | 74 | - **Acknowledgement:** Within 2 business days. 75 | - **Fix Timeline:** Critical fixes targeted within 7 days; medium/low within 14 days. 76 | - **Advisory Publication:** We will release a public advisory on GitHub and npm once a fix is ready—credits to the reporter will be provided unless requested otherwise. 77 | 78 | _Last updated: May 3, 2025_ 79 | -------------------------------------------------------------------------------- /packages/core/injector/container/container.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, ImportableModule, InjectionToken, isDynamicModule, MODULE_METADATA } from '@glandjs/common'; 2 | import { Module } from '../module'; 3 | import { DependencyGraph } from '../graph'; 4 | import { ModulesContainer } from './module-container'; 5 | import { Constructor, type Logger } from '@medishn/toolkit'; 6 | 7 | export class Container { 8 | private readonly _modules = new ModulesContainer(); 9 | private readonly instances = new Map(); 10 | private readonly graph = new DependencyGraph(); 11 | private logger?: Logger; 12 | constructor(logger?: Logger) { 13 | this.logger = logger?.child('Container'); 14 | } 15 | 16 | get modules(): ModulesContainer { 17 | return this._modules; 18 | } 19 | 20 | public async register(module: Constructor | DynamicModule, parentModuleId?: string): Promise { 21 | const { module: moduleClass, metadata } = this.normalizeModule(module); 22 | const moduleId = this.getId(moduleClass); 23 | 24 | const existingModule = this._modules.getByToken(moduleId); 25 | if (existingModule) { 26 | return existingModule; 27 | } 28 | this.logger?.debug(`Registering module: ${moduleId}`); 29 | const moduleRef = this.createModule(moduleId, moduleClass); 30 | this.graph.addNode(moduleId, 'module', parentModuleId, metadata); 31 | 32 | await this.processImports(moduleRef, metadata.imports || [], moduleId); 33 | 34 | this.processControllers(moduleRef, metadata.controllers || [], moduleId); 35 | this.processChannels(moduleRef, metadata.channels || [], moduleId); 36 | 37 | this.logger?.debug(`- Done.`); 38 | return moduleRef; 39 | } 40 | private normalizeModule(module: Constructor | DynamicModule) { 41 | if (isDynamicModule(module)) { 42 | return { 43 | module: module.module, 44 | metadata: { 45 | imports: module.imports || [], 46 | controllers: module.controllers || [], 47 | channels: module.channels || [], 48 | }, 49 | }; 50 | } 51 | return { 52 | module, 53 | metadata: Reflect.getMetadata(MODULE_METADATA, module) || { 54 | imports: [], 55 | controllers: [], 56 | channels: [], 57 | }, 58 | }; 59 | } 60 | private getId(token: InjectionToken): string { 61 | if (typeof token === 'string') return token; 62 | if (typeof token === 'symbol') return token.description || token.toString(); 63 | return token.name; 64 | } 65 | private createModule(token: string, module: Constructor): Module { 66 | const moduleRef = new Module(token, module); 67 | this._modules.set(token, moduleRef); 68 | return moduleRef; 69 | } 70 | 71 | private async processImports(module: Module, imports: ImportableModule[], moduleId: string): Promise { 72 | const importedModules: Module[] = []; 73 | 74 | for (const imported of imports) { 75 | const importedModule = await this.register(imported instanceof Promise ? await imported : imported, moduleId); 76 | importedModules.push(importedModule); 77 | } 78 | 79 | module.addImports(importedModules); 80 | } 81 | 82 | private processControllers(module: Module, controllers: Constructor[], moduleId: string): void { 83 | controllers.forEach((controller) => { 84 | const controllerId = this.getId(controller); 85 | this.graph.addNode(controllerId, 'controller', moduleId); 86 | 87 | const instance = this.resolve(controller); 88 | module.addController(controller, instance); 89 | 90 | const controllerNode = this.graph.getNode(controllerId); 91 | if (controllerNode) { 92 | controllerNode.instance = instance; 93 | } 94 | this.logger?.debug(`Registered controller: ${controllerId}`); 95 | }); 96 | } 97 | 98 | private processChannels(module: Module, channels: Constructor[], moduleId: string): void { 99 | channels.forEach((channel) => { 100 | const channelId = this.getId(channel); 101 | this.graph.addNode(channelId, 'channel', moduleId); 102 | 103 | const instance = this.resolve(channel); 104 | module.addChannel(channel, instance); 105 | 106 | const node = this.graph.getNode(channelId); 107 | if (node) { 108 | node.instance = instance; 109 | } 110 | this.logger?.debug(`Registered channel: ${channelId}`); 111 | }); 112 | } 113 | 114 | public resolve(token: InjectionToken): T { 115 | if (this.instances.has(token)) { 116 | return this.instances.get(token); 117 | } 118 | 119 | const constructor = token as Constructor; 120 | const paramTypes = Reflect.getMetadata('design:paramtypes', constructor) || []; 121 | 122 | const dependencies = paramTypes.map((param) => this.resolve(param)); 123 | 124 | const instance = new constructor(...dependencies); 125 | this.instances.set(token, instance); 126 | this.logger?.debug(`Resolved instance: ${this.getId(token)}`); 127 | return instance; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others' private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [bitsgenix@gmail.com](mailto:bitsgenix@gmail.com). All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. 74 | 75 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 76 | 77 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 78 | -------------------------------------------------------------------------------- /test/unit/injector.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { Module } from '../../packages/core/injector/module'; 4 | import { InstanceWrapper } from '../../packages/core/injector/instance-wrapper'; 5 | describe('Injector-Unit', () => { 6 | it('Controller method should work', () => { 7 | class TestController { 8 | sayHello() { 9 | return 'hello'; 10 | } 11 | } 12 | 13 | const module = new Module('TestModule', TestController); 14 | module.addController(TestController, new TestController()); 15 | 16 | const wrapper = module.controllers.get(TestController); 17 | const instance = wrapper?.getInstance(); 18 | 19 | expect(instance.sayHello()).to.deep.equal('hello'); 20 | }); 21 | it('Imported module should be added', () => { 22 | const rootModule = new Module('RootModule', class {}); 23 | const importedModule = new Module('ImportedModule', class {}); 24 | 25 | rootModule.addImports([importedModule]); 26 | const root = rootModule.imports; 27 | expect(root.has(importedModule)).to.be.equal(true); 28 | }); 29 | it('Channel method should work', () => { 30 | class TestChannel { 31 | handle() { 32 | return 'channel working'; 33 | } 34 | } 35 | 36 | const module = new Module('TestModule', TestChannel); 37 | module.addChannel(TestChannel, new TestChannel()); 38 | 39 | const wrapper = module.channels.get(TestChannel); 40 | const instance = wrapper?.getInstance(); 41 | expect(instance.handle()).to.deep.equal('channel working'); 42 | }); 43 | it('Should throw an Error', () => { 44 | class NoInstance {} 45 | 46 | const wrapper = new InstanceWrapper(NoInstance); 47 | expect(() => wrapper.getInstance()).to.throw('Instance class NoInstance{static{__name(this,"NoInstance")}} not initialized'); 48 | }); 49 | it('Combining Controller and Channel in one module', () => { 50 | class MyController { 51 | get() { 52 | return 'ok'; 53 | } 54 | } 55 | class MyChannel { 56 | publish() { 57 | return 'sent'; 58 | } 59 | } 60 | 61 | const apiModule = new Module('ApiModule', class ApiModule {}); 62 | 63 | apiModule.addController(MyController, new MyController()); 64 | apiModule.addChannel(MyChannel); 65 | expect(apiModule.controllers.size).to.deep.equal(1); 66 | expect(apiModule.channels.size).to.deep.equal(1); 67 | 68 | const chWrapper = apiModule.channels.get(MyChannel)!; 69 | expect(() => chWrapper.getInstance()).to.throw('Instance class MyChannel{static{__name(this,"MyChannel")}publish(){return"sent"}} not initialized'); 70 | }); 71 | it('Module A is added as an import to B', () => { 72 | const moduleA = new Module('A', class A {}); 73 | const moduleB = new Module('B', class B {}); 74 | 75 | moduleB.addImports([moduleA]); 76 | expect(moduleB.imports.has(moduleA)).to.be.true; 77 | expect(moduleA.imports.size).to.be.equal(0); 78 | }); 79 | it('Adding a controller to the module', () => { 80 | class MyController { 81 | get() { 82 | return 'ok'; 83 | } 84 | } 85 | 86 | const mod = new Module('AppModule', class AppModule {}); 87 | expect(mod.controllers.size).to.be.equal(0); 88 | 89 | mod.addController(MyController); 90 | expect(mod.controllers.has(MyController)).to.be.true; 91 | 92 | const wrapper = mod.controllers.get(MyController)!; 93 | expect(() => wrapper.getInstance()).to.throw('Instance class MyController{static{__name(this,"MyController")}get(){return"ok"}} not initialized'); 94 | mod.addController(MyController, new MyController()); 95 | 96 | const wrapper2 = mod.controllers.get(MyController)!; 97 | expect(wrapper2.getInstance().get()).to.deep.equal('ok'); 98 | }); 99 | 100 | it('InstanceWrapper.id uses symbol.toString()', () => { 101 | const sym = Symbol('MySym'); 102 | // @ts-ignore 103 | const wrapper = new InstanceWrapper(sym, class {}); 104 | expect(wrapper.id).to.equal(sym.toString()); 105 | }); 106 | 107 | it('addController overrides existing wrapper instance', () => { 108 | class MyCtrl {} 109 | const mod = new Module('M', class {}); 110 | mod.addController(MyCtrl); 111 | const original = mod.controllers.get(MyCtrl)!; 112 | // now override with a real instance 113 | const realInst = new MyCtrl(); 114 | mod.addController(MyCtrl, realInst); 115 | const updated = mod.controllers.get(MyCtrl)!; 116 | 117 | expect(mod.controllers.size).to.equal(1); 118 | expect(updated).to.not.equal(original); 119 | expect(updated.getInstance()).to.equal(realInst); 120 | }); 121 | 122 | it('addImports ignores duplicate modules', () => { 123 | const m1 = new Module('M1', class {}); 124 | const m2 = new Module('M2', class {}); 125 | // add the same import twice 126 | m1.addImports([m2, m2]); 127 | expect(m1.imports.size).to.equal(1); 128 | expect(m1.imports.has(m2)).to.be.true; 129 | }); 130 | 131 | it('Controller and Channel maps remain separate even with same token', () => { 132 | class Shared {} 133 | const mod = new Module('M', class {}); 134 | mod.addController(Shared, new Shared()); 135 | mod.addChannel(Shared, new Shared()); 136 | 137 | expect(mod.controllers.size).to.equal(1); 138 | expect(mod.channels.size).to.equal(1); 139 | 140 | const ctrlWrapper = mod.controllers.get(Shared)!; 141 | const chanWrapper = mod.channels.get(Shared)!; 142 | expect(ctrlWrapper).to.not.equal(chanWrapper); 143 | expect(ctrlWrapper.getInstance()).to.be.instanceOf(Shared); 144 | expect(chanWrapper.getInstance()).to.be.instanceOf(Shared); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /packages/core/injector/scanner/dependencies-scanner.ts: -------------------------------------------------------------------------------- 1 | import { Constructor, Logger } from '@medishn/toolkit'; 2 | import { DynamicModule, ImportableModule, InjectionToken, isDynamicModule, MODULE_METADATA, ModuleMetadata } from '@glandjs/common'; 3 | import { Container, ModulesContainer } from '../container'; 4 | import type { Module } from '../module'; 5 | 6 | export class DependenciesScanner { 7 | private readonly container: Container; 8 | private logger?: Logger; 9 | constructor(logger?: Logger) { 10 | this.container = new Container(logger); 11 | this.logger = logger?.child('Scanner'); 12 | } 13 | get modules(): ModulesContainer { 14 | return this.container.modules; 15 | } 16 | public async scan(rootModule: Constructor | DynamicModule): Promise { 17 | this.logger?.debug('Starting full module scan'); 18 | 19 | await this.scanForModules(rootModule); 20 | 21 | this.logger?.debug('Completed scanning module structure, scanning dependencies'); 22 | 23 | await this.scanModulesForDependencies(); 24 | 25 | this.logger?.debug('- Done.'); 26 | } 27 | 28 | private async scanForModules(rootModule: Constructor | DynamicModule): Promise { 29 | const moduleRef = await this.container.register(rootModule); 30 | this.logger?.debug(`Registered module: ${moduleRef.token}`); 31 | this.modules.set(moduleRef.token, moduleRef); 32 | 33 | for (const importedModule of moduleRef.imports) { 34 | if (!this.modules.has(importedModule.token)) { 35 | this.modules.set(importedModule.token, importedModule); 36 | 37 | const moduleType = this.getModuleByToken(importedModule.token); 38 | if (moduleType) { 39 | await this.scanForModules(moduleType); 40 | } 41 | } 42 | } 43 | } 44 | 45 | private async scanModulesForDependencies(): Promise { 46 | for (const [_, moduleRef] of this.modules.entries()) { 47 | await this.scanModuleDependencies(moduleRef); 48 | } 49 | } 50 | private async scanModuleDependencies(moduleRef: Module): Promise { 51 | const token = moduleRef.token; 52 | const moduleType = this.getModuleByToken(token); 53 | 54 | if (!moduleType) { 55 | throw new Error(`Could not find module type for token: ${token}`); 56 | } 57 | 58 | const metadata = this.extractModuleMetadata(moduleType); 59 | this.logger?.debug(`Scanning dependencies for module: ${token}`); 60 | 61 | // Process imports 62 | if (metadata.imports && metadata.imports.length > 0) { 63 | this.logger?.debug(` - Imports found: ${metadata.imports.length}`); 64 | await this.scanModuleImports(metadata.imports, moduleRef); 65 | } 66 | 67 | // Process controllers 68 | if (metadata.controllers && metadata.controllers.length > 0) { 69 | this.logger?.debug(` - Controllers found: ${metadata.controllers.map((c) => c.name).join(', ')}`); 70 | this.scanControllers(metadata.controllers, moduleRef); 71 | } 72 | 73 | // Process channels 74 | if (metadata.channels && metadata.channels.length > 0) { 75 | this.logger?.debug(` - Channels found: ${metadata.channels.map((c) => c.name).join(', ')}`); 76 | this.scanChannels(metadata.channels, moduleRef); 77 | } 78 | } 79 | 80 | private getModuleByToken(token: InjectionToken): Constructor | undefined { 81 | const moduleRef = this.modules.getByToken(token as string); 82 | return moduleRef ? moduleRef.metatype : undefined; 83 | } 84 | 85 | private extractModuleMetadata(moduleType: Constructor): ModuleMetadata { 86 | return ( 87 | Reflect.getMetadata(MODULE_METADATA, moduleType) ?? { 88 | imports: [], 89 | controllers: [], 90 | channels: [], 91 | } 92 | ); 93 | } 94 | 95 | private async getModuleType(moduleDefinition: ImportableModule): Promise { 96 | if (isDynamicModule(moduleDefinition)) { 97 | return moduleDefinition.module; 98 | } 99 | 100 | if (moduleDefinition instanceof Promise) { 101 | const resolvedModule = await moduleDefinition; 102 | return isDynamicModule(resolvedModule) ? resolvedModule.module : resolvedModule; 103 | } 104 | 105 | return moduleDefinition; 106 | } 107 | 108 | private async getModuleToken(moduleDefinition: ImportableModule): Promise { 109 | const moduleType = await this.getModuleType(moduleDefinition); 110 | return moduleType.name; 111 | } 112 | 113 | private async scanModuleImports(imports: ImportableModule[], moduleRef: Module): Promise { 114 | for (const importedModule of imports) { 115 | const token = await this.getModuleToken(importedModule); 116 | const importedRef = this.modules.getByToken(token); 117 | 118 | if (importedRef) { 119 | moduleRef.addImports([importedRef]); 120 | } else { 121 | this.logger?.warn(`Could not find imported module with token: ${token}`); 122 | } 123 | } 124 | } 125 | 126 | private scanControllers(controllers: Constructor[], moduleRef: Module): void { 127 | controllers.forEach((controller) => { 128 | const instance = this.container.resolve(controller); 129 | moduleRef.addController(controller, instance); 130 | this.logger?.debug(`Added controller: ${controller.name} to module: ${moduleRef.token}`); 131 | }); 132 | } 133 | 134 | private scanChannels(channels: Constructor[], moduleRef: Module): void { 135 | channels.forEach((channel) => { 136 | const instance = this.container.resolve(channel); 137 | moduleRef.addChannel(channel, instance); 138 | this.logger?.debug(`Added channel: ${channel.name} to module: ${moduleRef.token}`); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/core/hooks/lifecycle.scanner.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@medishn/toolkit'; 2 | import { ModulesContainer } from '../injector'; 3 | import { InstanceWrapper } from '../injector/instance-wrapper'; 4 | import type { Module } from '../injector/module'; 5 | 6 | export type LifecycleHook = 'onModuleInit' | 'onModuleDestroy' | 'onAppBootstrap' | 'onAppShutdown' | 'onChannelInit'; 7 | 8 | export type LifecycleComponentType = 'module' | 'controller' | 'channel'; 9 | 10 | export interface LifecycleComponent { 11 | instance: any; 12 | moduleToken: string; 13 | componentType: LifecycleComponentType; 14 | componentToken: string; 15 | } 16 | 17 | export class LifecycleScanner { 18 | private readonly moduleComponentMap = new Map>(); 19 | private readonly logger?: Logger; 20 | constructor( 21 | private readonly modulesContainer: ModulesContainer, 22 | logger?: Logger, 23 | ) { 24 | this.logger = logger?.child('LifecycleScanner'); 25 | } 26 | 27 | public scanForHooks(): void { 28 | this.logger?.debug('Scanning for lifecycle hooks'); 29 | 30 | for (const [moduleToken, module] of this.modulesContainer.entries()) { 31 | this.scanModule(moduleToken, module); 32 | } 33 | 34 | this.logger?.debug(`Found components with hooks in ${this.moduleComponentMap.size} modules`); 35 | } 36 | 37 | private scanModule(moduleToken: string, module: Module): void { 38 | const components = new Set(); 39 | this.moduleComponentMap.set(moduleToken, components); 40 | 41 | if (module.metatype) { 42 | try { 43 | const moduleInstance = this.getModuleInstance(module); 44 | if (moduleInstance) { 45 | components.add({ 46 | instance: moduleInstance, 47 | moduleToken, 48 | componentType: 'module', 49 | componentToken: moduleToken, 50 | }); 51 | this.logger?.debug(`Module ${moduleToken} registered for lifecycle hooks`); 52 | } 53 | } catch (error) { 54 | this.logger?.error(`Error resolving module instance for ${moduleToken}: ${error.message}`); 55 | } 56 | } 57 | 58 | this.scanComponentsForHooks(moduleToken, module.controllers, 'controller', components); 59 | 60 | this.scanComponentsForHooks(moduleToken, module.channels, 'channel', components); 61 | } 62 | private scanComponentsForHooks(moduleToken: string, components: Map, componentType: LifecycleComponentType, lifecycleComponents: Set): void { 63 | for (const [token, wrapper] of components.entries()) { 64 | try { 65 | const instance = wrapper.getInstance(); 66 | const componentToken = typeof token === 'function' ? token.name : String(token); 67 | const hasHooks = this.hasLifecycleHooks(instance); 68 | 69 | if (hasHooks) { 70 | lifecycleComponents.add({ 71 | instance, 72 | moduleToken, 73 | componentType, 74 | componentToken, 75 | }); 76 | this.logger?.debug(`${componentType} ${componentToken} registered for lifecycle hooks`); 77 | } 78 | } catch (error) { 79 | this.logger?.error(`Error resolving ${componentType} instance: ${error.message}`); 80 | } 81 | } 82 | } 83 | 84 | private getModuleInstance(module: Module): any { 85 | const token = module.metatype; 86 | return new module.metatype(); 87 | } 88 | 89 | private hasLifecycleHooks(instance: any): boolean { 90 | return ( 91 | this.hasHook(instance, 'onModuleInit') || 92 | this.hasHook(instance, 'onModuleDestroy') || 93 | this.hasHook(instance, 'onAppBootstrap') || 94 | this.hasHook(instance, 'onAppShutdown') || 95 | this.hasHook(instance, 'onChannelInit') 96 | ); 97 | } 98 | 99 | private hasHook(instance: any, hook: LifecycleHook): boolean { 100 | return instance && typeof instance[hook] === 'function'; 101 | } 102 | 103 | public async runHook(hook: LifecycleHook, signal?: string): Promise { 104 | this.logger?.debug(`Running ${hook} hooks`); 105 | 106 | const promises: Promise[] = []; 107 | 108 | for (const [moduleToken, components] of this.moduleComponentMap.entries()) { 109 | for (const component of components) { 110 | if (this.hasHook(component.instance, hook)) { 111 | this.logger?.debug(`Executing ${hook} on ${component.componentType} ${component.componentToken}`); 112 | 113 | try { 114 | let promise: Promise | void; 115 | 116 | if (hook === 'onAppShutdown') { 117 | promise = component.instance[hook](signal); 118 | } else { 119 | promise = component.instance[hook](); 120 | } 121 | 122 | // Ensure the result is a Promise 123 | if (promise && typeof promise.then === 'function') { 124 | promises.push(promise); 125 | } 126 | } catch (error) { 127 | this.logger?.error(`Error running ${hook} on ${component.componentType} ${component.componentToken}: ${error.message}`); 128 | } 129 | } 130 | } 131 | } 132 | 133 | if (promises.length > 0) { 134 | await Promise.all(promises); 135 | } 136 | 137 | this.logger?.debug(`Completed ${hook} hooks`); 138 | } 139 | 140 | public async onModuleInit(): Promise { 141 | await this.runHook('onModuleInit'); 142 | } 143 | 144 | public async onModuleDestroy(): Promise { 145 | await this.runHook('onModuleDestroy'); 146 | } 147 | 148 | public async onAppBootstrap(): Promise { 149 | await this.runHook('onAppBootstrap'); 150 | } 151 | 152 | public async onAppShutdown(signal?: string): Promise { 153 | await this.runHook('onAppShutdown', signal); 154 | } 155 | 156 | public async onChannelInit(): Promise { 157 | await this.runHook('onChannelInit'); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gland 2 | 3 | We’re excited that you’re interested in contributing to **Gland**! As a fully **event-driven framework**, Gland is designed to be lightweight, modular, and flexible. Your contributions can help shape its future and make it even better for developers worldwide. 4 | 5 | **Before contributing**, please read this guide thoroughly. It ensures we maintain consistency across the framework while fostering an open, collaborative environment. 6 | 7 | --- 8 | 9 | ## Table of Contents 10 | 11 | - [Contributing to Gland](#contributing-to-gland) 12 | - [Table of Contents](#table-of-contents) 13 | - [ Code of Conduct](#-code-of-conduct) 14 | - [ Support Questions](#-support-questions) 15 | - [ Getting Started](#-getting-started) 16 | - [Prerequisites](#prerequisites) 17 | - [Clone the Repository](#clone-the-repository) 18 | - [ Reporting Issues](#-reporting-issues) 19 | - [ Feature Requests](#-feature-requests) 20 | - [ Submitting Pull Requests](#-submitting-pull-requests) 21 | - [ Development Setup](#-development-setup) 22 | - [Running the Project](#running-the-project) 23 | - [Common Scripts](#common-scripts) 24 | - [ Coding Guidelines](#-coding-guidelines) 25 | - [ Commit Message Format](#-commit-message-format) 26 | - [Allowed Types](#allowed-types) 27 | - [Thank You!](#thank-you) 28 | 29 | --- 30 | 31 | ## Code of Conduct 32 | 33 | Gland is committed to fostering an inclusive and respectful community. All contributors are expected to adhere to our Code of Conduct, which promotes a harassment-free experience for everyone. Please read the [Code of Conduct](CODE_OF_CONDUCT.md) before participating. 34 | 35 | **Violations will not be tolerated.** Report issues to bitsgenix@gmail.com. 36 | 37 | --- 38 | 39 | ## Support Questions 40 | 41 | **Do not create GitHub issues for general questions.** Instead: 42 | 43 | - Use [Stack Overflow](https://stackoverflow.com) with the `gland` tag 44 | - Join our [Discord Community](https://discord.gg/GtRtkrEYwR) 45 | - Check our [Documentation](https://glandjs.github.io/docs/) 46 | 47 | ## Getting Started 48 | 49 | ### Prerequisites 50 | 51 | - **Node.js**: Ensure you have Node.js installed (version 18 or higher). 52 | - **Git**: Familiarity with Git and GitHub workflows is required. 53 | - **TypeScript**: Gland is built with TypeScript, so a basic understanding is helpful. 54 | 55 | ### Clone the Repository 56 | 57 | 1. Fork the [Gland repository](https://github.com/your-username/gland). 58 | 2. Clone your fork locally: 59 | ```bash 60 | git clone https://github.com/your-username/gland.git 61 | cd gland 62 | ``` 63 | 3. Install dependencies: 64 | ```bash 65 | npm install 66 | ``` 67 | 68 | --- 69 | 70 | ## Reporting Issues 71 | 72 | If you encounter a bug or have a question, please follow these steps: 73 | 74 | 1. **Search Existing Issues**: Check if the issue has already been reported. 75 | 2. **Create a New Issue**: If it hasn’t, open a new issue with the following details: 76 | - A clear title and description. 77 | - Steps to reproduce the issue. 78 | - Expected vs. actual behavior. 79 | - Relevant code snippets or screenshots (if applicable). 80 | - Your environment (Node.js version, OS, etc.). 81 | 82 | **Note**: For general questions, please use [Stack Overflow](https://stackoverflow.com) with the tag `gland`. 83 | 84 | Want real-time discussion? Join our [Discord community](https://discord.gg/GtRtkrEYwR). 85 | 86 | --- 87 | 88 | ## Feature Requests 89 | 90 | 1. **Open an Issue**: Describe the feature and its benefits. 91 | 2. **Discuss**: We’ll review the proposal and provide feedback. 92 | 3. **Implement**: If approved, you can submit a pull request (see below). 93 | 94 | --- 95 | 96 | ## Submitting Pull Requests 97 | 98 | 1. **Fork the Repository**: Create a fork of the Gland repository. 99 | 2. **Create a Branch**: 100 | ```bash 101 | git checkout -b my-feature-branch 102 | ``` 103 | 3. **Make Your Changes**: Follow the [Coding Guidelines](#coding-guidelines). 104 | 4. **Test Your Changes**: Ensure all tests pass and add new tests if needed. (Mocha&Chai preferred) 105 | 5. **Commit Your Changes**: Use a descriptive commit message (see [Commit Message Format](#commit-message-format)). 106 | 6. **Push Your Branch**: 107 | ```bash 108 | git push origin my-feature-branch 109 | ``` 110 | 7. **Open a PR**: Submit a pull request to the `main` branch of the Gland repository. 111 | 112 | **PR Guidelines**: 113 | 114 | - Provide a clear description of the changes. 115 | - Reference related issues (e.g., `Fixes #123`). 116 | - Ensure your code follows the project’s style and conventions. 117 | 118 | --- 119 | 120 | ## Development Setup 121 | 122 | ### Running the Project 123 | 124 | 1. Build the project: 125 | ```bash 126 | npm run build 127 | ``` 128 | 2. Run tests: 129 | ```bash 130 | npm test 131 | ``` 132 | 133 | ### Common Scripts 134 | 135 | - **Build**: `npm run build` 136 | - **Test**: `npm test` 137 | 138 | --- 139 | 140 | ## Coding Guidelines 141 | 142 | To maintain consistency across the codebase, please follow these rules: 143 | 144 | 1. **TypeScript**: Use TypeScript for all new code. 145 | 2. **Event-Driven Design**: Ensure all features align with Gland’s event-driven architecture. 146 | 3. **Modularity**: Keep code modular and reusable. 147 | 4. **Testing**: Write unit tests for new features and bug fixes. 148 | 5. **Documentation**: Add comments and update documentation as needed. 149 | 150 | --- 151 | 152 | ## Commit Message Format 153 | 154 | We follow a structured commit message format to maintain a clean project history. Each commit message should include: 155 | 156 | - **Type**: The type of change (e.g., `feat`, `fix`, `docs`). 157 | - **Scope**: The affected module or package (e.g., `core`, `channel`). 158 | - **Subject**: A concise description of the change. 159 | 160 | Example: 161 | 162 | ``` 163 | feat(core): add support for event-scoped middleware 164 | ``` 165 | 166 | ### Allowed Types 167 | 168 | - **feat**: A new feature. 169 | - **fix**: A bug fix. 170 | - **docs**: Documentation changes. 171 | - **style**: Code formatting or style changes. 172 | - **refactor**: Code refactoring (no functional changes). 173 | - **test**: Adding or updating tests. 174 | - **chore**: Maintenance tasks (e.g., dependency updates). 175 | 176 | --- 177 | 178 | ## Thank You! 179 | 180 | Your contributions make Gland better for everyone. Whether you’re fixing a bug, adding a feature, or improving documentation, we appreciate your effort and dedication. 181 | 182 | If you have any questions, feel free to reach out by opening an issue or joining our community discussions. 183 | --------------------------------------------------------------------------------