From cfdb9adaa25b41364d5f398004bb9f1ca63b6725 Mon Sep 17 00:00:00 2001 From: Drischdaan <42834596+Drischdaan@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:46:33 +0200 Subject: [PATCH] Implemented everything to a working state --- .github/workflows/publish.yml | 19 +++++ .npmignore | 11 +++ README.md | 2 +- package.json | 14 +++- src/application/application.interfaces.ts | 12 +++ src/application/application.ts | 97 +++++++++++++++++++++++ src/index.ts | 13 +++ src/ioc/container.ts | 80 +++++++++++++++++++ src/ioc/ioc.interfaces.ts | 47 +++++++++++ src/ioc/scopes.ts | 42 ++++++++++ src/ioc/util.ts | 19 +++++ src/module/module.interfaces.ts | 30 +++++++ src/mooncake.ts | 18 +++++ src/types.ts | 4 + src/util/basic.logger.ts | 22 +++++ src/util/logger.interfaces.ts | 5 ++ tsconfig.json | 6 +- yarn.lock | 14 +++- 18 files changed, 448 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 .npmignore create mode 100644 src/application/application.interfaces.ts create mode 100644 src/application/application.ts create mode 100644 src/ioc/container.ts create mode 100644 src/ioc/ioc.interfaces.ts create mode 100644 src/ioc/scopes.ts create mode 100644 src/ioc/util.ts create mode 100644 src/module/module.interfaces.ts create mode 100644 src/mooncake.ts create mode 100644 src/types.ts create mode 100644 src/util/basic.logger.ts create mode 100644 src/util/logger.interfaces.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..93d8147 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +name: Publish +on: + release: + types: [created] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '14.x' + registry-url: 'https://registry.npmjs.org' + - run: yarn install + - run: yarn build + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5063d8f --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +src/ +test/ +tsconfig.json +jest.config.json +*.js.map +.eslintrc.js +typings +.vscode +.prettierrc +.gitignore +yarn.lock \ No newline at end of file diff --git a/README.md b/README.md index 562a2be..2bd5678 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

# mooncake -Component based app framework +NestJs and Angular inspired application framework ## Support diff --git a/package.json b/package.json index 6129908..3510131 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,23 @@ { - "name": "mooncake", + "name": "@drischdaan/mooncake", + "description": "NestJs and Angular inspired application framework", "version": "1.0.0", "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "typings": "./dist/index.d.ts", "repository": "https://github.com/Drischdaan/mooncake", "author": "Drischdaan", "license": "MIT", + "files": [ + "dist" + ], "scripts": { "prebuild": "rimraf dist", "build": "tsc", "preversion": "yarn run build", "prestart": "yarn run build", "start": "node ./dist/index.js", - "start:dev": "ts-node-dev --pretty --log-error --quiet --respawn ./src/index.ts", + "start:dev": "ts-node-dev --pretty --log-error --respawn ./src/index.ts", "format": "prettier --write \"src/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" --fix" }, @@ -25,5 +31,9 @@ "rimraf": "^3.0.2", "ts-node-dev": "^1.1.6", "typescript": "^4.2.4" + }, + "dependencies": { + "reflect-metadata": "^0.1.13", + "tslog": "^3.2.0" } } diff --git a/src/application/application.interfaces.ts b/src/application/application.interfaces.ts new file mode 100644 index 0000000..c0678d4 --- /dev/null +++ b/src/application/application.interfaces.ts @@ -0,0 +1,12 @@ +import { IContainer } from "../ioc/ioc.interfaces"; +import { ILogger } from "../util/logger.interfaces"; + +export interface IApplicationOptions { + name: string; +} + +export interface IApplication { + onInitialization(): Promise; + onStart(): Promise; + onStop(): Promise; +} \ No newline at end of file diff --git a/src/application/application.ts b/src/application/application.ts new file mode 100644 index 0000000..0f4d378 --- /dev/null +++ b/src/application/application.ts @@ -0,0 +1,97 @@ +import { IocContainer } from "../ioc/container"; +import { IClassProvider, IContainer, IFactoryProvider, IProvider, IScope, IValueProvider } from "../ioc/ioc.interfaces"; +import { isClassProvider, isFactoryProvider, isValueProvider } from "../ioc/util"; +import { IModule, IModuleOptions } from "../module/module.interfaces"; +import { Class } from "../types"; +import { ILogger } from "../util/logger.interfaces"; +import { BasicLogger } from "../util/basic.logger"; +import { IApplication, IApplicationOptions } from "./application.interfaces"; + +export abstract class Application implements IApplication { + + protected readonly container: IContainer; + protected readonly logger: ILogger; + private readonly internalLogger: ILogger; + + protected readonly controllers: Class[]; + protected readonly providers: (Class | IProvider)[]; + + constructor( + protected readonly mainModule: IModule, + protected readonly options: IApplicationOptions, + ) { + this.container = new IocContainer(); + this.logger = new BasicLogger(options.name); + this.internalLogger = new BasicLogger('Bootstrap'); + this.controllers= []; + this.providers = []; + } + + public async onInitialization(): Promise { + this.internalLogger.info('Initializing', this.options.name, '...'); + this.internalLogger.info('Preparing container providers...'); + await this.registerModule(this.mainModule); + } + + public async onStart(): Promise { + + } + + public async onStop(): Promise { + + } + + private async registerModule(module: IModule): Promise { + await this.registerProviders(module); + await this.registerControllers(module); + await this.registerImports(module); + } + + private async registerProviders(module: IModule): Promise { + for(const provider of module.options.providers) { + if(isClassProvider(provider)) { + const classProvider: IClassProvider = >provider; + this.container.register(classProvider.key).asClassProvider(classProvider.class, classProvider.scope); + } else if(isFactoryProvider(provider)) { + const factoryProvider: IFactoryProvider = >provider; + this.container.register(factoryProvider.key).asFactoryProvider(factoryProvider.factory); + } else if(isValueProvider(provider)) { + const valueProvider: IValueProvider = >provider; + this.container.register(valueProvider.key).asValueProvider(valueProvider.value); + } else { + const providerName: string = Reflect.getMetadata('injectable:name', provider); + const providerScope: Class = Reflect.getMetadata('injectable:scope', provider); + if(providerName === undefined || providerScope === undefined) + throw new Error(`Invalid provider found: ${provider}`); + this.providers.push(provider); + this.container.register(>provider).asClassProvider(>provider, new providerScope()); + } + } + } + + private async registerControllers(module: IModule): Promise { + for(const controller of module.options.controllers) { + const controllerName: string = Reflect.getMetadata('injectable:name', controller); + const controllerScope: Class = Reflect.getMetadata('injectable:scope', controller); + if(controllerName === undefined || controllerScope === undefined) + throw new Error(`Invalid controller found: ${controller}`); + this.controllers.push(controller); + this.container.register(controllerName).asClassProvider(controller, new controllerScope()); + } + } + + private async registerImports(module: IModule): Promise { + for(const importedModule of module.options.imports) { + const moduleName: string = Reflect.getMetadata('module:name', importedModule) + const moduleOptions: IModuleOptions = Reflect.getMetadata('module:options', importedModule); + if(moduleName === undefined || moduleOptions === undefined) + throw new Error(`Invalid imported module found: ${importedModule}`); + const instance: any = new importedModule(); + if('onInitialization' in instance) + await instance['onInitialization'](); + const generatedModule: IModule = { name: moduleName, moduleClass: importedModule, options: moduleOptions }; + await this.registerModule(generatedModule); + } + } + +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e69de29..4a9ce1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,13 @@ +import 'reflect-metadata'; + +export * from './mooncake'; +export * from './types'; +export * from './util/logger.interfaces'; +export * from './util/basic.logger'; +export * from './module/module.interfaces'; +export * from './ioc/container'; +export * from './ioc/ioc.interfaces'; +export * from './ioc/scopes'; +export * from './ioc/util'; +export * from './application/application.interfaces'; +export * from './application/application'; \ No newline at end of file diff --git a/src/ioc/container.ts b/src/ioc/container.ts new file mode 100644 index 0000000..d9009d9 --- /dev/null +++ b/src/ioc/container.ts @@ -0,0 +1,80 @@ +import { TransientScope } from ".."; +import { Class } from "../types"; +import { IClassProvider, IContainer, IContainerEntry, IFactoryProvider, IProvider, IScope, IValueProvider } from "./ioc.interfaces"; +import { isClassProvider, isFactoryProvider, isValueProvider } from "./util"; + +export class ContainerEntry implements IContainerEntry { + + public provider: IProvider; + + constructor( + public key: string, + ) {} + + public asClassProvider(value: Class, scope: IScope = new TransientScope()): void { + this.provider = >{ + key: this.key, + class: value, + scope: scope, + }; + } + + public asFactoryProvider(value: Function): void { + this.provider = >{ + key: this.key, + factory: value + }; + } + + public asValueProvider(value: T): void { + this.provider = >{ + key: this.key, + value: value + }; + } + +} + +export class IocContainer implements IContainer { + + private readonly entries: IContainerEntry[]; + + constructor() { + this.entries = new Array>(); + } + + public register(key: Class | string): IContainerEntry { + const generatedKey: string = this.generateKey(key); + const entries: IContainerEntry[] = this.entries.filter((entry: IContainerEntry) => entry.provider.key === generatedKey); + if(entries.length !== 0) + throw new Error(`There is already an entry with that key!`); + let entry: IContainerEntry = new ContainerEntry(generatedKey); + this.entries.push(entry); + return entry; + } + + public resolve(key: string | Class): T { + const generatedKey: string = this.generateKey(key); + const entry: IContainerEntry = this.entries.find((entry: IContainerEntry) => entry.provider.key === generatedKey); + if(entry === undefined) + throw new Error(`There is no entry with that key: ${key}`); + if(isClassProvider(entry.provider)) + return (>entry.provider).scope.resolve((>entry.provider), this); + else if(isFactoryProvider(entry.provider)) + return (>entry.provider).factory(); + else if(isValueProvider(entry.provider)) + return (>entry.provider).value; + else + throw new Error(`Invalid provider detected with key "${entry.provider.key}"`); + } + + public generateKey(key: any): string { + if(typeof(key) === 'string') + return key; + const name: string = Reflect.getMetadata('injectable:name', key); + if(name === undefined) + return key.name; + return name; + } + +} \ No newline at end of file diff --git a/src/ioc/ioc.interfaces.ts b/src/ioc/ioc.interfaces.ts new file mode 100644 index 0000000..202e83b --- /dev/null +++ b/src/ioc/ioc.interfaces.ts @@ -0,0 +1,47 @@ +import { Class } from "../types"; +import { TransientScope } from "./scopes"; +import { makeTargetInjectable } from "./util"; + +export interface IScope { + resolve(provider: IClassProvider, container: IContainer): T; +} + +export interface IProvider { + key: string, +} + +export interface IFactoryProvider extends IProvider { + factory: Function, +} + +export interface IClassProvider extends IProvider { + class: Class, + scope: IScope, +} + +export interface IValueProvider extends IProvider { + value: T, +} + +export interface IContainerEntry { + provider: IProvider; + asClassProvider(value: Class, scope?: IScope): void; + asFactoryProvider(factory: Function): void; + asValueProvider(value: T): void; +} + +export interface IContainer { + register(key: Class | string): IContainerEntry; + resolve(key: Class | string): T; + generateKey(key: any): string; +} + +export const Injectable = (name?: string, scope: Class = TransientScope): ClassDecorator => (target: TFunction): void => { + makeTargetInjectable(target, name, scope); +} + +export const Inject = (name: string): ParameterDecorator => (target: Object, propertyKey: string | symbol, parameterIndex: number): void => { + const keys: any = Reflect.getMetadata('injection:keys', target) ?? []; + keys.push({ key: name, index: parameterIndex }); + Reflect.defineMetadata('injection:keys', keys, target); +} \ No newline at end of file diff --git a/src/ioc/scopes.ts b/src/ioc/scopes.ts new file mode 100644 index 0000000..c01edc2 --- /dev/null +++ b/src/ioc/scopes.ts @@ -0,0 +1,42 @@ +import { IClassProvider, IContainer, IProvider, IScope } from "./ioc.interfaces"; + +export class SingletonScope implements IScope { + + private instance: any; + + public resolve(provider: IClassProvider, container: IContainer): T { + if(this.instance !== undefined) + return this.instance; + const parameterTypes: any[] = Reflect.getMetadata('design:paramtypes', provider.class) ?? []; + const injectionKeys: any[] = Reflect.getMetadata('injection:keys', provider.class) ?? []; + const resolvedParameters: any[] = []; + parameterTypes.forEach((parameter: any, index: number) => { + const key: string = injectionKeys.find((value: any) => value.index === index)?.key; + const resolved: any = container.resolve(key ?? parameter); + if(resolved === undefined) + throw new Error(`Couldn't resolve parameter "${parameter}"!`); + resolvedParameters.push(resolved); + }); + this.instance = new provider.class(...resolvedParameters); + return this.instance; + } + +} + +export class TransientScope implements IScope { + + public resolve(provider: IClassProvider, container: IContainer): T { + const parameterTypes: any[] = Reflect.getMetadata('design:paramtypes', provider.class) ?? []; + const injectionKeys: any[] = Reflect.getMetadata('injection:keys', provider.class) ?? []; + const resolvedParameters: any[] = []; + parameterTypes.forEach((parameter: any, index: number) => { + const key: string = injectionKeys.find((value: any) => value.index === index)?.key; + const resolved: any = container.resolve(key ?? parameter); + if(resolved === undefined) + throw new Error(`Couldn't resolve parameter "${parameter}"!`); + resolvedParameters.push(resolved); + }); + return new provider.class(...resolvedParameters); + } + +} \ No newline at end of file diff --git a/src/ioc/util.ts b/src/ioc/util.ts new file mode 100644 index 0000000..501e517 --- /dev/null +++ b/src/ioc/util.ts @@ -0,0 +1,19 @@ +import { Class } from "../types"; +import { IProvider, IScope } from "./ioc.interfaces"; + +export function makeTargetInjectable(target: any, name: string, scope: Class) { + Reflect.defineMetadata('injectable:name', name ?? target.name, target); + Reflect.defineMetadata('injectable:scope', scope, target); +} + +export function isClassProvider(provider: IProvider): boolean { + return (provider).class !== undefined && (provider).scope !== undefined; +} + +export function isFactoryProvider(provider: IProvider): boolean { + return (provider).factory !== undefined; +} + +export function isValueProvider(provider: IProvider): boolean { + return (provider).value !== undefined; +} \ No newline at end of file diff --git a/src/module/module.interfaces.ts b/src/module/module.interfaces.ts new file mode 100644 index 0000000..a71e01a --- /dev/null +++ b/src/module/module.interfaces.ts @@ -0,0 +1,30 @@ +import { IClassProvider, IFactoryProvider, IProvider, IValueProvider } from "../ioc/ioc.interfaces"; +import { Class } from "../types"; + + +export interface IModuleOptions { + imports?: Class[], + controllers?: Class[], + providers?: (Class | IClassProvider | IFactoryProvider | IValueProvider)[], +} + +export interface IModule { + name: string, + moduleClass: Class, + options: IModuleOptions, +} + +export interface IModuleInitialize { + onInitialization(): Promise; +} + +export const Module = (options: IModuleOptions): ClassDecorator => (target: TFunction): void => { + const defaultOptions: IModuleOptions = { imports: [], controllers: [], providers: [] }; + let mergedOptions: IModuleOptions = {}; + + Object.assign(mergedOptions, defaultOptions); + Object.assign(mergedOptions, options); + + Reflect.defineMetadata('module:name', target.name, target); + Reflect.defineMetadata('module:options', mergedOptions, target); +} \ No newline at end of file diff --git a/src/mooncake.ts b/src/mooncake.ts new file mode 100644 index 0000000..7796e88 --- /dev/null +++ b/src/mooncake.ts @@ -0,0 +1,18 @@ +import { IApplication, IApplicationOptions } from "./application/application.interfaces"; +import { IModule, IModuleOptions } from "./module/module.interfaces"; +import { Class } from './types'; + +export class Mooncake { + + public static async createApp(applicationClass: Class, mainModule: Class, options: IApplicationOptions): Promise { + const moduleName: string = Reflect.getMetadata('module:name', mainModule); + const moduleOptions: IModuleOptions = Reflect.getMetadata('module:options', mainModule); + if(moduleName === undefined || moduleOptions === undefined) + throw new Error(`Invalid module provided!`); + const module: IModule = { name: moduleName, moduleClass: mainModule, options: moduleOptions }; + const app: IApplication = new applicationClass(module, options); + await app.onInitialization(); + return app; + } + +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2bf067c --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +export type Class = { new(...args: any[]): T; }; +export interface Type extends Function { + new (...args: any[]): T; +} \ No newline at end of file diff --git a/src/util/basic.logger.ts b/src/util/basic.logger.ts new file mode 100644 index 0000000..e2741e2 --- /dev/null +++ b/src/util/basic.logger.ts @@ -0,0 +1,22 @@ +import { Logger } from "tslog"; +import { ILogger } from "./logger.interfaces"; + +export class BasicLogger implements ILogger { + + private logger: Logger; + + constructor(name: string) { + this.logger = new Logger({ name: name }); + } + + public info(message: string, ...args: string[]): void { + this.logger.info(message, ...args); + } + public error(message: string, ...args: string[]): void { + this.logger.error(message, ...args); + } + public warn(message: string, ...args: string[]): void { + this.logger.warn(message, ...args); + } + +} \ No newline at end of file diff --git a/src/util/logger.interfaces.ts b/src/util/logger.interfaces.ts new file mode 100644 index 0000000..fb0cd3f --- /dev/null +++ b/src/util/logger.interfaces.ts @@ -0,0 +1,5 @@ +export interface ILogger { + info(message: string, ...args: string[]): void; + error(message: string, ...args: string[]): void; + warn(message: string, ...args: string[]): void; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 985f0c6..9b6709e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ @@ -18,14 +18,14 @@ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ + "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ + // "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ diff --git a/yarn.lock b/yarn.lock index 36cc0a0..c9a9e03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1154,6 +1154,11 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" @@ -1253,7 +1258,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -source-map-support@^0.5.12, source-map-support@^0.5.17: +source-map-support@^0.5.12, source-map-support@^0.5.17, source-map-support@^0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -1435,6 +1440,13 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslog@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.0.tgz#4982c289a8948670d6a1c49c29977ae9f861adfa" + integrity sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A== + dependencies: + source-map-support "^0.5.19" + tsutils@^3.17.1: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"