Compare commits

..

5 Commits

Author SHA1 Message Date
20Max01
95ef003e99 Feature/integrate register instance in register (#2)
* refactor: consolidate registration decorators

- Introduced `Register` decorator to handle class and instance registration in the DI container.
- Deprecated `RegisterInstance` in favor of `Register`, which now internally handles instance registration.
- Added support for marking dependencies as deprecated with a warning logged upon first resolution.
- Updated documentation with examples and notes on deprecation.

* tests: add mode parameter to RegisterInstanceDecorator

- Introduced a `mode` parameter to the `test_RegisterInstanceDecorator` function allowing 'instance' or 'standalone' modes.
- Updated test cases to utilize the new `mode` parameter when registering an instance.
- Disabled specific ESLint rule in `Decorators.test.ts` for deprecation warnings.
- Added an additional test call to `test_RegisterInstanceDecorator` with 'instance' mode.

* docs: Reflect changes to changelog

* refactor: add region tags for overloads in Register.ts

* docs: Reflect changes to changelog

* refactor: consolidate registration decorators

- Introduced `Register` decorator to handle class and instance registration in the DI container.
- Deprecated `RegisterInstance` in favor of `Register`, which now internally handles instance registration.
- Added support for marking dependencies as deprecated with a warning logged upon first resolution.
- Updated documentation with examples and notes on deprecation.

* tests: add mode parameter to RegisterInstanceDecorator

- Introduced a `mode` parameter to the `test_RegisterInstanceDecorator` function allowing 'instance' or 'standalone' modes.
- Updated test cases to utilize the new `mode` parameter when registering an instance.
- Disabled specific ESLint rule in `Decorators.test.ts` for deprecation warnings.
- Added an additional test call to `test_RegisterInstanceDecorator` with 'instance' mode.

* docs: Reflect changes to changelog

* refactor: add region tags for overloads in Register.ts

* docs: Reflect changes to changelog

* docs: Reflect changes to changelog und push version
2025-03-14 13:43:10 +01:00
9660d77e0c docs: push version to 0.2.0 2025-03-14 13:36:29 +01:00
dce76a5fb3 docs: reflect new cli command to changelog 2025-03-14 13:36:29 +01:00
23e6248299 feat: Introduced a new CLI command tsinjex-generate to automate the generation of import statements for registered dependencies. 2025-03-14 13:36:29 +01:00
d8fb82943f feat: Introduced a new script to automate the generation of import statements for registered dependencies 2025-03-14 13:36:29 +01:00
10 changed files with 487 additions and 220 deletions

View File

@@ -9,24 +9,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### 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 ### Deprecated
### Removed ### Removed
- feat!: Disable `experimentalDecorators` and `emitDecoratorMetadata` in the `tsconfig.json` file to reflect the change to the stable decorators api.
### Fixed ### 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 ### Security
## [0.3.0]
### Added
- refactor: consolidate registration decorators
Introduced Register decorator to handle class and instance registration in the DI container.
Deprecated RegisterInstance in favor of Register, which now internally handles instance registration.
Added support for marking dependencies as deprecated with a warning logged upon first resolution.
Updated documentation with examples and notes on deprecation.
- tests: add mode parameter to RegisterInstanceDecorator
Introduced a mode parameter to the test_RegisterInstanceDecorator function allowing 'instance' or 'standalone' modes.
Updated test cases to utilize the new mode parameter when registering an instance.
Disabled specific ESLint rule in Decorators.test.ts for deprecation warnings.
Added an additional test call to test_RegisterInstanceDecorator with 'instance' mode.
- refactor: add region tags for overloads in Register.ts
## [0.2.0]
### Added
- Add pre release building to release workflow on dev/* branches an version changes.
- feat: Introduced a new CLI command `tsinjex-generate` to automate the generation of import statements for registered dependencies.
The command scans `.ts` files for `@Register` and `@RegisterInstance` decorators and generates an `auto-imports.ts` file.
This ensures that all registered dependencies are automatically included without requiring manual imports.
The CLI can be executed via `npx tsinjex-generate` or added as a script in `package.json` for easier integration.
### Deprecated
### Removed
### Fixed
### Security
## [0.0.14] ## [0.0.14]
@@ -59,4 +90,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
--- ---
[unreleased]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...HEAD [unreleased]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...HEAD
[0.0.14]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.13...v0.0.14 [0.0.14]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.13...v0.0.14
[0.2.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...v0.2.0
[0.3.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.2.0...v0.3.0

111
bin/generate-imports.cjs Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const yargs = require('yargs');
// CLI argument parsing
const argv = yargs
.option('src', {
alias: 's',
type: 'string',
description: 'Directory to search for files',
default: 'src',
})
.option('output', {
alias: 'o',
type: 'string',
description: 'Path to the output file',
default: 'src/auto-imports.ts',
})
.option('pattern', {
alias: 'p',
type: 'string',
description: 'File pattern to search for (e.g., .ts, .js)',
default: '.ts',
})
.help()
.argv;
// Fixed RegEx patterns for decorator detection
const SEARCH_PATTERNS = [
/^@Register(?:<(.+)?>)?\(\s*["']{1}(.+)?["']{1}\s*,?\s*(true|false)?\s*\)/m, // Matches @Register(...)
/^@RegisterInstance(?:<(.+)?>)?\(\s*["']{1}(.+)?["']{1}\s*,?\s*(.+)?\s*\)/m, // Matches @RegisterInstance(...)
];
const FILE_PATTERN = argv.pattern.startsWith('.') ? argv.pattern : `.${argv.pattern}`; // Ensure the pattern starts with a dot
/**
* Recursively searches for all files in a directory matching the specified pattern.
* @param {string} dirPath - The directory to search.
* @returns {string[]} - List of matching files.
*/
function getAllFiles(dirPath) {
let files = fs.readdirSync(dirPath);
let arrayOfFiles = [];
files.forEach((file) => {
const fullPath = path.join(dirPath, file);
if (fs.statSync(fullPath).isDirectory()) {
arrayOfFiles = arrayOfFiles.concat(getAllFiles(fullPath));
} else if (file.endsWith(FILE_PATTERN)) {
arrayOfFiles.push(fullPath);
}
});
return arrayOfFiles;
}
/**
* Filters files that contain at least one of the specified regex patterns.
* @param {string[]} files - List of files to check.
* @returns {string[]} - Files that contain at least one of the specified patterns.
*/
function findFilesWithPattern(files) {
return files.filter((file) => {
const content = fs.readFileSync(file, 'utf8');
return SEARCH_PATTERNS.some((pattern) => pattern.test(content));
});
}
/**
* Generates an import file containing imports for all found files.
* @param {string[]} files - List of relevant files.
* @returns {string} - Generated import code.
*/
function generateImports(files) {
return files.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n') + '\n';
}
/**
* Main function that executes the script.
*/
function main() {
try {
const srcDir = path.resolve(process.cwd(), argv.src);
const outputFile = path.resolve(process.cwd(), argv.output);
if (!fs.existsSync(srcDir)) {
console.error(`❌ Error: The directory '${srcDir}' does not exist.`);
process.exit(1);
}
const allFiles = getAllFiles(srcDir);
const filesWithPattern = findFilesWithPattern(allFiles);
if (filesWithPattern.length === 0) {
console.log(`ℹ️ No ${FILE_PATTERN} files found matching the specified decorator patterns.`);
return;
}
const importContent = generateImports(filesWithPattern);
fs.writeFileSync(outputFile, importContent);
console.log(`✅ Imports successfully generated: ${outputFile}`);
} catch (error) {
console.error('❌ An error occurred:', error.message);
process.exit(1);
}
}
main();

View File

@@ -1,10 +1,13 @@
{ {
"name": "ts-injex", "name": "ts-injex",
"version": "0.1.0-.1", "version": "0.3.0",
"description": "Simple boilerplate code free dependency injection system for TypeScript.", "description": "Simple boilerplate code free dependency injection system for TypeScript.",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"bin": {
"tsinjex-generate": "./bin/generate-imports.cjs"
},
"scripts": { "scripts": {
"prepare": "npm run build", "prepare": "npm run build",
"build": "npm run build:tsc", "build": "npm run build:tsc",
@@ -60,4 +63,4 @@
"LICENSE", "LICENSE",
"package.json" "package.json"
] ]
} }

View File

@@ -67,29 +67,6 @@ export function test_InjectDecorator(
expect(instance.getDependency().value).toBe('test-value-init'); 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', () => { it('should throw an error when necessary is true and the initializer throws an error', () => {
let _error: Error | undefined = undefined; let _error: Error | undefined = undefined;
@@ -101,7 +78,7 @@ export function test_InjectDecorator(
class TestClass { class TestClass {
@Inject( @Inject(
'InitThrowDependencie', 'InitThrowDependencie',
(): any => { () => {
throw new Error('Initializer error'); throw new Error('Initializer error');
}, },
true, true,
@@ -298,19 +275,6 @@ export function test_RegisterDecorator(
TestClass, 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);
});
}); });
} }
@@ -318,6 +282,7 @@ export function test_RegisterInstanceDecorator(
Container: ITSinjex_, Container: ITSinjex_,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
registerInstance: Function, registerInstance: Function,
mode: 'instance' | 'standalone' = 'standalone',
): void { ): void {
describe('RegisterInstance Decorator Tests', () => { describe('RegisterInstance Decorator Tests', () => {
let container: ITSinjex; let container: ITSinjex;
@@ -331,7 +296,10 @@ export function test_RegisterInstanceDecorator(
}); });
it('should register an instance of a dependency', () => { it('should register an instance of a dependency', () => {
@registerInstance('InstanceIdentifier') @registerInstance(
'InstanceIdentifier',
mode === 'instance' ? 'instance' : undefined,
)
class TestClass { class TestClass {
private readonly _dependency!: any; private readonly _dependency!: any;
@@ -347,23 +315,6 @@ export function test_RegisterInstanceDecorator(
).toBe('instance'); ).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', () => { it('should register an instance of a dependency an run the init function', () => {
@registerInstance( @registerInstance(
'InstanceIdentifier', 'InstanceIdentifier',
@@ -390,7 +341,10 @@ export function test_RegisterInstanceDecorator(
}); });
it('should register an instance of a dependency and get it on set', () => { it('should register an instance of a dependency and get it on set', () => {
@registerInstance('InstanceIdentifier') @registerInstance(
'InstanceIdentifier',
mode === 'instance' ? 'instance' : undefined,
)
class TestClass { class TestClass {
private readonly _dependency!: any; private readonly _dependency!: any;

View File

@@ -1,3 +1,4 @@
/* eslint-disable deprecation/deprecation */
import { TSinjex } from 'src/classes/TSinjex'; import { TSinjex } from 'src/classes/TSinjex';
import { Inject } from 'src/decorators/Inject'; import { Inject } from 'src/decorators/Inject';
import { Register } from 'src/decorators/Register'; import { Register } from 'src/decorators/Register';
@@ -13,3 +14,5 @@ test_InjectDecorator(TSinjex, Inject);
test_RegisterDecorator(TSinjex, Register); test_RegisterDecorator(TSinjex, Register);
test_RegisterInstanceDecorator(TSinjex, RegisterInstance); test_RegisterInstanceDecorator(TSinjex, RegisterInstance);
test_RegisterInstanceDecorator(TSinjex, Register, 'instance');

View File

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

View File

@@ -1,14 +1,142 @@
import { IdentifierRequiredError } from 'src/interfaces/Exceptions'; import { InitDelegate } from 'src/types/InitDelegate';
import { TSinjex } from '../classes/TSinjex'; import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier'; import { Identifier } from '../types/Identifier';
//#region Overloads
/** /**
* A decorator to register a class in the **TSinjex** DI (Dependency Injection) container. * A decorator to register a class in the **TSinjex** DI (Dependency Injection) container.
* @template TargetType The type of the class to be registered. * @template TargetType The type of the class to be registered.
* @param identifier The {@link Identifier|identifier} used to register the class in the DI container or the class name if not provided. * @param identifier The identifier used to register the class in the DI container.
* @param deprecated If true, the dependency is deprecated and a warning is logged only once upon the first resolution of the dependency. * @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.
* @returns The decorator function to be applied on the class.
* @example
* ```ts
* \@Register('MyClassIdentifier')
* class MyClass {
* // ...
* }
* ```
* @example
* ```ts
* \@Register('MyClassIdentifier', true)
* class MyClass {
* // ...
* }
* ```
*/
export function Register<
TargetType extends new (...args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
deprecated?: boolean,
): (constructor: TargetType, ...args: unknown[]) => void;
/**
* 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 shouldRegister Set to 'instance' to register the instance in the DI container
* with an empty constructor.
* @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.
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', 'instance')
* class MyClass {
* // ...
* }
* ```
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', 'instance', true)
* class MyClass {
* // ...
* }
* ```
*/
export function Register<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
shouldRegister: 'instance',
deprecated?: boolean,
): (constructor: TargetType, ...args: unknown[]) => void;
/**
* 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
* as input and returns an instance of the class.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @see {@link InitDelegate} for more information on initializer functions.
* @returns The decorator function to be applied on the class.
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
* class MyClass {
* // ...
* }
* ```
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor(), true)
* class MyClass {
* // ...
* }
* ```
*/
export function Register<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
init?: InitDelegate<
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
InstanceType<TargetType>
>,
deprecated?: boolean,
): (constructor: TargetType, ...args: unknown[]) => void;
//#endregion Overloads
// eslint-disable-next-line jsdoc/require-jsdoc
export function Register<
TargetType extends new (...args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
arg1?:
| undefined
| boolean
| InitDelegate<TargetType, InstanceType<TargetType>>
| 'instance',
arg2?: boolean,
): (constructor: TargetType, ...args: unknown[]) => void {
const deprecated = typeof arg1 === 'boolean' ? arg1 : arg2;
const init = typeof arg1 === 'function' ? arg1 : undefined;
const shouldRegisterInstance = arg1 === 'instance';
if (init == undefined && shouldRegisterInstance !== true) {
return _register(identifier, deprecated);
} else {
return _registerInstance(identifier, init, deprecated);
}
}
/**
* 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.
* @returns The decorator function to be applied on the class. * @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 * @example
* ```ts * ```ts
* \@Register('MyClassIdentifier') * \@Register('MyClassIdentifier')
@@ -17,18 +145,122 @@ import { Identifier } from '../types/Identifier';
* } * }
* ``` * ```
*/ */
export function Register< function _register<
TargetType extends new (...args: unknown[]) => InstanceType<TargetType>, TargetType extends new (...args: unknown[]) => InstanceType<TargetType>,
>(identifier?: Identifier, deprecated?: boolean) { >(identifier: Identifier, deprecated?: boolean) {
return function ( return function (constructor: TargetType, ...args: unknown[]): void {
constructor: TargetType, // Get the instance of the DI container
context: ClassDecoratorContext<TargetType>,
) {
const _identifier = identifier ?? context.name;
if (_identifier == null) throw new IdentifierRequiredError();
const diContainer = TSinjex.getInstance(); const diContainer = TSinjex.getInstance();
diContainer.register(_identifier, constructor, deprecated);
// Register the class in the DI container
diContainer.register(identifier, constructor, deprecated);
}; };
} }
/**
* 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
* as input and returns an instance of the class.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @see {@link InitDelegate} for more information on initializer functions.
* @returns The decorator function to be applied on the class.
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
* class MyClass {
* // ...
* }
* ```
*/
function _registerInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
init?: InitDelegate<
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
InstanceType<TargetType>
>,
deprecated?: boolean,
) {
return function (constructor: TargetType, ...args: unknown[]): void {
// Get the instance of the DI container
const diContainer = TSinjex.getInstance();
let instance: InstanceType<TargetType>;
// Create a proxy to instantiate the class when needed (Lazy Initialization)
let lazyProxy: unknown = new Proxy(
{},
{
get(target, prop, receiver) {
({ instance, lazyProxy } = initializeInstance<TargetType>(
instance,
init,
constructor,
args,
lazyProxy,
));
// Return the requested property of the instance
return instance[prop as keyof InstanceType<TargetType>];
},
set(target, prop, value, receiver) {
({ instance, lazyProxy } = initializeInstance<TargetType>(
instance,
init,
constructor,
args,
lazyProxy,
));
// Set the requested property of the instance
return (instance[prop as keyof InstanceType<TargetType>] =
value);
},
},
);
// Register the lazy proxy in the DI container
diContainer.register(identifier, lazyProxy, deprecated);
};
}
/**
* Initializes the instance of the class.
* @template TargetType The type of the class whose instance is to be initialized.
* @param instance The instance of the class to be initialized.
* @param init The optional initializer function to initialize the instance.
* @param constructor The constructor of the class.
* @param args The arguments to be passed to the constructor of the class.
* @param lazyProxy The lazy proxy to instantiate the class when needed.
* @returns The initialized instance and the lazy proxy.
*/
function initializeInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
instance: InstanceType<TargetType>,
init:
| InitDelegate<
TargetType &
(new (..._args: unknown[]) => InstanceType<TargetType>),
InstanceType<TargetType>
>
| undefined,
constructor: TargetType,
args: unknown[],
lazyProxy: unknown,
): { instance: InstanceType<TargetType>; lazyProxy: unknown } {
if (instance == null) {
if (init) {
instance = init(constructor);
} else {
instance = new constructor(...args);
}
}
lazyProxy = instance;
return { instance, lazyProxy };
}

View File

@@ -1,16 +1,18 @@
import { IdentifierRequiredError } from 'src/interfaces/Exceptions'; import { Register } from './Register';
import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier'; import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate'; import { InitDelegate } from '../types/InitDelegate';
/** /**
* A decorator to register an instance of a class in the DI (Dependency Injection) container. * 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. * @template TargetType The type of the class whose instance is to be registered.
* @param identifier The {@link Identifier|identifier} used to register the class in the DI container or the class name if not provided. * @param identifier The identifier used to register the instance in the DI container.
* @param init An optional initializer {@link InitDelegate|function} which get the constructor of the class * @see {@link Identifier} for more information on identifiers.
* @param init An optional initializer function which get the constructor of the class
* as input and returns an instance of the class. * as input and returns an instance of the class.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @see {@link InitDelegate} for more information on initializer functions.
* @returns The decorator function to be applied on the class. * @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 * @example
* ```ts * ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor()) * \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
@@ -18,63 +20,20 @@ import { InitDelegate } from '../types/InitDelegate';
* // ... * // ...
* } * }
* ``` * ```
* @deprecated Use {@link Register} instead. This decorator already uses the {@link Register} decorator internally.
*/ */
export function RegisterInstance< export function RegisterInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>, TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>( >(
identifier?: Identifier, identifier: Identifier,
init?: InitDelegate< init?: InitDelegate<
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> }, TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
InstanceType<TargetType> InstanceType<TargetType>
>, >,
) { deprecated?: boolean,
return function ( ): (constructor: TargetType, ...args: unknown[]) => void {
constructor: TargetType, const initDelegate = typeof init === 'function' ? init : undefined;
context: ClassDecoratorContext<TargetType>,
): void {
const _identifier = identifier ?? context.name;
if (_identifier == null) throw new IdentifierRequiredError(); if (initDelegate) return Register(identifier, initDelegate, deprecated);
else return Register(identifier, 'instance', deprecated);
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)
const lazyProxy: unknown = new Proxy(
{},
{
get(_target, prop, _receiver) {
getAndRegisterInstance();
// Return the requested property of the instance
return instance[prop as keyof InstanceType<TargetType>];
},
set(_target, prop, value, _receiver) {
getAndRegisterInstance();
// Set the requested property of the instance
return (instance[prop as keyof InstanceType<TargetType>] =
value);
},
},
);
diContainer.register(_identifier, lazyProxy);
};
} }

View File

@@ -15,19 +15,6 @@ 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}. * Error class for dependency resolution errors in {@link ITSinjex}.
* @see {@link ITSinjex.resolve} * @see {@link ITSinjex.resolve}

View File

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