├── src ├── providers │ ├── ClassProvider │ │ ├── index.ts │ │ ├── ClassProvider.types.ts │ │ ├── ClassProvider.spec.ts │ │ └── ClassProvider.ts │ ├── ValueProvider │ │ ├── index.ts │ │ └── ValueProvider.ts │ ├── FactoryProvider │ │ ├── index.ts │ │ ├── FactoryProvider.types.ts │ │ └── FactoryProvider.ts │ ├── AbstractProvider.ts │ └── index.ts ├── core │ ├── Registration.types.ts │ ├── AbstractModule.ts │ ├── Container.types.ts │ ├── index.ts │ ├── AbstractModule.spec.ts │ ├── Registration.ts │ ├── Registry.ts │ ├── Registration.spec.ts │ ├── ResolutionContext.spec.ts │ ├── ResolutionContext.ts │ ├── Container.ts │ └── Container.spec.ts ├── errors │ ├── index.ts │ ├── ValueOrRegistrationIdRequiredError.ts │ ├── NotRegisteredError.ts │ └── AlreadyRegisteredError.ts ├── builder │ ├── RegistrationBuilder.types.ts │ ├── RegistrationBuilder.spec.ts │ ├── RegistrationBuilder.ts │ └── syntax │ │ ├── InSyntax.spec.ts │ │ ├── InSyntax.ts │ │ ├── AsSyntax.spec.ts │ │ └── AsSyntax.ts ├── types.ts ├── index.ts ├── Microinjection.spec.ts └── Microinjection.ts ├── docs └── assets │ └── logo.png ├── tsconfig.build.json ├── jest.config.js ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── .github └── workflows │ ├── build-and-test.yml │ ├── npm-publish.yml │ └── codeql-analysis.yml ├── LICENSE ├── package.json ├── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── yarn.lock /src/providers/ClassProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { ClassProvider } from "./ClassProvider"; -------------------------------------------------------------------------------- /src/providers/ValueProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { ValueProvider } from "./ValueProvider"; -------------------------------------------------------------------------------- /src/providers/FactoryProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { FactoryProvider } from "./FactoryProvider"; -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microkits/microinjection/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "**/*.spec.ts" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/core/Registration.types.ts: -------------------------------------------------------------------------------- 1 | import { AnyClass } from "../types"; 2 | 3 | export type RegistrationId = string | symbol | AnyClass; -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { AlreadyRegisteredError } from "./AlreadyRegisteredError"; 2 | export { NotRegisteredError } from "./NotRegisteredError"; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | testEnvironment: 'node', 4 | preset: 'ts-jest', 5 | coveragePathIgnorePatterns: [ 6 | "index.ts" 7 | ] 8 | }; -------------------------------------------------------------------------------- /src/builder/RegistrationBuilder.types.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "../core"; 2 | 3 | export interface OnRegistrationReady { 4 | (registration: Registration): void; 5 | } -------------------------------------------------------------------------------- /src/core/AbstractModule.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./Container"; 2 | 3 | export abstract class AbstractModule { 4 | abstract configure(container: Container): Promise | void; 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/FactoryProvider/FactoryProvider.types.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionContext } from "../../core/ResolutionContext"; 2 | 3 | export interface Factory { 4 | (context: ResolutionContext): T; 5 | } -------------------------------------------------------------------------------- /src/providers/AbstractProvider.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionContext } from "../core/ResolutionContext"; 2 | 3 | export abstract class AbstractProvider { 4 | abstract resolve(context: ResolutionContext): T; 5 | } -------------------------------------------------------------------------------- /src/core/Container.types.ts: -------------------------------------------------------------------------------- 1 | import { Scope } from "../types"; 2 | import { Container } from "./Container"; 3 | 4 | export interface ContainerOptions { 5 | parent?: Container; 6 | defaultScope?: Scope; 7 | defaultStrict?: boolean; 8 | } -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export { ClassProvider } from "./ClassProvider"; 2 | export { FactoryProvider } from "./FactoryProvider"; 3 | export { ValueProvider } from "./ValueProvider"; 4 | export { AbstractProvider } from "./AbstractProvider"; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ConcreteClass = ( 2 | new (...args: unknown[]) => T 3 | ); 4 | 5 | export type AnyClass = ( 6 | abstract new (...args: unknown[]) => T 7 | ); 8 | 9 | export enum Scope { 10 | TRANSIENT, SINGLETON, CONTEXT 11 | } 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS metadata 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # Ignore built ts files 6 | dist/**/* 7 | 8 | # Dependency directory 9 | node_modules 10 | 11 | #Logs 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Coverage directory 16 | coverage 17 | 18 | # Build directory 19 | dist -------------------------------------------------------------------------------- /src/errors/ValueOrRegistrationIdRequiredError.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationId } from "src/core/Registration.types"; 2 | 3 | export class ValueOrRegistrationIdRequiredError extends Error { 4 | constructor(name: string) { 5 | super(`${name}: You must provide a value or a registration id`); 6 | } 7 | } -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { AbstractModule } from "./AbstractModule"; 2 | export { Container } from "./Container"; 3 | export { Registration } from "./Registration"; 4 | export { Registry } from "./Registry"; 5 | export { ResolutionContext } from "./ResolutionContext"; 6 | export { RegistrationId } from "./Registration.types"; -------------------------------------------------------------------------------- /src/errors/NotRegisteredError.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationId } from "src/core/Registration.types"; 2 | 3 | /* `NotRegisteredError` is an error that is thrown when a registration id is not registered */ 4 | export class NotRegisteredError extends Error { 5 | constructor(id: RegistrationId) { 6 | super(`${id.toString()} is not registered.`); 7 | } 8 | } -------------------------------------------------------------------------------- /src/errors/AlreadyRegisteredError.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationId } from "src/core/Registration.types"; 2 | 3 | /* `AlreadyRegisteredError` is an error that is thrown when a registration id is already registered */ 4 | export class AlreadyRegisteredError extends Error { 5 | constructor(id: RegistrationId) { 6 | super(`${id.toString()} is already registered.`); 7 | } 8 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | AbstractModule, 3 | Container, 4 | ResolutionContext, 5 | RegistrationId 6 | } from "./core"; 7 | 8 | export { 9 | AbstractProvider, 10 | ClassProvider, 11 | FactoryProvider, 12 | ValueProvider 13 | } from "./providers" 14 | 15 | export { Scope } from "./types"; 16 | 17 | export { Microinjection } from "./Microinjection"; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "baseUrl": ".", 5 | "module": "commonjs", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "noImplicitAny": false, 10 | "removeComments": true, 11 | "noLib": false, 12 | "target": "es2019", 13 | "sourceMap": true 14 | } 15 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ["**/*.spec.ts"], 4 | parser: '@typescript-eslint/parser', 5 | plugins: [ 6 | '@typescript-eslint', 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | rules: { 13 | "@typescript-eslint/explicit-module-boundary-types": "off" 14 | } 15 | }; -------------------------------------------------------------------------------- /src/providers/ValueProvider/ValueProvider.ts: -------------------------------------------------------------------------------- 1 | import { AbstractProvider } from "../AbstractProvider"; 2 | 3 | export class ValueProvider extends AbstractProvider { 4 | private readonly _value: T; 5 | 6 | constructor(value: T) { 7 | super(); 8 | this._value = value; 9 | } 10 | 11 | /** 12 | * It returns the value stored on provider 13 | * @returns The value stored. 14 | */ 15 | resolve(): T { 16 | return this._value 17 | } 18 | } -------------------------------------------------------------------------------- /src/providers/ClassProvider/ClassProvider.types.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationId } from "../../core/Registration.types"; 2 | 3 | export type ClassDependency = RegistrationId | { 4 | inject?: RegistrationId; 5 | required?: boolean; 6 | value?: unknown; 7 | } 8 | 9 | export interface ClassProperty { 10 | name: string | symbol; 11 | inject: RegistrationId; 12 | required?: boolean; 13 | value?: unknown; 14 | } 15 | 16 | export interface ClassProviderOptions { 17 | properties?: ClassProperty[]; 18 | dependencies?: ClassDependency[]; 19 | } -------------------------------------------------------------------------------- /src/providers/ClassProvider/ClassProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValueOrRegistrationIdRequiredError } from "../../errors/ValueOrRegistrationIdRequiredError"; 2 | import { ClassProvider } from "./ClassProvider"; 3 | 4 | describe("ClassProvider", () => { 5 | it("should throw an error if neither a value nor a registration is provided", () => { 6 | class A { } 7 | 8 | const provider = new ClassProvider(A, { 9 | dependencies: [{}] 10 | }); 11 | 12 | expect(() => { 13 | provider.resolve(null); 14 | }).toThrowError(ValueOrRegistrationIdRequiredError); 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/core/AbstractModule.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractModule } from "./AbstractModule"; 2 | import { Container } from "./Container"; 3 | 4 | describe("AbstractModule", () => { 5 | it("should create a Module", () => { 6 | const handler = jest.fn(); 7 | 8 | class DependencyModule extends AbstractModule { 9 | configure(container: Container) { 10 | handler(container); 11 | } 12 | } 13 | 14 | const container = new Container(); 15 | container.addModule(new DependencyModule()); 16 | expect(handler).toBeCalled(); 17 | expect(handler).toBeCalledWith(container); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/builder/RegistrationBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import { RegistrationBuilder } from "./RegistrationBuilder"; 2 | 3 | describe("RegistrationBuilder", () => { 4 | it("should never call onReady", () => { 5 | const onReady = jest.fn(); 6 | const builder = new RegistrationBuilder("test"); 7 | 8 | builder.start(onReady); 9 | 10 | expect(onReady).not.toBeCalled(); 11 | }); 12 | 13 | it("should call onReady when a registration become ready", () => { 14 | const onReady = jest.fn(); 15 | const builder = new RegistrationBuilder("test"); 16 | 17 | builder.start(onReady).asValue(123); 18 | 19 | expect(onReady).toBeCalled(); 20 | }); 21 | }); -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x, 16.x, 18.x] 18 | 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install dependencies 29 | run: yarn --frozen-lockfile 30 | 31 | - name: Run lint 32 | run: yarn lint 33 | 34 | - name: Run tests 35 | run: yarn test 36 | 37 | - name: Run build 38 | run: yarn build 39 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-publish: 9 | name: Build and Publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install dependencies 22 | run: yarn --frozen-lockfile 23 | 24 | - name: Run tests 25 | run: yarn test 26 | 27 | - name: Build package 28 | run: yarn build 29 | 30 | - name: Publish package 31 | run: yarn publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | -------------------------------------------------------------------------------- /src/builder/RegistrationBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "../core"; 2 | import { RegistrationId } from "../core/Registration.types"; 3 | import { Scope } from "../types"; 4 | import { OnRegistrationReady } from "./RegistrationBuilder.types"; 5 | import { AsSyntax } from "./syntax/AsSyntax"; 6 | 7 | export class RegistrationBuilder { 8 | private readonly registration: Registration; 9 | 10 | constructor(id: RegistrationId, scope?: Scope) { 11 | this.registration = new Registration(id, scope); 12 | } 13 | 14 | start(onReady: OnRegistrationReady): AsSyntax { 15 | const registration = new Proxy(this.registration, { 16 | set: (target, ...args) => { 17 | const success = Reflect.set(target, ...args); 18 | 19 | if (target.isReady()) 20 | onReady(this.registration); 21 | 22 | return success; 23 | } 24 | }); 25 | 26 | return new AsSyntax(registration); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/FactoryProvider/FactoryProvider.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionContext } from "../../core/ResolutionContext"; 2 | import { AbstractProvider } from "../AbstractProvider"; 3 | import { Factory } from "./FactoryProvider.types"; 4 | 5 | export class FactoryProvider extends AbstractProvider { 6 | private readonly _factory: Factory; 7 | 8 | constructor(factory: Factory) { 9 | super(); 10 | this._factory = factory; 11 | } 12 | 13 | /** 14 | * "The resolve function returns the result of calling the factory function with the context." 15 | * 16 | * The factory function is the function that was passed to the constructor. The context is the object 17 | * that was passed to the resolve function 18 | * @param {ResolutionContext} context - The context of the current resolution. 19 | * @returns A function that takes a context and returns a value. 20 | */ 21 | resolve(context: ResolutionContext): T { 22 | return this._factory(context) 23 | } 24 | } -------------------------------------------------------------------------------- /src/Microinjection.spec.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./core"; 2 | import { Microinjection } from "./Microinjection"; 3 | 4 | describe("Container", () => { 5 | it("should get the default container", () => { 6 | const container = Microinjection.getDefaultContainer(); 7 | expect(container).toBeInstanceOf(Container); 8 | }); 9 | 10 | it("should create a child container", () => { 11 | const defaultContainer = Microinjection.getDefaultContainer(); 12 | const container = Microinjection.createContainer(); 13 | 14 | expect(container.parent).toBe(defaultContainer); 15 | }); 16 | 17 | it("should create two different containers", () => { 18 | const defaultContainer = Microinjection.getDefaultContainer(); 19 | const container1 = Microinjection.createContainer(); 20 | const container2 = Microinjection.createContainer(); 21 | 22 | expect(container1.parent).toBe(defaultContainer); 23 | expect(container2.parent).toBe(defaultContainer); 24 | expect(container1).not.toBe(container2); 25 | }) 26 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Microkits 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 | -------------------------------------------------------------------------------- /src/Microinjection.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./core/Container"; 2 | 3 | export class Microinjection { 4 | private static container: Container; 5 | 6 | /** 7 | * Return the default container. If the container is null, create a new container and return it. 8 | * @returns The default container. 9 | */ 10 | static getDefaultContainer(): Container { 11 | if (this.container == null) { 12 | this.container = new Container(); 13 | } 14 | 15 | return this.container; 16 | } 17 | 18 | /** 19 | * "Create a new container, and if a parent container is provided, make it the parent of the new 20 | * container." 21 | * 22 | * The first thing we do is check if a parent container was provided. If not, we get the default 23 | * container 24 | * @param {Container} [parent] - The parent container to use. If not specified, the default container 25 | * will be used. 26 | * @returns A new instance of the Container class. 27 | */ 28 | static createContainer(parent?: Container): Container { 29 | if (parent == null) { 30 | parent = this.getDefaultContainer(); 31 | } 32 | 33 | return new Container(parent); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@microkits/microinjection", 3 | "version": "2.1.1", 4 | "description": "An inversion of control container for build better Node.js applications.", 5 | "author": { 6 | "name": "Guilherme Covre Dalleprane", 7 | "email": "guilherme.dalleprane@gmail.com" 8 | }, 9 | "license": "MIT", 10 | "repository": "microkits/microinjection", 11 | "main": "dist/index.js", 12 | "typings": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc --build tsconfig.build.json", 18 | "test": "jest --color", 19 | "lint": "eslint src --ext .ts" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^28.1.0", 23 | "@types/node": "^14.14.20", 24 | "@typescript-eslint/eslint-plugin": "^4.33.0", 25 | "@typescript-eslint/parser": "^4.33.0", 26 | "eslint": "^7.32.0", 27 | "jest": "^28.1.0", 28 | "ts-jest": "^28.0.3", 29 | "tsc": "^2.0.3", 30 | "typescript": "^4.7.3" 31 | }, 32 | "keywords": [ 33 | "dependency injection", 34 | "dependency", 35 | "injection", 36 | "ioc", 37 | "container", 38 | "javascript", 39 | "typescript" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/builder/syntax/InSyntax.spec.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "../../core" 2 | import { Scope } from "../../types"; 3 | import { InSyntax } from "./InSyntax"; 4 | 5 | describe("InSyntax", () => { 6 | it("should set a scope for a registration", () => { 7 | const registration = new Registration(""); 8 | 9 | new InSyntax(registration).inScope(Scope.CONTEXT); 10 | 11 | expect(registration.scope).toBe(Scope.CONTEXT); 12 | }); 13 | 14 | it("should set a singleton scope for a registration", () => { 15 | const registration = new Registration(""); 16 | 17 | new InSyntax(registration).inSingletonScope(); 18 | 19 | expect(registration.scope).toBe(Scope.SINGLETON); 20 | }); 21 | 22 | it("should set a transient scope for a registration", () => { 23 | const registration = new Registration(""); 24 | 25 | new InSyntax(registration).inTransientScope(); 26 | 27 | expect(registration.scope).toBe(Scope.TRANSIENT); 28 | }); 29 | 30 | it("should set a context scope for a registration", () => { 31 | const registration = new Registration(""); 32 | 33 | new InSyntax(registration).inContextScope(); 34 | 35 | expect(registration.scope).toBe(Scope.CONTEXT); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/builder/syntax/InSyntax.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "../../core"; 2 | import { Scope } from "../../types"; 3 | 4 | export class InSyntax { 5 | private readonly registration: Registration; 6 | 7 | constructor(registration: Registration) { 8 | this.registration = registration; 9 | } 10 | 11 | /** 12 | * This function sets the scope of the registration to the given scope. 13 | * @param {Scope} scope - Scope - The scope of the registration. 14 | * @returns This instance of RegistrationBuilder . 15 | */ 16 | public inScope(scope: Scope): void { 17 | this.registration.scope = scope; 18 | } 19 | /** 20 | * This function sets the scope of the registration to the Scope.SINGLETON 21 | * @returns This instance of RegistrationBuilder . 22 | */ 23 | public inSingletonScope(): void { 24 | return this.inScope(Scope.SINGLETON); 25 | } 26 | 27 | /** 28 | * This function sets the scope of the registration to the Scope.TRANSIENT 29 | * @returns This instance of RegistrationBuilder . 30 | */ 31 | public inTransientScope(): void { 32 | return this.inScope(Scope.TRANSIENT); 33 | } 34 | 35 | /** 36 | * This function returns a new RegistrationBuilder with the scope set to Scope.CONTEXT. 37 | * @returns A RegistrationBuilder 38 | */ 39 | public inContextScope(): void { 40 | return this.inScope(Scope.CONTEXT) 41 | } 42 | } -------------------------------------------------------------------------------- /src/builder/syntax/AsSyntax.spec.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "../../core" 2 | import { AbstractProvider, ClassProvider, FactoryProvider, ValueProvider } from "../../providers"; 3 | import { AsSyntax } from "./AsSyntax"; 4 | 5 | describe("AsSyntax", () => { 6 | it("should set a ClassProvider for a registration", () => { 7 | const registration = new Registration(""); 8 | 9 | new AsSyntax(registration) 10 | .asClass((class Test { })); 11 | 12 | expect(registration.provider).toBeInstanceOf(ClassProvider); 13 | }); 14 | 15 | it("should set a ValueProvider for a registration", () => { 16 | const registration = new Registration(""); 17 | 18 | new AsSyntax(registration) 19 | .asValue(123); 20 | 21 | expect(registration.provider).toBeInstanceOf(ValueProvider); 22 | }); 23 | 24 | it("should set a FactoryProvider for a registration", () => { 25 | const registration = new Registration(""); 26 | 27 | new AsSyntax(registration) 28 | .asFactory(() => 123) 29 | 30 | expect(registration.provider).toBeInstanceOf(FactoryProvider); 31 | }); 32 | 33 | it("should set a AbstractProvider for a registration", () => { 34 | const registration = new Registration(""); 35 | 36 | class Provider extends AbstractProvider { 37 | resolve() { 38 | return 123; 39 | } 40 | } 41 | 42 | new AsSyntax(registration) 43 | .asProvider(new Provider()); 44 | 45 | expect(registration.provider).toBeInstanceOf(AbstractProvider); 46 | expect(registration.provider).toBeInstanceOf(Provider); 47 | }); 48 | }); -------------------------------------------------------------------------------- /src/core/Registration.ts: -------------------------------------------------------------------------------- 1 | import { AbstractProvider } from "../providers/AbstractProvider"; 2 | import { Scope } from "../types"; 3 | import { RegistrationId } from "./Registration.types"; 4 | import { ResolutionContext } from "./ResolutionContext"; 5 | 6 | export class Registration { 7 | public readonly id: RegistrationId; 8 | public provider: AbstractProvider; 9 | public scope: Scope; 10 | public instance: T; 11 | 12 | constructor(id: RegistrationId, scope?: Scope) { 13 | this.id = id; 14 | this.scope = scope; 15 | } 16 | 17 | /** 18 | * It deletes the instance cached on registration 19 | */ 20 | clearCache(): void { 21 | delete this.instance; 22 | } 23 | 24 | /** 25 | * If the scope is singleton, then return the instance if it exists, otherwise ask a 26 | * new instance from provider and return it 27 | * If the scope is not singleton, then ask a provider to return a new instance 28 | * @param {ResolutionContext} [context] - The resolution context. 29 | * @returns The instance of the class that is being resolved. 30 | */ 31 | resolve(context?: ResolutionContext): T { 32 | if (this.scope == Scope.SINGLETON) { 33 | if (this.instance == null) { 34 | this.instance = this.provider.resolve(context); 35 | } 36 | 37 | return this.instance; 38 | } 39 | 40 | return this.provider.resolve(context); 41 | } 42 | 43 | /** 44 | * It checks if the registration is ready to be resolved. 45 | * @returns A boolean value. 46 | */ 47 | isReady(): boolean { 48 | return this.provider != null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/core/Registry.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Registration } from "./Registration"; 3 | import { RegistrationId } from "./Registration.types"; 4 | 5 | export class Registry { 6 | private readonly data: Map; 7 | 8 | constructor() { 9 | this.data = new Map(); 10 | } 11 | 12 | /** 13 | * Return true if the given id exists. 14 | * @param {RegistrationId} id - RegistrationId - The id of the registration to check for. 15 | * @returns A boolean value. 16 | */ 17 | has(id: RegistrationId): boolean { 18 | return this.data.has(id); 19 | } 20 | 21 | /** 22 | * It returns the value associated with the given key 23 | * @param {RegistrationId} id - RegistrationId - The id of the registration you want to get. 24 | * @returns Registration 25 | */ 26 | get(id: RegistrationId): Registration { 27 | return this.data.get(id) as Registration; 28 | } 29 | 30 | /** 31 | * It takes a Registration and saves it 32 | * @param registration - Registration 33 | */ 34 | save(registration: Registration): void { 35 | this.data.set(registration.id, registration); 36 | } 37 | 38 | /** 39 | * Delete the registration with the given id, and return true if it was found and deleted, or false if 40 | * it was not found. 41 | * @param {RegistrationId} id - The id of the registration to delete. 42 | * @returns A boolean value indicating whether the registration was deleted. 43 | */ 44 | delete(id: RegistrationId): boolean { 45 | return this.data.delete(id); 46 | } 47 | 48 | /** 49 | * It clears the registry 50 | */ 51 | clear(): void { 52 | this.data.clear() 53 | } 54 | } -------------------------------------------------------------------------------- /src/core/Registration.spec.ts: -------------------------------------------------------------------------------- 1 | import { Scope } from "../types"; 2 | import { Registration } from "./Registration"; 3 | 4 | describe("Registration", () => { 5 | it("should create a Registration instance correctly", () => { 6 | const registration = new Registration("id", Scope.SINGLETON); 7 | 8 | expect(registration).toBeDefined(); 9 | expect(registration.id).toBe("id"); 10 | expect(registration.scope).toBe(Scope.SINGLETON); 11 | }); 12 | 13 | it("should be ready when provider is defined", () => { 14 | const registration = new Registration("id", Scope.SINGLETON); 15 | 16 | expect(registration.isReady()).toBe(false); 17 | 18 | registration.provider = { 19 | resolve() { 20 | return "" 21 | } 22 | } 23 | 24 | expect(registration.isReady()).toBe(true); 25 | }); 26 | 27 | it("should cache a instance when scope is SINGLETON", () => { 28 | const registration = new Registration("name", Scope.SINGLETON); 29 | 30 | registration.provider = { 31 | resolve() { 32 | return "anyvalue"; 33 | } 34 | } 35 | 36 | const resolved = registration.resolve(); 37 | expect(resolved).toBe("anyvalue"); 38 | expect(registration.instance).toBe("anyvalue"); 39 | }); 40 | 41 | it("should clear a cached instance", () => { 42 | const registration = new Registration("name", Scope.SINGLETON); 43 | 44 | registration.provider = { 45 | resolve() { 46 | return "anyvalue"; 47 | } 48 | } 49 | 50 | registration.resolve(); 51 | expect(registration.instance).toBe("anyvalue"); 52 | registration.clearCache(); 53 | expect(registration.instance).toBeUndefined(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/core/ResolutionContext.spec.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "./Container"; 2 | import { ResolutionContext } from "./ResolutionContext"; 3 | 4 | describe("ResolutionContext", () => { 5 | it("should create a resolution context", () => { 6 | const container = new Container(); 7 | const context = container.createResolutionContext(); 8 | 9 | expect(context).toBeInstanceOf(ResolutionContext); 10 | }); 11 | 12 | it("should create a child resolution context", () => { 13 | const container = new Container(); 14 | const context = container.createResolutionContext(); 15 | 16 | const child = context.createChildContext(); 17 | 18 | expect(child).toBeInstanceOf(ResolutionContext); 19 | expect(child.getParent()).toBe(context); 20 | }); 21 | 22 | it("should save instance to context cache", () => { 23 | const container = new Container(); 24 | 25 | class User { } 26 | 27 | container.register("user").asClass(User).inContextScope(); 28 | const context = container.createResolutionContext(); 29 | 30 | const resolved0 = container.resolve("user"); 31 | const resolved1 = context.resolve("user"); 32 | const resolved2 = context.resolve("user"); 33 | 34 | expect(resolved1).not.toBe(resolved0); 35 | expect(resolved2).not.toBe(resolved0); 36 | expect(resolved1).toBe(resolved2); 37 | }); 38 | 39 | it("should get instance from parent context cache", () => { 40 | const container = new Container(); 41 | 42 | class User { } 43 | 44 | container.register("user").asClass(User).inContextScope(); 45 | const parentContext = container.createResolutionContext(); 46 | const childContext = parentContext.createChildContext(); 47 | 48 | const resolved0 = container.resolve("user"); 49 | const resolved1 = parentContext.resolve("user"); 50 | const resolved2 = childContext.resolve("user"); 51 | 52 | expect(resolved1).not.toBe(resolved0); 53 | expect(resolved2).not.toBe(resolved0); 54 | expect(resolved1).toBe(resolved2); 55 | }); 56 | }) -------------------------------------------------------------------------------- /src/builder/syntax/AsSyntax.ts: -------------------------------------------------------------------------------- 1 | import { Registration } from "../../core"; 2 | import { AbstractProvider, ClassProvider, FactoryProvider, ValueProvider } from "../../providers"; 3 | import { ClassProviderOptions } from "../../providers/ClassProvider/ClassProvider.types"; 4 | import { Factory } from "../../providers/FactoryProvider/FactoryProvider.types"; 5 | import { ConcreteClass } from "../../types"; 6 | import { InSyntax } from "./InSyntax"; 7 | 8 | export class AsSyntax { 9 | private readonly registration: Registration; 10 | 11 | constructor(registration: Registration) { 12 | this.registration = registration; 13 | } 14 | 15 | /** 16 | * This function sets the provider property of the registration object to the provider argument. 17 | * @param provider - AbstractProvider 18 | * @returns RegistrationBuilder 19 | */ 20 | public asProvider(provider: AbstractProvider): InSyntax { 21 | this.registration.provider = provider; 22 | return new InSyntax(this.registration); 23 | } 24 | 25 | /** 26 | * "Register a ClassProvider." 27 | * 28 | * The first parameter is the class to register. The second parameter is an optional object that can be 29 | * used to configure the provider 30 | * @param constructor - The constructor function of the class to be instantiated. 31 | * @param {ClassProviderOptions} [options] - ClassProviderOptions 32 | * @returns A RegistrationBuilder 33 | */ 34 | public asClass(constructor: ConcreteClass, options?: ClassProviderOptions): InSyntax { 35 | const provider = new ClassProvider(constructor, options); 36 | return this.asProvider(provider); 37 | } 38 | 39 | /** 40 | * This function takes a value and returns a RegistrationBuilder that uses a ValueProvider to provide 41 | * that value. 42 | * @param {T} value - T - The value to be provided. 43 | * @returns RegistrationBuilder 44 | */ 45 | public asValue(value: T): InSyntax { 46 | const provider = new ValueProvider(value); 47 | return this.asProvider(provider); 48 | } 49 | 50 | /** 51 | * This function takes a factory function and returns a new provider that uses that factory function to 52 | * create the instance. 53 | * @param factory - Factory 54 | * @returns A RegistrationBuilder 55 | */ 56 | public asFactory(factory: Factory): InSyntax { 57 | const provider = new FactoryProvider(factory); 58 | return this.asProvider(provider); 59 | } 60 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microinjection 2 | 3 | ![Image of Microinjection](./docs/assets/logo.png) 4 | 5 | Connecting everything is a tedious part of app development. Microinjection aims to make development easier and faster by helping you write testable, loosely coupled, and maintainable applications. 6 | 7 | Microinjection is written entirely in TypeScript, but supports JavaScript as well. 8 | 9 | [![npm version](https://badge.fury.io/js/%40microkits%2Fmicroinjection.svg)](https://badge.fury.io/js/%40microkits%2Fmicroinjection) 10 | [![CodeQL](https://github.com/microkits/microinjection/workflows/CodeQL/badge.svg)](https://github.com/microkits/microinjection/actions?query=workflow%3ACodeQL) 11 | ## Features 12 | 13 | - Property Injection 14 | - Constructor Injection 15 | - Multiple DI containers 16 | - Registering and resolving dependencies hierarchically 17 | - Singleton, Transient, and Context scopes 18 | - Circular dependencies 19 | - Out-of-the-box support for injecting values, factories and classes. 20 | 21 | ## Installation 22 | 23 | Microinjection is available as a package on NPM. 24 | 25 | To install and save in your package.json dependencies, run the following command: 26 | 27 | Install with **npm**: 28 | ```sh 29 | npm install @microkits/microinjection 30 | ``` 31 | 32 | Install with **yarn**: 33 | ```sh 34 | yarn add @microkits/microinjection 35 | ``` 36 | 37 | ## Basic usage 38 | 39 | Containers are the main components of Microinjection. The first step to start using Microinjection is to getting a container. 40 | 41 | ```typescript 42 | import { Microinjection } from "@microkits/microinjection"; 43 | 44 | const container = Microinjection.getDefaultContainer(); 45 | ``` 46 | 47 | With an instance of the `Container` in hand, you can add your Registrations. 48 | 49 | ```typescript 50 | class Cat { 51 | speak() { 52 | console.log("meooow!") 53 | } 54 | } 55 | 56 | class CatOwner { 57 | cat: Cat; 58 | } 59 | 60 | container.register("Cat").asClass(Cat); 61 | container.register("CatOwner").asClass(CatOwner, { 62 | properties: [{ 63 | name: "cat", 64 | inject: "Cat" 65 | }] 66 | }); 67 | ``` 68 | 69 | Now, you can request that the `Container` resolve a `Registration` for you. It will also resolve all necessary dependencies. 70 | 71 | ```typescript 72 | // Container will inject an instance of Cat on resolved CatOwner. 73 | const owner = container.resolve("CatOwner"); 74 | 75 | owner.cat.speak(); 76 | // logs "meooow!" to the console 77 | ``` 78 | -------------------------------------------------------------------------------- /src/providers/ClassProvider/ClassProvider.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionContext } from "../../core/ResolutionContext"; 2 | import { ValueOrRegistrationIdRequiredError } from "../../errors/ValueOrRegistrationIdRequiredError"; 3 | import { ConcreteClass } from "../../types"; 4 | import { AbstractProvider } from "../AbstractProvider"; 5 | import { ClassDependency, ClassProperty, ClassProviderOptions } from "./ClassProvider.types"; 6 | 7 | export class ClassProvider extends AbstractProvider { 8 | private target: ConcreteClass; 9 | private properties?: ClassProperty[]; 10 | private dependencies?: ClassDependency[]; 11 | 12 | constructor(target: ConcreteClass, options?: ClassProviderOptions) { 13 | super(); 14 | this.target = target; 15 | this.properties = options?.properties; 16 | this.dependencies = options?.dependencies; 17 | } 18 | 19 | /** 20 | * It creates an instance of the target class, injects the dependencies into the constructor, and 21 | * then injects the properties 22 | * @param {ResolutionContext} context - The context of the resolution. 23 | * @returns An instance of the target class with all of the dependencies and properties resolved. 24 | */ 25 | resolve(context: ResolutionContext): T { 26 | const params = []; 27 | 28 | this.dependencies?.forEach((dependency) => { 29 | if (typeof dependency !== "object") { 30 | dependency = { 31 | inject: dependency 32 | } 33 | } 34 | 35 | if (dependency.value == null) { 36 | if (dependency.inject == null) { 37 | throw new ValueOrRegistrationIdRequiredError(this.target.name); 38 | } 39 | 40 | dependency.value = context.resolve( 41 | dependency.inject, 42 | dependency.required 43 | ); 44 | } 45 | params.push(dependency.value); 46 | }); 47 | 48 | const instance = Reflect.construct(this.target, params); 49 | 50 | this.properties?.forEach((property) => { 51 | let cachedValue = property.value; 52 | 53 | Object.defineProperty(instance, property.name, { 54 | enumerable: true, 55 | configurable: true, 56 | get: () => { 57 | if (cachedValue == null) { 58 | cachedValue = context.resolve( 59 | property.inject, 60 | property.required 61 | ) 62 | } 63 | 64 | return cachedValue 65 | } 66 | }); 67 | }); 68 | 69 | return instance; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '24 16 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /src/core/ResolutionContext.ts: -------------------------------------------------------------------------------- 1 | import { NotRegisteredError } from "../errors/NotRegisteredError"; 2 | import { ClassProvider } from "../providers"; 3 | import { ClassProviderOptions } from "../providers/ClassProvider/ClassProvider.types"; 4 | import { ConcreteClass, Scope } from "../types"; 5 | import { Container } from "./Container"; 6 | import { RegistrationId } from "./Registration.types"; 7 | 8 | export class ResolutionContext { 9 | private readonly cache: Map; 10 | private readonly container: Container; 11 | private readonly parent?: ResolutionContext; 12 | 13 | constructor(container: Container, parent?: ResolutionContext) { 14 | this.cache = new Map(); 15 | this.container = container; 16 | this.parent = parent; 17 | } 18 | 19 | /** 20 | * It returns the parent of the current resolution context 21 | * @returns The parent of the current context. 22 | */ 23 | getParent(): ResolutionContext { 24 | return this.parent; 25 | } 26 | 27 | /** 28 | * If the current context has a saved instance for the given id, return it. Otherwise, ask the 29 | * parent context to return a saved instance. 30 | * @param {RegistrationId} id - The id of the registration. 31 | * @returns The saved instance of the registration id. 32 | */ 33 | private getSavedInstance(id: RegistrationId) { 34 | if (this.cache.has(id)) { 35 | return this.cache.get(id); 36 | } 37 | 38 | return this.getParent()?.getSavedInstance(id); 39 | } 40 | 41 | /** 42 | * It saves an value in the cache 43 | * @param {RegistrationId} id - The registration id of the instance. 44 | * @param {unknown} instance - The value that we want to register. 45 | */ 46 | private saveInstance(id: RegistrationId, instance: unknown) { 47 | this.cache.set(id, instance); 48 | } 49 | 50 | /** 51 | * Create a new resolution context that inherits from the current one. 52 | * @returns A new ResolutionContext object. 53 | */ 54 | createChildContext(): ResolutionContext { 55 | return new ResolutionContext(this.container, this); 56 | } 57 | 58 | /** 59 | * It creates a new instance of the given class, and returns it 60 | * @param target - The class to instantiate. 61 | * @param {ClassProviderOptions} [options] - { 62 | * @returns A new instance of the target class. 63 | */ 64 | instantiate(target: ConcreteClass, options?: ClassProviderOptions) { 65 | const provider = new ClassProvider(target, options); 66 | return provider.resolve(this); 67 | } 68 | 69 | /** 70 | * If the registration is ready, then resolve it based on scope 71 | * @param {RegistrationId} id - The registration id of the service you want to resolve. 72 | * @param {boolean} [required] - If true, the resolve method will throw an error if the registration is 73 | * not found. If false, it will return undefined. 74 | * @returns The resolved instance of the registration. 75 | */ 76 | resolve(id: RegistrationId, required?: boolean): T { 77 | const registration = this.container.get(id); 78 | 79 | if (registration?.isReady()) { 80 | if (registration.scope == Scope.CONTEXT) { 81 | const instance = this.getSavedInstance(id); 82 | 83 | if (instance != null) { 84 | return instance; 85 | } 86 | 87 | const resolved = registration.resolve(this); 88 | this.saveInstance(id, resolved); 89 | return resolved; 90 | } 91 | 92 | return registration.resolve(this); 93 | } 94 | 95 | if (required !== false) { 96 | throw new NotRegisteredError(id); 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/core/Container.ts: -------------------------------------------------------------------------------- 1 | import { ConcreteClass, Scope } from "../types"; 2 | import { Registry } from "./Registry"; 3 | import { Registration } from "./Registration"; 4 | import { RegistrationId } from "./Registration.types"; 5 | import { ResolutionContext } from "./ResolutionContext"; 6 | import { AlreadyRegisteredError } from "../errors/AlreadyRegisteredError"; 7 | import { NotRegisteredError } from "../errors/NotRegisteredError"; 8 | import { AbstractModule } from "./AbstractModule"; 9 | import { RegistrationBuilder } from "../builder/RegistrationBuilder"; 10 | import { AsSyntax } from "../builder/syntax/AsSyntax"; 11 | import { ClassProviderOptions } from "../providers/ClassProvider/ClassProvider.types"; 12 | 13 | export class Container { 14 | readonly parent?: Container; 15 | readonly registry: Registry; 16 | 17 | constructor(parent?: Container) { 18 | this.registry = new Registry(); 19 | this.parent = parent; 20 | } 21 | 22 | /** 23 | * The clear() function delete all registrations. 24 | */ 25 | clear(): void { 26 | this.registry.clear(); 27 | } 28 | 29 | /** 30 | * Return true if the container has a registration with the given id. 31 | * @param {RegistrationId} id - The id of the registration to check for. 32 | * @returns A boolean value. 33 | */ 34 | has(id: RegistrationId): boolean { 35 | return this.registry.has(id); 36 | } 37 | 38 | /** 39 | * If the registry has the id, return the registration, otherwise, return the parent's registration 40 | * @param {RegistrationId} id - The id of the registration. 41 | * @returns A Registration 42 | */ 43 | get(id: RegistrationId): Registration { 44 | if (this.registry.has(id)) { 45 | return this.registry.get(id); 46 | } 47 | 48 | return this.parent?.get(id); 49 | } 50 | 51 | /** 52 | * "If the registry doesn't have the given id, throw an error. Otherwise, remove the id from the 53 | * registry." 54 | * 55 | * The function checks if the given id exists, if it doesn't, it throws an NotRegisteredError. 56 | * @param {RegistrationId} id - The id of the registration to remove. 57 | */ 58 | remove(id: RegistrationId): void { 59 | if (!this.registry.has(id)) { 60 | throw new NotRegisteredError(id); 61 | } 62 | 63 | this.registry.delete(id); 64 | } 65 | 66 | /** 67 | * "If the registry doesn't already have a registration with the given id, create a new registration 68 | * with the given id and scope, save it to the registry, and return a new RegistrationBuilder for the 69 | * new registration." 70 | * 71 | * The first thing the function does is check to see if the registry already has a registration with 72 | * the given id. If it does, it throws an AlreadyRegisteredError. If it doesn't, it creates a new 73 | * registration with the given id and scope, saves it to the registry, and returns a new 74 | * RegistrationBuilder for the new registration 75 | * @param {RegistrationId} id - The id of the registration. 76 | * @param {Scope} [scope] - Scope 77 | * @returns A RegistrationBuilder 78 | */ 79 | register(id: RegistrationId, scope?: Scope): AsSyntax { 80 | if (this.has(id)) { 81 | throw new AlreadyRegisteredError(id); 82 | } 83 | 84 | const builder = new RegistrationBuilder(id, scope); 85 | 86 | return builder.start((registration) => { 87 | this.registry.save(registration); 88 | }); 89 | } 90 | 91 | /** 92 | * It takes an array of modules, and for each module, it calls the configure function 93 | * @param modules - [AbstractModule, ...AbstractModule[]] 94 | */ 95 | async addModule(...modules: [AbstractModule, ...AbstractModule[]]): Promise { 96 | const promises = modules.map(module => 97 | Promise.resolve(module.configure(this)) 98 | ) 99 | 100 | await Promise.all(promises) 101 | 102 | return this 103 | } 104 | 105 | /** 106 | * "Resolve the given registration id in the current context." 107 | * 108 | * The first thing we do is create a new resolution context. This is a new object that will be used to 109 | * track the resolution process 110 | * @param {RegistrationId} id - The registration id of the service you want to resolve. 111 | * @param {boolean} [required] - If true, the container will throw an error if the requested service is 112 | * not registered. 113 | * @returns The resolved value. 114 | */ 115 | resolve(id: RegistrationId, required?: boolean): T { 116 | const context = this.createResolutionContext(); 117 | return context.resolve(id, required); 118 | } 119 | 120 | /** 121 | * It creates a new instance of the given class, and returns it 122 | * @param target - The class to instantiate. 123 | * @param {ClassProviderOptions} [options] - { 124 | * @returns A new instance of the target class. 125 | */ 126 | instantiate(target: ConcreteClass, options?: ClassProviderOptions) { 127 | const context = this.createResolutionContext(); 128 | return context.instantiate(target, options); 129 | } 130 | 131 | /** 132 | * Create a new resolution context, and pass this container to it. 133 | * @returns A new ResolutionContext object. 134 | */ 135 | createResolutionContext(): ResolutionContext { 136 | return new ResolutionContext(this); 137 | } 138 | 139 | /** 140 | * Create a new container that is a child of this container. 141 | * @returns A new instance of the Container class. 142 | */ 143 | createChildContainer(): Container { 144 | return new Container(this); 145 | } 146 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | As the creators and maintainers of this project, we want to ensure that it lives on and continues to grow and evolve. We would like to encourage everyone to help and support this library by contributing. 4 | 5 | As a contributor, here are the guidelines we would like you to follow: 6 | 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Submission Guidelines](#submit) 10 | - [Coding Standard](#coding-standard) 11 | - [Commit Message Format](#commit) 12 | 13 | ## Found a Bug? 14 | 15 | If you find a bug in the source code, you can help by [submitting an issue](#submit-issue) or even better, by [submitting a Pull Request](#submit-pr) with a fix. 16 | 17 | ## Missing a Feature? 18 | 19 | You can *request* a new feature by [submitting an issue](#submit-issue). 20 | If you would like to *implement* a new feature, please submit an issue with a proposal for your work first, 21 | so that it can be discussed. This will also allow us to better coordinate our efforts, 22 | prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project. 23 | ## Submission Guidelines 24 | 25 | ### Submitting an Issue 26 | 27 | Before you submit an issue, please search the issue tracker, 28 | maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 29 | 30 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. 31 | In order to reproduce bugs we ask you to provide a minimal code snippet that shows a reproduction of the problem. 32 | 33 | 34 | ### Submitting a Pull Request (PR) 35 | Before you submit your Pull Request (PR) consider the following guidelines: 36 | 37 | * Search [GitHub](https://github.com/microkits/microinjection/pulls) for an open or closed PR 38 | that relates to your submission. You don't want to duplicate effort. 39 | 40 | * Clone your fork of the repository. 41 | ```sh 42 | git clone https://github.com/YOUR_USERNAME/microinjection.git 43 | ``` 44 | 45 | * Install dependencies. 46 | ```sh 47 | yarn 48 | ``` 49 | 50 | * Create your patch, **including appropriate test cases**. This project aims to have 100% code coverage, without tests your PR will not be accepted. 51 | * Run all test suites and ensure that all tests pass. 52 | ```sh 53 | yarn test 54 | ``` 55 | * Run lint and ensure all code is following lint rules. 56 | ```sh 57 | yarn lint 58 | ``` 59 | * Commit your changes using a descriptive commit message that follows our [Commit Message Format](#commit). 60 | * In GitHub, send a pull request to `main` branch. 61 | * If we suggest changes then: 62 | * Make the required updates. 63 | * Re-run all test suites to ensure tests are still passing. 64 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 65 | 66 | ```shell 67 | git rebase main -i 68 | git push -f 69 | ``` 70 | 71 | That's it! Thank you for your contribution! 72 | 73 | ## Coding Standard 74 | 75 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 76 | 77 | - All features or bug fixes **must be tested**. 78 | - If you are implementing new feature or extending public API, you should **document it**. 79 | 80 | Some highlights: 81 | 82 | - use 2 spaces for indentation 83 | - always use semicolons 84 | - prefer `const` over `let` (and do not use `var`) 85 | 86 | ## Commit Message Guidelines 87 | 88 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 89 | readable messages** that are easy to follow when looking through the **project history**. 90 | 91 | ### Commit Message Format 92 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 93 | format that includes a **type** and a **subject**: 94 | 95 | Each commit message consists of a header, a body and a footer, following [Conventional Commits] specification. 96 | 97 | ``` 98 | : 99 | 100 | 101 | 102 |