diff --git a/README.md b/README.md index b0a5dde..06ae236 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ A modern, lightweight implementation of dependency injection inspired by [JSR-330](https://jcp.org/en/jsr/detail?id=330) and [Spring](https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-spring-beans-and-dependency-injection.html). -[![Version](https://img.shields.io/npm/v/es-injection.svg?label=Version&style=for-the-badge&logo=npm)](https://www.npmjs.com/package/es-injection) -[![Downloads](https://img.shields.io/npm/dt/es-injection.svg?label=Downloads&style=for-the-badge&logo=npm)](https://www.npmjs.com/package/es-injection) +[![Version](https://img.shields.io/npm/v/@es-injection/core.svg?label=Version&style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@es-injection/core) +[![Downloads](https://img.shields.io/npm/dt/@es-injection/core.svg?label=Downloads&style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@es-injection/core) [![AppVeyor](https://img.shields.io/appveyor/ci/rraziel/es-injection/master.svg?label=Win32&style=for-the-badge&logo=appveyor)](https://ci.appveyor.com/project/rraziel/es-injection) [![CircleCI](https://img.shields.io/circleci/project/github/rraziel/es-injection/master.svg?label=MacOS&style=for-the-badge&logo=circleci)](https://circleci.com/gh/rraziel/es-injection) [![Travis CI](https://img.shields.io/travis/rraziel/es-injection/master.svg?label=Linux&style=for-the-badge&logo=travis)](https://travis-ci.org/rraziel/es-injection) @@ -37,25 +37,25 @@ This loose coupling tends to make code more self-contained and easier to test, a The library can be installed using `npm`: ``` -npm install es-injection --save +npm install @es-injection/core --save ``` Or using `yarn`: ``` -yarn add es-injection +yarn add @es-injection/core ``` ## Development -The module can be built using the following command: +The modules can be built using the following command: ``` -npm run compile +yarn run build ``` -It is also possible to keep unit and integration tests executing as a background task: +Tests are written using [Jest](https://jestjs.io/) and can be kept running as a background task with: ``` -npm run test:watch +yarn run test:watch ``` diff --git a/doc/annotation-configuration.md b/doc/annotation-configuration.md index 4c90168..67bb004 100644 --- a/doc/annotation-configuration.md +++ b/doc/annotation-configuration.md @@ -47,7 +47,8 @@ This effectively lets the application context create `MyComponent` instances. ### Component Instance -Sometimes a component cannot be easily instantiated, either because it requires specific settings that are not available to the component and need to be passed in, or simply because the objects are from third-party libraries. +Sometimes a component cannot be easily instantiated, either because it requires specific settings that are not +available to the component and need to be passed in, or simply because the objects are from third-party libraries. In this scenario, components can be created directly within the configuration class: @@ -105,7 +106,8 @@ As projects grow larger, having multiple configuration classes - one per sub-mod In many cases, components defined within a sub-module will have dependencies that come from other sub-modules. -For such a scenario, requiring the user of sub-module A to know they need to add the configuration for sub-module B simply because A requires B is not convenient. +For such a scenario, requiring the user of sub-module A to know they need to add the configuration for sub-module B +simply because A requires B is not convenient. To work around this, the @Import decorator can be used: diff --git a/doc/application-context.md b/doc/application-context.md index 1b4ae5e..644dea7 100644 --- a/doc/application-context.md +++ b/doc/application-context.md @@ -12,7 +12,8 @@ The application context is the central interface for [component injection](compo ## Context Lifecycle -The application context has a lifecycle that determines what type of operation can be performed with the application context. +The application context has a lifecycle that determines what type of operation can be performed with the application +context. The lifecycle can be describe as the following steps: @@ -21,7 +22,7 @@ The lifecycle can be describe as the following steps: 3. the execution phase 4. the [context stop](#context-stop) -The context gets configured and, once started, becomes read-only.It is configured once and then, once started, becomes read-only. +The context gets configured and, once started, becomes read-only. ## Context Configuration @@ -38,7 +39,8 @@ Annotation-based configuration makes it possible to: - declare other configurations that are dependencies, via the `@Import` decorator - provide component instances via methods annotated with a `@Component` decorator -More information regarding annotation-based configuration can be found in the [Annotation-based Configuration chapter](annotation-configuration.md). +More information regarding annotation-based configuration can be found in the +[Annotation-based Configuration chapter](annotation-configuration.md). ## Context Start diff --git a/doc/component-declaration.md b/doc/component-declaration.md index 2249d44..22f565e 100644 --- a/doc/component-declaration.md +++ b/doc/component-declaration.md @@ -7,14 +7,16 @@ ## Introduction -Not documented yet. +Declaring a component allows its lifecycle to be managed by es-injection. + +The decorators necessary for component declaration can be imported from the @es-injection/decorators module. ## Basic Declaration Declaring a component is as simple as adding a `@Component` decorator to a class. ```typescript -import {Component} from 'es-injection'; +import {Component} from '@es-injection/decorators'; @Component class MyDependency { @@ -34,7 +36,8 @@ As an alternative, it is also possible to use one of three more specific stereot - `@Repository`, dedicated to the persistence layer - `@Service`, dedicated to the service layer -All four decorators exhibit the same behavior, but the annotated code is usually more readable when using the proper stereotype. +All four decorators exhibit the same behavior, but the annotated code is usually more readable when using the proper +stereotype. It also makes it possible to filter components based on their stereotype. @@ -43,7 +46,7 @@ It also makes it possible to filter components based on their stereotype. The aforementioned decorators also make it possible to give a name to a component. ```typescript -import {Component} from 'es-injection'; +import {Component} from '@es-injection/decorators'; @Component('my-dependency-name') class MyDependency { @@ -51,7 +54,9 @@ class MyDependency { } ``` -Components with a name can be queried by name, either programmatically through the [application context](application-context.md) or using the [@Named](component-injection.md#named-dependencies) decorator. +Components with a name can be queried by name, either programmatically through the +[application context](application-context.md) or using the [@Named](component-injection.md#named-dependencies) +decorator. ## Abstract Class vs. Interface @@ -59,7 +64,8 @@ Currently, interfaces are not concrete types and do not get compiled into an act Due to this, it is not possible to add decorators to an interface and then retrieve components through this interface. -It, however, possible to achieve something quite similar by creating an abstract class with nothing but abstract methods. +It, however, possible to achieve something quite similar by creating an abstract class with nothing but abstract +methods. The following interface: diff --git a/doc/component-injection.md b/doc/component-injection.md index fe8e9b1..23ea7a5 100644 --- a/doc/component-injection.md +++ b/doc/component-injection.md @@ -1,22 +1,88 @@ # Injecting Components -TODO +- [Introduction](#introduction) +- [Dependencies](#dependencies) + - [Constructor-based Injection](#constructor-based-injection) + - [Method-based Injection](#method-based-injection) + - [Property-based Injection](#property-based-injection) +- [Named Dependencies](#named-dependencies) +- [Optional Dependencies](#optional-dependencies) +- [Containers](#container-dependencies) + - [Arrays](#arrays) + - [Maps](#maps) + +## Introduction + +The main advantage of using es-injection is that components may be injected into other components. + +This helps properly separating the responsibilities of each components, while making testing more straightforward as +injected dependencies can simply be mocked. ## Dependencies -TODO. +Injecting a dependency is done using the `@Inject` decorator. -### Constructor-based +A number of additional decorators also make it possible to further refine what gets injected. -TODO. +### Constructor-based Injection -### Method-based +Since es-injection manages the instantiation of components, it needs to be able to provide all constructor arguments. -TODO. +In practice, it means all constructor parameters come from injection without requiring the `@Inject` decorator. -### Property-based +```typescript +@Component +class MyComponent { + constructor(dependency1: DependencyComponent, dependency2: OtherDependencyComponent) { + } +} +``` -TODO. +While convenient, constructor-based injection should be limited to dependencies that are necessary to the instance +construction. + +When that is not the case, [method-based injection](#method-based) should be favored. + +### Method-based Injection + +Method-based injection tells es-injection to inject a dependency via a method call. + +For such cases, the `@Inject` decorator is required. + +```typescript +@Component +class MyComponent { + @Inject + setDependencies(dependency1: DependencyComponent, dependency2: OtherDependencyComponent): void { + } + + @Inject + setMoreDependencies(dependency3: YetAnotherDependencyComponent): void { + } +} +``` + +Multiple methods can be decorated with `@Inject`, but common practice involves injecting a single dependency per method +call though. + +### Property-based Injection + +Property-based injection tells es-injection to inject a dependency directly into a property. + +Doing this required the `@Inject` decorator as well. + +```typescript +@Component +class MyComponent { + @Inject + dependency1: DependencyComponent; + @Inject + dependency2: OtherDependencyComponent; +} +``` + +While convenient, property-based injection can prove difficult to work with as such injected properties are generally +meant to be private and could therefore not be set from outside the class, e.g. for tests. ## Named Dependencies @@ -24,12 +90,56 @@ TODO. ## Optional Dependencies -TODO. +An optional dependency is a dependency that may not be available within the +[application context](./application-context.md). -## List Dependencies +When encountering such an optional dependency, the injected value will be `undefined` if the dependency is unavailable. -TODO. +```typescript +@Component +class MyComponent { + @Inject + setDependency(@Optional dependency: DependencyComponent|undefined): void { + } +} +``` -## Map Dependencies +## Containers -TODO. +Sometimes multiple classes match an injection request, such as injecting a based class that has multiple derived +components. + +In that case, an array or a map can be used to inject all available components. + +Due to current limitations with the code emitted from a TypeScript compilation, es-injection cannot know the type +to inject when using arrays or maps. + +To work around this issue, the `@ElementClass` decorator must be used. + +### Arrays + +When injecting an array, the `@ElementClass` decorator hints at the type of the elements in the list. + +```typescript +@Component +class MyComponent { + @Inject + setDependencies(@ElementClass(BaseClass) dependencies: Array): void { + } +} +``` + +### Maps + +When injecting a map, the `@ElementClass` decorator hints at the type of the values in the list. + +The keys must always be strings, and will be each component's name. + +```typescript +@Component +class MyComponent { + @Inject + setDependencies(@ElementClass(BaseClass) dependencies: Map): void { + } +} +``` diff --git a/doc/component-lifecycle.md b/doc/component-lifecycle.md index 0972d92..f4beda7 100644 --- a/doc/component-lifecycle.md +++ b/doc/component-lifecycle.md @@ -14,7 +14,8 @@ A component possesses a lifecycle, with two phases that occurs during the component's construction and destruction. -It is possible to execute logic during those phases, which are generally used to initialize a component or shut it down, respectively. +It is possible to execute logic during those phases, which are generally used to initialize a component or shut it +down, respectively. ## Construction Phase @@ -30,7 +31,7 @@ The construction phase starts when a component needs to be instantiated. This ph A post-destruction method is a method decorated with the `@PostConstruct` decorator: ```typescript -import {Component, PostConstruct} from 'es-injection'; +import {Component, PostConstruct} from '@es-injection/decorators'; @Component class MyComponent { @@ -48,7 +49,7 @@ class MyComponent { It is also possible to perform an asynchronous cleanup, simply by having the method return a promise: ```typescript -import {Component, PreDestroy} from 'es-injection'; +import {Component, PreDestroy} from '@es-injection/decorators'; @Component class MyComponent { @@ -66,7 +67,7 @@ class MyComponent { The pre-construction method call order can be explicitely set using `@Order` decorators: ```typescript -import {Component, Order, PostConstruct} from 'es-injection'; +import {Component, Order, PostConstruct} from '@es-injection/decorators'; @Component class MyComponent { @@ -106,7 +107,7 @@ The destruction phase is simpler, but still made of multiple steps: A pre-destruction method is a method decorated with the `@PreDestroy` decorator: ```typescript -import {Component, PreDestroy} from 'es-injection'; +import {Component, PreDestroy} from '@es-injection/decorators'; @Component class MyComponent { @@ -124,7 +125,7 @@ class MyComponent { It is also possible to perform an asynchronous cleanup, simply by having the method return a promise: ```typescript -import {Component, PreDestroy} from 'es-injection'; +import {Component, PreDestroy} from '@es-injection/decorators'; @Component class MyComponent { @@ -142,7 +143,7 @@ class MyComponent { The pre-destruction method call order can be explicitely set using `@Order` decorators: ```typescript -import {Component, Order, PreDestroy} from 'es-injection'; +import {Component, Order, PreDestroy} from '@es-injection/decorators'; @Component class MyComponent { diff --git a/doc/component-scope.md b/doc/component-scope.md index 9dd114e..8d30e92 100644 --- a/doc/component-scope.md +++ b/doc/component-scope.md @@ -8,14 +8,16 @@ A component can have one of two scopes, declared using the `@Scope` decorator. -The scope determines when happens when the component needs to be [injected](component-injection.md) or when it gets retrieved through an [application context](application-context.md). +The scope determines when happens when the component needs to be [injected](component-injection.md) or when it gets +retrieved through an [application context](application-context.md). ## Singleton Scope -The singleton scope is the default, implicit scope. When no `@Scope` decorator is present, a component has a singleton scope. It can, however, be set explicitely: +The singleton scope is the default, implicit scope. When no `@Scope` decorator is present, a component has a singleton +scope. It can, however, be set explicitely: ```typescript -import {ApplicationContext, Component, Scope, ScopeType} from 'es-injection'; +import {ApplicationContext, Component, Scope, ScopeType} from '@es-injection/decorators'; @Component @Scope(ScopeType.SINGLETON) @@ -24,15 +26,16 @@ class MyComponent { } async function sameInstances(applicationContext: ApplicationContext): boolean { - let instance1: MyComponent = await applicationContext.getComponent(MyComponent); - let instance2: MyComponent = await applicationCOntext.getComponent(MyComponent); + const instance1: MyComponent = await applicationContext.getComponent(MyComponent); + const instance2: MyComponent = await applicationContext.getComponent(MyComponent); return instance1 === instance2; // always true } ``` When a component is a singleton, it will be instantiated only once. -Each time the component needs to be [injected](component-injection.md), and each time the component is retrieved through an [application context](application-context.md), the same single unique instance is used. +Each time the component needs to be [injected](component-injection.md), and each time the component is retrieved +through an [application context](application-context.md), the same single unique instance is used. This scope is generally used for global objects that are expected to be singletons. @@ -43,7 +46,7 @@ The prototype scope requires a new component instance to be used whenever reques It is set explicitely using a `PROTOTYPE` scope type: ```typescript -import {ApplicationContext, Component, Scope, ScopeType} from 'es-injection'; +import {ApplicationContext, Component, Scope, ScopeType} '@es-injection/decorators'; @Component @Scope(ScopeType.PROTOTYPE) @@ -60,4 +63,5 @@ async function sameInstances(applicationContext: ApplicationContext): boolean { This scope is generally used for objects that get instantiated many times throughought the lifecycle of an application. -The advantage of using a prototype component versus simply creating a new instance manually is that the prototype component will get its dependencies injected automatically. +The advantage of using a prototype component versus simply creating a new instance manually is that the prototype +component will get its dependencies injected automatically. diff --git a/doc/constant-injection.md b/doc/constant-injection.md index ae66527..720ab86 100644 --- a/doc/constant-injection.md +++ b/doc/constant-injection.md @@ -1,3 +1,3 @@ # Injecting Constants -Not documented yet. +Not available yet. diff --git a/packages/decorators/src/Optional.spec.ts b/packages/decorators/src/Optional.spec.ts index a41596a..cb3bedb 100644 --- a/packages/decorators/src/Optional.spec.ts +++ b/packages/decorators/src/Optional.spec.ts @@ -8,7 +8,7 @@ describe('@Optional decorator', () => { it('a property', () => { // given class TestClass { - @Optional private p: string = 'test'; + @Optional private p: string|undefined = 'test'; constructor() { this.p; } } // when @@ -21,7 +21,7 @@ describe('@Optional decorator', () => { it('a constructor parameter', () => { // given class TestClass { - constructor(@Optional p: string) { /* empty */ } + constructor(@Optional p: string|undefined) { /* empty */ } } // when const methodInfo: MethodInfo|undefined = getMethodInfo(TestClass); @@ -35,7 +35,7 @@ describe('@Optional decorator', () => { it('a method parameter', () => { // given class TestClass { - method(@Optional p: string): void { /* empty */ } + method(@Optional p: string|undefined): void { /* empty */ } } // when const methodInfo: MethodInfo|undefined = getMethodInfo(TestClass, 'method'); @@ -54,7 +54,7 @@ describe('@Optional decorator', () => { // expect expect(() => { class TestClass { - static method(@Optional p: string): void { /* empty */ } + static method(@Optional p: string|undefined): void { /* empty */ } } TestClass; }).toThrowError(/cannot be applied to static method or property TestClass\.method/); @@ -64,7 +64,7 @@ describe('@Optional decorator', () => { // expect expect(() => { class TestClass { - @Optional private static p: string; + @Optional private static p: string|undefined; constructor() { TestClass.p; } } TestClass;