Compare commits

..

14 Commits

Author SHA1 Message Date
16bc8e4187 fix: Test 2024-08-28 15:48:45 +02:00
b3de04e3f9 fix: Try another api 2024-08-28 15:46:39 +02:00
e0ab54d38b fix: Fix import paths to relative 2024-08-28 15:12:10 +02:00
45bec44a2d fix: Fix typing in injector 2024-08-28 15:06:38 +02:00
ab07e03c0f version: Fix version number 2024-08-28 13:59:34 +02:00
8bb153fe54 version: Pre release version bump to v0.1.0-.1 2024-08-24 03:22:58 +02:00
Max P.
794c1e07c3 Merge branch 'main' into dev/v1.0.0 2024-08-24 03:16:29 +02:00
1a67d3f4e3 docs: Reflect curent changes to the changelog 2024-08-24 02:40:21 +02:00
042d918d00 test: Add tests to reflect the changes in the signature of the decorators
The identify is no optional with fallback to the property key or class
2024-08-24 02:34:21 +02:00
06ae6737fd feat!: Update Inject Decorator for stable decorator api of typescript
Update the function and refactor the typing to reflect the property type
2024-08-24 02:32:58 +02:00
81873f3689 feat!: Update RegisterInstance Decorator for stable decorator api of typescript 2024-08-24 02:21:50 +02:00
4aa12321e8 feat!: Update Register Decorator for stable decorator api of typescript 2024-08-24 02:18:16 +02:00
26b35d1e5b refactor: Add error class for missing identifiers 2024-08-24 02:16:02 +02:00
78564b9a76 refactor!: Disable experimentalDecorators and emitDecoratorMetadata in tsconfig 2024-08-24 02:02:41 +02:00
9 changed files with 222 additions and 133 deletions

View File

@@ -9,17 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- feat: Add new `IdentifierRequiredError` class for missing identifiers.
- feat: Add the option to use the decorators without passing the identifier: In this case, the identifier will be the class name (register) or the property name (inject).
- Add pre release building to release workflow on dev/* branches an version changes.
### Deprecated
### Removed
- feat!: Disable `experimentalDecorators` and `emitDecoratorMetadata` in the `tsconfig.json` file to reflect the change to the stable decorators api.
### Fixed
- feat!: Update `Register`, `RegisterInstance` and `Inject` decorators to reflect the change to the stable decorators api.
- feat!: Update `Inject` Decorator typing to reflect the correct property type.
### Security

55
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ts-injex",
"version": "0.1.0",
"version": "0.1.0-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ts-injex",
"version": "0.1.0",
"version": "0.1.0-alpha",
"license": "MIT",
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
@@ -20,7 +20,7 @@
"@typescript-eslint/parser": "^8.1.0",
"axios": "^1.7.2",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^50.2.2",
"eslint-plugin-override": "https://github.com/PxaMMaxP/eslint-plugin-override",
"istanbul-badges-readme": "^1.9.0",
@@ -1187,12 +1187,6 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true
},
"node_modules/@shikijs/core": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.13.0.tgz",
@@ -3001,9 +2995,9 @@
}
},
"node_modules/eslint-module-utils": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
"integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
"dev": true,
"dependencies": {
"debug": "^3.2.7"
@@ -3163,36 +3157,34 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
"array.prototype.findlastindex": "^1.2.5",
"array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.3",
"array.prototype.flat": "^1.3.2",
"array.prototype.flatmap": "^1.3.2",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.12.0",
"hasown": "^2.0.2",
"is-core-module": "^2.15.1",
"eslint-module-utils": "^2.8.0",
"hasown": "^2.0.0",
"is-core-module": "^2.13.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"object.groupby": "^1.0.3",
"object.values": "^1.2.0",
"object.fromentries": "^2.0.7",
"object.groupby": "^1.0.1",
"object.values": "^1.1.7",
"semver": "^6.3.1",
"string.prototype.trimend": "^1.0.8",
"tsconfig-paths": "^3.15.0"
},
"engines": {
"node": ">=4"
},
"peerDependencies": {
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
}
},
"node_modules/eslint-plugin-import/node_modules/debug": {
@@ -4161,9 +4153,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
@@ -5966,6 +5958,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "ts-injex",
"version": "0.1.0",
"version": "0.1.0-alpha",
"description": "Simple boilerplate code free dependency injection system for TypeScript.",
"type": "module",
"main": "./dist/index.js",
@@ -40,7 +40,7 @@
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^50.2.2",
"eslint-plugin-override": "https://github.com/PxaMMaxP/eslint-plugin-override",
"jest": "^29.7.0",

View File

@@ -1,9 +1,9 @@
/* istanbul ignore file */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Inject } from 'src/decorators/Inject';
import { DependencyResolutionError } from 'src/interfaces/Exceptions';
import { ForceConstructor } from 'src/types/GenericContructor';
import { DependencyResolutionError } from '../interfaces/Exceptions';
import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex';
import { ForceConstructor } from '../types/GenericContructor';
/**
* Test the Inject decorator.
@@ -67,6 +67,29 @@ export function test_InjectDecorator(
expect(instance.getDependency().value).toBe('test-value-init');
});
it('should inject dependency and run initializer without identifier', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject(undefined, (x: string) => {
(x as unknown as { value: string }).value =
'test-value-init';
return x;
})
MockDependencyIdentifier!: any;
public getDependency() {
return this.MockDependencyIdentifier;
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value-init');
});
it('should throw an error when necessary is true and the initializer throws an error', () => {
let _error: Error | undefined = undefined;
@@ -78,7 +101,7 @@ export function test_InjectDecorator(
class TestClass {
@Inject(
'InitThrowDependencie',
() => {
(): any => {
throw new Error('Initializer error');
},
true,
@@ -275,6 +298,19 @@ export function test_RegisterDecorator(
TestClass,
);
});
it('should register a dependency without an identifier', () => {
@register()
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(container.resolve('TestClass')).toBe(TestClass);
});
});
}
@@ -311,6 +347,23 @@ export function test_RegisterInstanceDecorator(
).toBe('instance');
});
it('should register an instance of a dependency with an identifier', () => {
@registerInstance()
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = 'instance';
}
expect(container.resolve<TestClass>('TestClass').mark).toBe(
'instance',
);
});
it('should register an instance of a dependency an run the init function', () => {
@registerInstance(
'InstanceIdentifier',

View File

@@ -1,26 +1,27 @@
import { TSinjex } from '../classes/TSinjex';
import {
DependencyResolutionError,
IdentifierRequiredError,
InitializationError,
InjectorError,
NoInstantiationMethodError,
} from 'src/interfaces/Exceptions';
import { TSinjex } from '../classes/TSinjex';
} from '../interfaces/Exceptions';
import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate';
/**
* A decorator to inject a dependency from a DI (Dependency Injection) container into a class property.
* @template T The type of the dependency to be injected.
* @template U The type of the property to be injected.
* @param identifier The identifier used to resolve the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param init Optional an initializer function to transform the dependency before injection
* @template TargetType The type of the class to inject the dependency into.
* @template DependencyType The type of the dependency to be injected.
* @template PropertyType The type of the property to be injected.
* @param identifier The {@link Identifier|identifier} used to resolve the dependencie in the DI container or the property name if not provided.
* @param init An optional initializer {@link InitDelegate|function} to transform the dependency before injection
* or true to instantiate the dependency if it has a constructor.
* @see {@link InitDelegate} for more information on initializer functions.
* @param necessary If true, throws an error if the dependency is not found.
* @returns The resolved dependency or undefined if the dependency is not necessary
* and not found, or throws an error if the dependency is necessary and not found.
* @throws **Only throws errors if the dependency is necessary.**
* @throws An {@link IdentifierRequiredError} if the identifier is not provided and the class name is not available.
* @throws A {@link DependencyResolutionError} if the dependency is not found.
* @throws A {@link InjectorError} if an error occurs during the injection process.
* @throws A {@link NoInstantiationMethodError} if the dependency does not have a constructor.
@@ -40,71 +41,83 @@ import { InitDelegate } from '../types/InitDelegate';
* }
* ```
*/
export function Inject<T, U>(
identifier: Identifier,
init?: InitDelegate<T, U> | true,
export function Inject<TargetType, DependencyType, PropertyType>(
identifier?: Identifier,
init?: InitDelegate<DependencyType, PropertyType> | true,
necessary = true,
) {
return function (target: unknown, propertyKey: string | symbol): void {
return function (
constructor: undefined,
context: ClassFieldDecoratorContext<TargetType>,
): (
this: TargetType,
initialValue: PropertyType | undefined,
) => PropertyType {
const _identifier = identifier ?? context.name;
if (_identifier == null && necessary === true)
throw new IdentifierRequiredError();
/**
* Function to evaluate the dependency lazily
* to avoid circular dependencies, not found dependencies, etc.
* @returns The resolved dependency or undefined if the dependency is not found.
*/
const resolve = (): T | undefined => {
return TSinjex.getInstance().resolve<T>(identifier, necessary);
const resolve = (): DependencyType | undefined => {
return TSinjex.getInstance().resolve<DependencyType>(
_identifier,
necessary,
);
};
Object.defineProperty(target, propertyKey, {
get() {
let instance: T | U | undefined;
return function (
this: TargetType,
initialValue: PropertyType | undefined,
): PropertyType {
let instance: DependencyType | PropertyType | undefined;
const dependency: T | undefined = tryAndCatch(
() => resolve(),
necessary,
identifier,
DependencyResolutionError,
);
const dependency: DependencyType | undefined = tryAndCatch(
() => resolve(),
necessary,
_identifier,
DependencyResolutionError,
);
if (dependency != null) {
const initFunction: (() => U) | undefined =
typeof init === 'function' && dependency != null
? (): U => init(dependency)
: init === true && hasConstructor(dependency)
? (): U => new dependency() as U
: undefined;
if (dependency != null) {
const initFunction: (() => PropertyType) | undefined =
typeof init === 'function' && dependency != null
? (): PropertyType => init(dependency)
: init === true && hasConstructor(dependency)
? (): PropertyType => new dependency() as PropertyType
: undefined;
if (init == null) instance = dependency;
else if (initFunction != null)
instance = tryAndCatch(
initFunction,
necessary,
identifier,
InitializationError,
);
else if (necessary)
throw new NoInstantiationMethodError(identifier);
} else if (necessary)
throw new DependencyResolutionError(identifier);
if (init == null) instance = dependency;
else if (initFunction != null)
instance = tryAndCatch(
initFunction,
necessary,
_identifier,
InitializationError,
);
else if (necessary)
throw new NoInstantiationMethodError(_identifier);
} else if (necessary)
throw new DependencyResolutionError(_identifier);
/**
* Replace itself with the resolved dependency
* for performance reasons.
*/
Object.defineProperty(this, propertyKey, {
value: instance,
writable: false,
enumerable: false,
configurable: false,
});
return instance;
},
/**
* Make the property configurable to allow replacing it
* Replace itself with the resolved dependency
* for performance reasons.
*/
configurable: true,
});
Object.defineProperty(this, context.name, {
value: instance,
writable: false,
enumerable: false,
configurable: false,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return instance as any;
};
};
}

View File

@@ -1,14 +1,14 @@
import { TSinjex } from '../classes/TSinjex';
import { IdentifierRequiredError } from '../interfaces/Exceptions';
import { Identifier } from '../types/Identifier';
/**
* A decorator to register a class in the **TSinjex** DI (Dependency Injection) container.
* @template TargetType The type of the class to be registered.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @param identifier The {@link Identifier|identifier} used to register the class in the DI container or the class name if not provided.
* @param deprecated If true, the dependency is deprecated and a warning is logged only once upon the first resolution of the dependency.
* @returns The decorator function to be applied on the class.
* @throws An {@link IdentifierRequiredError} if the identifier is not provided and the class name is not available.
* @example
* ```ts
* \@Register('MyClassIdentifier')
@@ -19,12 +19,16 @@ import { Identifier } from '../types/Identifier';
*/
export function Register<
TargetType extends new (...args: unknown[]) => InstanceType<TargetType>,
>(identifier: Identifier, deprecated?: boolean) {
return function (constructor: TargetType, ...args: unknown[]): void {
// Get the instance of the DI container
const diContainer = TSinjex.getInstance();
>(identifier?: Identifier, deprecated?: boolean) {
return function (
constructor: TargetType,
context: ClassDecoratorContext<TargetType>,
) {
const _identifier = identifier ?? context.name;
// Register the class in the DI container
diContainer.register(identifier, constructor, deprecated);
if (_identifier == null) throw new IdentifierRequiredError();
const diContainer = TSinjex.getInstance();
diContainer.register(_identifier, constructor, deprecated);
};
}

View File

@@ -1,16 +1,16 @@
import { TSinjex } from '../classes/TSinjex';
import { IdentifierRequiredError } from '../interfaces/Exceptions';
import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate';
/**
* A decorator to register an instance of a class in the DI (Dependency Injection) container.
* @template TargetType The type of the class whose instance is to be registered.
* @param identifier The identifier used to register the instance in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param init An optional initializer function which get the constructor of the class
* @param identifier The {@link Identifier|identifier} used to register the class in the DI container or the class name if not provided.
* @param init An optional initializer {@link InitDelegate|function} which get the constructor of the class
* as input and returns an instance of the class.
* @see {@link InitDelegate} for more information on initializer functions.
* @returns The decorator function to be applied on the class.
* @throws An {@link IdentifierRequiredError} if the identifier is not provided and the class name is not available.
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
@@ -22,43 +22,51 @@ import { InitDelegate } from '../types/InitDelegate';
export function RegisterInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
identifier?: Identifier,
init?: InitDelegate<
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
InstanceType<TargetType>
>,
) {
return function (constructor: TargetType, ...args: unknown[]): void {
// Get the instance of the DI container
return function (
constructor: TargetType,
context: ClassDecoratorContext<TargetType>,
): void {
const _identifier = identifier ?? context.name;
if (_identifier == null) throw new IdentifierRequiredError();
const diContainer = TSinjex.getInstance();
let instance: InstanceType<TargetType>;
/**
* Get the instance of the class
* and replace the lazy proxy with the instance
* for performance optimization.
*/
const getAndRegisterInstance = (): void => {
if (instance == null) {
if (init) {
instance = init(constructor);
} else {
instance = new constructor();
}
}
diContainer.register(_identifier, instance);
};
// Create a proxy to instantiate the class when needed (Lazy Initialization)
let lazyProxy: unknown = new Proxy(
const lazyProxy: unknown = new Proxy(
{},
{
get(target, prop, receiver) {
if (instance == null) {
if (init) {
instance = init(constructor);
} else {
instance = new constructor(...args);
}
}
lazyProxy = instance;
get(_target, prop, _receiver) {
getAndRegisterInstance();
// Return the requested property of the instance
return instance[prop as keyof InstanceType<TargetType>];
},
set(target, prop, value, receiver) {
if (instance == null) {
if (init) {
instance = init(constructor);
} else {
instance = new constructor(...args);
}
}
lazyProxy = instance;
set(_target, prop, value, _receiver) {
getAndRegisterInstance();
// Set the requested property of the instance
return (instance[prop as keyof InstanceType<TargetType>] =
@@ -67,7 +75,6 @@ export function RegisterInstance<
},
);
// Register the lazy proxy in the DI container
diContainer.register(identifier, lazyProxy);
diContainer.register(_identifier, lazyProxy);
};
}

View File

@@ -15,6 +15,19 @@ export class TSinjexError extends Error {
}
}
/**
* Error class for missing identifiers in {@link ITSinjex} methods.
*/
export class IdentifierRequiredError extends TSinjexError {
/**
* Creates a new instance of {@link IdentifierRequiredError}
*/
constructor() {
super('Identifier is required.');
this.name = 'TSinjexIdentifierRequiredError';
}
}
/**
* Error class for dependency resolution errors in {@link ITSinjex}.
* @see {@link ITSinjex.resolve}

View File

@@ -18,8 +18,6 @@
"importHelpers": true,
"isolatedModules": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"lib": [