Skip to content

Commit

Permalink
Implemented everything to a working state
Browse files Browse the repository at this point in the history
  • Loading branch information
Drischdaan committed Jul 8, 2021
1 parent 305d763 commit cfdb9ad
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 7 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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 }}
11 changes: 11 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
src/
test/
tsconfig.json
jest.config.json
*.js.map
.eslintrc.js
typings
.vscode
.prettierrc
.gitignore
yarn.lock
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</p>

# mooncake
Component based app framework
NestJs and Angular inspired application framework

## Support

Expand Down
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand All @@ -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"
}
}
12 changes: 12 additions & 0 deletions src/application/application.interfaces.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
onStart(): Promise<void>;
onStop(): Promise<void>;
}
97 changes: 97 additions & 0 deletions src/application/application.ts
Original file line number Diff line number Diff line change
@@ -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<any>[];
protected readonly providers: (Class<any> | IProvider<any>)[];

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<void> {
this.internalLogger.info('Initializing', this.options.name, '...');
this.internalLogger.info('Preparing container providers...');
await this.registerModule(this.mainModule);
}

public async onStart(): Promise<void> {

}

public async onStop(): Promise<void> {

}

private async registerModule(module: IModule): Promise<void> {
await this.registerProviders(module);
await this.registerControllers(module);
await this.registerImports(module);
}

private async registerProviders(module: IModule): Promise<void> {
for(const provider of module.options.providers) {
if(isClassProvider(<any>provider)) {
const classProvider: IClassProvider<any> = <IClassProvider<any>>provider;
this.container.register(classProvider.key).asClassProvider(classProvider.class, classProvider.scope);
} else if(isFactoryProvider(<any>provider)) {
const factoryProvider: IFactoryProvider<any> = <IFactoryProvider<any>>provider;
this.container.register(factoryProvider.key).asFactoryProvider(factoryProvider.factory);
} else if(isValueProvider(<any>provider)) {
const valueProvider: IValueProvider<any> = <IValueProvider<any>>provider;
this.container.register(valueProvider.key).asValueProvider(valueProvider.value);
} else {
const providerName: string = Reflect.getMetadata('injectable:name', provider);
const providerScope: Class<IScope> = Reflect.getMetadata('injectable:scope', provider);
if(providerName === undefined || providerScope === undefined)
throw new Error(`Invalid provider found: ${provider}`);
this.providers.push(provider);
this.container.register(<Class<any>>provider).asClassProvider(<Class<any>>provider, new providerScope());
}
}
}

private async registerControllers(module: IModule): Promise<void> {
for(const controller of module.options.controllers) {
const controllerName: string = Reflect.getMetadata('injectable:name', controller);
const controllerScope: Class<IScope> = 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<void> {
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);
}
}

}
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
80 changes: 80 additions & 0 deletions src/ioc/container.ts
Original file line number Diff line number Diff line change
@@ -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<T> implements IContainerEntry<T> {

public provider: IProvider<T>;

constructor(
public key: string,
) {}

public asClassProvider(value: Class<T>, scope: IScope = new TransientScope()): void {
this.provider = <IClassProvider<T>>{
key: this.key,
class: value,
scope: scope,
};
}

public asFactoryProvider(value: Function): void {
this.provider = <IFactoryProvider<T>>{
key: this.key,
factory: value
};
}

public asValueProvider(value: T): void {
this.provider = <IValueProvider<T>>{
key: this.key,
value: value
};
}

}

export class IocContainer implements IContainer {

private readonly entries: IContainerEntry<any>[];

constructor() {
this.entries = new Array<IContainerEntry<any>>();
}

public register<T>(key: Class<T> | string): IContainerEntry<T> {
const generatedKey: string = this.generateKey(key);
const entries: IContainerEntry<any>[] = this.entries.filter((entry: IContainerEntry<any>) => entry.provider.key === generatedKey);
if(entries.length !== 0)
throw new Error(`There is already an entry with that key!`);
let entry: IContainerEntry<T> = new ContainerEntry<T>(generatedKey);
this.entries.push(entry);
return entry;
}

public resolve<T>(key: string | Class<T>): T {
const generatedKey: string = this.generateKey(key);
const entry: IContainerEntry<any> = this.entries.find((entry: IContainerEntry<any>) => entry.provider.key === generatedKey);
if(entry === undefined)
throw new Error(`There is no entry with that key: ${key}`);
if(isClassProvider(entry.provider))
return (<IClassProvider<T>>entry.provider).scope.resolve<T>((<IClassProvider<T>>entry.provider), this);
else if(isFactoryProvider(entry.provider))
return (<IFactoryProvider<T>>entry.provider).factory();
else if(isValueProvider(entry.provider))
return (<IValueProvider<T>>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;
}

}
47 changes: 47 additions & 0 deletions src/ioc/ioc.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Class } from "../types";
import { TransientScope } from "./scopes";
import { makeTargetInjectable } from "./util";

export interface IScope {
resolve<T>(provider: IClassProvider<T>, container: IContainer): T;
}

export interface IProvider<T> {
key: string,
}

export interface IFactoryProvider<T> extends IProvider<T> {
factory: Function,
}

export interface IClassProvider<T> extends IProvider<T> {
class: Class<T>,
scope: IScope,
}

export interface IValueProvider<T> extends IProvider<T> {
value: T,
}

export interface IContainerEntry<T> {
provider: IProvider<T>;
asClassProvider(value: Class<T>, scope?: IScope): void;
asFactoryProvider(factory: Function): void;
asValueProvider(value: T): void;
}

export interface IContainer {
register<T>(key: Class<T> | string): IContainerEntry<T>;
resolve<T>(key: Class<T> | string): T;
generateKey(key: any): string;
}

export const Injectable = (name?: string, scope: Class<IScope> = TransientScope): ClassDecorator => <TFunction extends Function>(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);
}
42 changes: 42 additions & 0 deletions src/ioc/scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { IClassProvider, IContainer, IProvider, IScope } from "./ioc.interfaces";

export class SingletonScope implements IScope {

private instance: any;

public resolve<T>(provider: IClassProvider<T>, 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<T>(provider: IClassProvider<T>, 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);
}

}
Loading

0 comments on commit cfdb9ad

Please sign in to comment.