Comparing version 4.0.0-rc.1 to 4.0.0-rc.2
{ | ||
"name": "cheap-di", | ||
"description": "TypeScript dependency injection like Autofac in .Net", | ||
"version": "4.0.0-rc.1", | ||
"version": "4.0.0-rc.2", | ||
"scripts": { | ||
@@ -6,0 +6,0 @@ "compile": "tsc --build tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", |
419
README.md
@@ -5,13 +5,11 @@ # cheap-di | ||
* [Minimal Sample (TypeScript)](#minimal-sample) | ||
* [How to use (JavaScript)](#how-to-use) | ||
* [TypeScript](#type-script) | ||
* [Decorators](#decorators) | ||
* [dependencies](#dependencies) | ||
* [inject](#inject) | ||
* [di](#di) | ||
* [singleton](#singleton) | ||
* [How to use](#how-to-use) | ||
* [Registration variants](#registration-variants) | ||
* [registerImplementation](#register-implementation) | ||
* [registerInstance](#register-instance) | ||
## <a name="minimal-sample"></a> Minimal Sample (TypeScript) | ||
## <a name="how-to-use"></a> How to use | ||
The recommended way of using this package is using it with code transformers like `cheap-di-ts-transform`. Because in this way you will get the truly dependency injection: | ||
```ts | ||
@@ -23,5 +21,3 @@ abstract class Logger { | ||
class ConsoleLogger implements Logger { | ||
constructor(prefix) { | ||
this.prefix = prefix; | ||
} | ||
constructor(public prefix: string) {} | ||
@@ -33,5 +29,2 @@ debug(message: string) { | ||
const metadata = <T>(constructor: T): T => constructor; | ||
@metadata | ||
class Service { | ||
@@ -44,5 +37,25 @@ constructor(private logger: Logger) {} | ||
} | ||
/** | ||
* With cheap-di-ts-transform here will be added information about Service dependencies. | ||
* It will looks like: | ||
* @example | ||
* import { findOrCreateMetadata } from 'cheap-di'; | ||
* | ||
* // for Logger | ||
* try { | ||
* const metadata = findOrCreateMetadata(Logger); | ||
* | ||
* // only classes may be instantiated with DI, other parameters can be filled with argument injection | ||
* metadata.dependencies = ["unknown"]; | ||
* } catch {} | ||
* | ||
* // for Service | ||
* try { | ||
* const metadata = findOrCreateMetadata(Service); | ||
* | ||
* metadata.dependencies = [Logger]; | ||
* } catch {} | ||
* */ | ||
// somewhere | ||
// somewhere in you application initialization | ||
import { container } from 'cheap-di'; | ||
@@ -52,4 +65,7 @@ | ||
container.registerType(ConsoleLogger).as(Logger).with(myLogPrefix); | ||
container.registerType(Service); | ||
// somewhere in inside your code | ||
// or you may use some middleware to do this, to get rid of Service Locator antipattern | ||
import { container } from 'cheap-di'; | ||
const service = container.resolve(Service); | ||
@@ -59,356 +75,145 @@ service.doSome(); | ||
## <a name="how-to-use"></a> How to use (JavaScript) | ||
But if you can't use transformers you still may use cheap-di with decorators: | ||
You have an interface (base/abstract class) and its implementation (derived class) | ||
```ts | ||
import { inject } from 'cheap-di'; | ||
`user-repository.js` | ||
```js | ||
export class UserRepository { | ||
constructor() { | ||
if (new.target === UserRepository) { | ||
throw new TypeError('Cannot construct UserRepository instance directly'); | ||
} | ||
} | ||
list() { | ||
throw new Error("Not implemented"); | ||
} | ||
getById(userId) { | ||
throw new Error("Not implemented"); | ||
} | ||
abstract class SessionAccessor { | ||
abstract getSession(): string; | ||
} | ||
``` | ||
`fake-user-repository.js` | ||
```js | ||
import { UserRepository } from './user-repository'; | ||
export class FakeUserRepository extends UserRepository { | ||
constructor() { | ||
super(); | ||
this.users = [ | ||
{ | ||
id: 1, | ||
name: 'user-1' | ||
}, | ||
{ | ||
id: 2, | ||
name: 'user-2' | ||
}, | ||
]; | ||
} | ||
list() { | ||
return this.users; | ||
} | ||
getById(userId) { | ||
return this.users.find(user => user.id === userId); | ||
} | ||
abstract class Logger { | ||
abstract debug(message: string): void; | ||
} | ||
``` | ||
abstract class InfoLogger extends Logger {} | ||
abstract class ErrorLogger extends Logger {} | ||
There is simple logger. | ||
// non-classes-arguments specified as "unknown" | ||
@inject('unknown', SessionAccessor) | ||
class ConsoleLogger implements Logger { | ||
constructor(public prefix: string, private sessionAccessor: SessionAccessor) {} | ||
`logger.js` | ||
```js | ||
export class Logger { | ||
debug(message) { | ||
throw new Error("Not implemented"); | ||
debug(message: string) { | ||
console.log(`[${this.sessionAccessor.getSession()}] ${this.prefix}: ${message}`); | ||
} | ||
} | ||
``` | ||
`console-logger.js` | ||
```js | ||
import { Logger } from './logger'; | ||
class Service { | ||
constructor(private logger: InfoLogger) {} | ||
export class ConsoleLogger extends Logger { | ||
constructor(prefix) { | ||
super(); | ||
this.prefix = prefix; | ||
doSome() { | ||
this.logger.debug('Hello world!'); | ||
} | ||
debug(message) { | ||
console.log(`${this.prefix}: ${message}`); | ||
} | ||
} | ||
``` | ||
You have the repository consumer. To allow DI container inject dependencies in your consumer class you should | ||
specify `__dependencies` static property. That property should contain constructor array in the order of your | ||
constructor. | ||
`user-service.js` | ||
```js | ||
import { dependenciesSymbol } from 'cheap-di'; | ||
import { Logger } from './logger'; | ||
import { UserRepository } from './user-repository'; | ||
export class UserService { | ||
static [dependenciesSymbol] = [UserRepository, Logger]; | ||
constructor(userRepository, logger) { | ||
this.userRepository = userRepository; | ||
this.logger = logger; | ||
} | ||
list() { | ||
this.logger.debug('Access to users list'); | ||
return this.userRepository.list(); | ||
} | ||
get(userId) { | ||
return this.userRepository.getById(userId); | ||
} | ||
} | ||
``` | ||
Dependency registration | ||
`some-config.js` | ||
```js | ||
// somewhere | ||
import { container } from 'cheap-di'; | ||
import { ConsoleLogger } from './console-logger'; | ||
import { FakeUserRepository } from './fake-user-repository'; | ||
import { Logger } from './logger'; | ||
import { UserRepository } from './user-repository'; | ||
container.registerType(ConsoleLogger).as(Logger).with('most valuable message prefix'); | ||
container.registerType(FakeUserRepository).as(UserRepository); | ||
``` | ||
const infoPrefix = 'INFO: '; | ||
container.registerType(ConsoleLogger).as(InfoLogger).with(infoPrefix); | ||
To get instance of your service with injected parameters you should call `resolve` method. | ||
const errorPrefix = 'ERROR: '; | ||
container.registerType(ConsoleLogger).as(ErrorLogger).with(errorPrefix); | ||
`some-place.js` | ||
```js | ||
// somewhere in inside your code | ||
// or you may use some middleware to do this, to get rid of Service Locator antipattern | ||
import { container } from 'cheap-di'; | ||
import { UserService } from './user-service'; | ||
const service = container.resolve(UserService); | ||
const users = service.list(); | ||
const service = container.resolve(Service); | ||
service.doSome(); | ||
``` | ||
Your injection parameter can be placed in middle of constructor params. In this case you should put `undefined` | ||
or `null` in `[dependencies]` with accordance order | ||
```js | ||
import { dependenciesSymbol, container } from 'cheap-di'; | ||
// ... | ||
## <a name="registration-variants"></a> Registration variants | ||
export class UserService { | ||
static [dependenciesSymbol] = [UserRepository, undefined, Logger]; | ||
#### <a name="register-implementation"></a> registerImplementation | ||
constructor(userRepository, someMessage, logger) { | ||
// ... | ||
} | ||
} | ||
If you would like to specify implementation of your interface: | ||
```ts | ||
import { container } from 'cheap-di'; | ||
// ... | ||
abstract class Service {/**/} | ||
class ServiceImpl extends Service {/**/} | ||
container.registerType(UserService).with('my injection string'); | ||
container | ||
.registerImplementation(ServiceImpl) | ||
.as(Service); | ||
``` | ||
## <a name="type-script"></a> TypeScript | ||
`logger.ts` | ||
Or if you want to inject some parameters to its constructor: | ||
```ts | ||
export abstract class Logger { | ||
abstract debug: (message: string) => void; | ||
} | ||
``` | ||
import { container } from 'cheap-di'; | ||
`console-logger.ts` | ||
```ts | ||
import { Logger } from './logger'; | ||
export class ConsoleLogger extends Logger { | ||
constructor(prefix) { | ||
super(); | ||
this.prefix = prefix; | ||
} | ||
debug(message: string) { | ||
console.log(`${this.prefix}: ${message}`); | ||
} | ||
class Some { | ||
constructor(private name: string) {} | ||
} | ||
``` | ||
You can use typescript reflection for auto resolve your class dependencies and inject them. | ||
For that you should add lines below to your `tsconfig.json`: | ||
container | ||
.registerImplementation(Service) | ||
.inject('some name'); | ||
``` | ||
"emitDecoratorMetadata": true, | ||
"experimentalDecorators": true, | ||
``` | ||
and add any class-decorator to your class. For example: | ||
`metadata.ts` | ||
Or if you want to have only one instance of the implementation class: | ||
```ts | ||
export const metadata = <T>(constructor: T): T => constructor; | ||
``` | ||
import { container } from 'cheap-di'; | ||
`service.ts` | ||
```ts | ||
import { metadata } from './metadata'; | ||
import { Logger } from './logger'; | ||
class Some {} | ||
@metadata | ||
export class Service { | ||
constructor(private logger: Logger) {} | ||
doSome() { | ||
this.logger.debug('Hello world!'); | ||
} | ||
} | ||
container | ||
.registerImplementation(Service) | ||
.asSingleton(); | ||
``` | ||
But you should explicitly register each type like this to resolve his dependencies by container: | ||
And singleton also may be used with interface specification: | ||
```ts | ||
import { container } from 'cheap-di'; | ||
import { Service } from './service'; | ||
container.registerType(Service); | ||
abstract class Service {/**/} | ||
class ServiceImpl extends Service {/**/} | ||
const service = container.resolve(Service); | ||
service.doSome(); | ||
container | ||
.registerImplementation(ServiceImpl) | ||
.asSingleton(Service); | ||
``` | ||
<h3>In this scenario you don't need decorators from next section!</h3> | ||
--- | ||
## <a name="decorators"></a> Decorators | ||
If you want to use any of next decorators, you should add line below to your `tsconfig.json`: | ||
``` | ||
"experimentalDecorators": true, | ||
``` | ||
### <a name="dependencies"></a> dependencies | ||
`@dependencies` decorator can be used to simplify dependency syntax | ||
`user-service.ts` | ||
And even with argument injection: | ||
```ts | ||
import { dependencies } from 'cheap-di'; | ||
import { Logger } from './logger'; | ||
import { UserRepository } from './user-repository'; | ||
import { container } from 'cheap-di'; | ||
@dependencies(UserRepository, Logger) | ||
export class UserService { | ||
constructor( | ||
private userRepository: UserRepository, | ||
private logger: Logger, | ||
) {} | ||
abstract class Service {/**/} | ||
list() { | ||
this.logger.debug('Access to users list'); | ||
return this.userRepository.list(); | ||
class ServiceImpl extends Service { | ||
constructor(private name: string) { | ||
super(); | ||
} | ||
get(userId) { | ||
return this.userRepository.getById(userId); | ||
} | ||
} | ||
``` | ||
### <a name="inject"></a> inject | ||
`@inject` decorator can be used instead of `@dependencies` like below: | ||
```ts | ||
import { inject } from 'cheap-di'; | ||
export class UserService { | ||
constructor( | ||
@inject(UserRepository) private userRepository: UserRepository, | ||
@inject(Logger) private logger: Logger, | ||
) {} | ||
} | ||
container | ||
.registerImplementation(ServiceImpl) | ||
.asSingleton(Service) | ||
.inject('some name'); | ||
``` | ||
This approach allows you to mix dependency with injection params with any order: | ||
#### <a name="register-instance"></a> registerInstance | ||
```ts | ||
class Service { | ||
constructor( | ||
message1: string, | ||
@inject(Repository) public repository: Repository, | ||
message2: string, | ||
@inject(Database) public db: Database, | ||
) { | ||
} | ||
} | ||
If you want to register some instance as interface | ||
const message1 = '123'; | ||
const message2 = '456'; | ||
container.registerType(Service).with(message1, message2); | ||
``` | ||
### <a name="di"></a> di | ||
This decorator uses typescript reflection, so you should add line below to your `tsconfig.json`: | ||
``` | ||
"emitDecoratorMetadata": true, | ||
``` | ||
`@di` decorator can be used instead of `@dependencies` and `@inject` like below: | ||
```ts | ||
import { di } from 'cheap-di'; | ||
import { container } from 'cheap-di'; | ||
@di | ||
export class UserService { | ||
constructor( | ||
private userRepository: UserRepository, | ||
private logger: Logger, | ||
) {} | ||
abstract class Database { | ||
abstract get(): Promise<string>; | ||
} | ||
@di | ||
class Service { | ||
constructor( | ||
message1: string, | ||
public repository: UserRepository, | ||
message2: string, | ||
public db: Database, | ||
) {} | ||
} | ||
const db: Database = { | ||
async get() { | ||
return Promise.resolve('name1'); | ||
}, | ||
}; | ||
const message1 = '123'; | ||
const message2 = '456'; | ||
container.registerType(Service).with(message1, message2); | ||
container.registerInstance(db).as(Database); | ||
``` | ||
It automatically adds `@inject` decorators to your service. | ||
### <a name="singleton"></a> singleton | ||
`@singleton` decorator allows you to inject the same instance everywhere. | ||
```ts | ||
import { singleton } from 'cheap-di'; | ||
import { Logger } from './logger'; | ||
import { UserRepository } from './user-repository'; | ||
@singleton | ||
export class UserService { | ||
names: string[]; | ||
constructor() { | ||
this.names = []; | ||
} | ||
list() { | ||
return this.names; | ||
} | ||
add(name: string) { | ||
this.names.push(name); | ||
} | ||
} | ||
``` | ||
You can see more examples in `cheap-di/src/ContainerImpl.test.ts` | ||
[Changelog](../../CHANGELOG.md) |
@@ -1,5 +0,6 @@ | ||
import { isSingleton, modifySingleton } from './singleton.js'; | ||
import { CircularDependencyError } from './CircularDependencyError.js'; | ||
import { findMetadata, findOrCreateMetadata } from './findMetadata.js'; | ||
import { isSingleton } from './isSingleton.js'; | ||
import { modifyConstructor } from './modifyConstructor.js'; | ||
import { Trace } from './Trace.js'; | ||
import { | ||
@@ -16,3 +17,3 @@ AbstractConstructor, | ||
} from './types.js'; | ||
import { Trace } from './Trace.js'; | ||
import { workWithDiSettings } from './workWithDiSettings.js'; | ||
@@ -30,5 +31,5 @@ class ContainerImpl implements Container, IHaveSingletons, IHaveInstances, IHaveDependencies, Disposable { | ||
/** register class */ | ||
registerType<TInstance>(implementationType: ImplementationType<TInstance>) { | ||
const withInjection = (...injectionParams: any[]) => { | ||
/** register implementation class */ | ||
registerImplementation<TInstance>(implementationType: ImplementationType<TInstance>) { | ||
const inject = (...injectionParams: any[]) => { | ||
modifyConstructor(implementationType, (settings) => { | ||
@@ -49,6 +50,6 @@ const metadata = findOrCreateMetadata(settings); | ||
/** add parameters that will be passed to the class constructor */ | ||
with: withInjection, | ||
inject, | ||
}; | ||
}, | ||
/** as singleton (optionally super class). Only if you didn't use @singleton decorator */ | ||
/** as singleton (optionally super class) */ | ||
asSingleton: <TBase extends Partial<TInstance>>(type?: RegistrationType<TBase>) => { | ||
@@ -61,3 +62,7 @@ if (type) { | ||
if (!isSingleton(implementationType)) { | ||
modifySingleton(implementationType); | ||
workWithDiSettings(implementationType, (settings) => { | ||
const metadata = findOrCreateMetadata(settings); | ||
metadata.modifiedClass = implementationType as TInstance; | ||
metadata.singleton = true; | ||
}); | ||
} | ||
@@ -67,7 +72,7 @@ | ||
/** add parameters that will be passed to the class constructor */ | ||
with: withInjection, | ||
inject, | ||
}; | ||
}, | ||
/** add parameters that will be passed to the class constructor */ | ||
with: withInjection, | ||
inject, | ||
}; | ||
@@ -165,7 +170,7 @@ } | ||
if (dependencies?.length) { | ||
const injectableDependencies = dependencies.filter((d) => d); | ||
const length = injectableDependencies.length + injectionParams.length; | ||
const injectableDependencies = dependencies.filter((dependency) => dependency !== 'unknown') as Dependency[]; | ||
// const length = injectableDependencies.length + injectionParams.length; | ||
while (true) { | ||
const dependencyType = dependencies[index]; | ||
const dependencyType = injectableDependencies[index]; | ||
if (dependencyType) { | ||
@@ -192,3 +197,4 @@ trace.addTrace(dependencyType.name); | ||
if (index >= length) { | ||
// if (index >= length) { | ||
if (index >= injectableDependencies.length) { | ||
break; | ||
@@ -195,0 +201,0 @@ } |
@@ -0,5 +1,8 @@ | ||
export * from './cheapDiSymbol.js'; | ||
export * from './CircularDependencyError.js'; | ||
export * from './ContainerImpl.js'; | ||
export * from './singleton.js'; | ||
export * from './CircularDependencyError.js'; | ||
export * from './decorators/inject.js'; | ||
export * from './findMetadata.js'; | ||
export * from './isSingleton.js'; | ||
export * from './Trace.js'; | ||
export * from './types.js'; | ||
export * from './cheapDiSymbol.js'; |
@@ -1,2 +0,2 @@ | ||
import { InheritancePreserver } from './InheritancePreserver.js'; | ||
import { findOrCreateMetadata } from './findMetadata.js'; | ||
import { Constructor, ImplementationType } from './types.js'; | ||
@@ -9,4 +9,8 @@ import { workWithDiSettings } from './workWithDiSettings.js'; | ||
) { | ||
workWithDiSettings(constructor, modification); | ||
InheritancePreserver.constructorModified(constructor); | ||
workWithDiSettings(constructor, (settings) => { | ||
const metadata = findOrCreateMetadata(settings); | ||
metadata.modifiedClass = constructor as TClass; | ||
modification(settings); | ||
}); | ||
} |
@@ -7,2 +7,4 @@ import { cheapDiSymbol } from './cheapDiSymbol.js'; | ||
type SomeDependency = Dependency | 'unknown'; | ||
type ImplementationType<TClass> = Constructor<TClass> & { | ||
@@ -18,3 +20,3 @@ [Symbol.metadata]: DiMetadataStorage<TClass>; | ||
singleton?: boolean; | ||
dependencies?: Dependency[]; | ||
dependencies?: SomeDependency[]; | ||
modifiedClass?: TClass; | ||
@@ -28,8 +30,8 @@ injected?: unknown[]; | ||
/** add parameters that will be passed to the class constructor */ | ||
with: (...injectionParams: any[]) => void; | ||
inject: (...injectionParams: any[]) => void; | ||
} | ||
interface DependencyRegistrator<RegisterTypeExtension = object, RegisterInstanceExtension = object> { | ||
/** register class */ | ||
registerType: <TClass>(implementationType: ImplementationType<TClass>) => { | ||
/** register implementation class */ | ||
registerImplementation: <TClass>(implementationType: ImplementationType<TClass>) => { | ||
/** as super class */ | ||
@@ -93,3 +95,4 @@ as: <TBase extends Partial<TClass>>(type: RegistrationType<TBase>) => WithInjectionParams; | ||
RegistrationType, | ||
SomeDependency, | ||
WithInjectionParams, | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
504
0
23497
214