Compare commits

...

64 Commits

Author SHA1 Message Date
c55f39d1e5 chore: update version in Deno configuration file
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 20:01:05 +02:00
3ac76b09d3 feat(export): add re-exports for shared modules
- Add re-exports for classes, types, interfaces, decorators, and helpers.
- Remove unused `export.ts` file to streamline module structure.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 20:00:59 +02:00
46c9a8b990 refactor(di): modularize and improve dependency injection for deno
- Consolidate import paths into scoped modules for better structure.
- Refactor decorators (`Inject`, `Register`) for improved type safety.
- Add `clear` method to the DI container for easier test cleanup.
- Introduce lazy initialization for registered instances.
- Add comprehensive unit tests for decorators and DI container.
- Standardize error handling and naming conventions for exceptions.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 19:55:16 +02:00
9e1b7a8d7b refactor: reorganize imports and improve dependency injection decorators; add tests for TSinjex functionality
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 19:55:01 +02:00
3a52f14dcd chore: add initial Deno configuration file
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 19:54:48 +02:00
4fa78afbc2 chore: clean up .gitignore by removing unnecessary entries
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 19:54:36 +02:00
67f587d0d0 chore(repo): remove project configuration and source files
- Delete all project configuration files, including ESLint, Prettier, and TypeScript settings.
- Remove GitHub workflows for CI/CD and documentation deployment.
- Delete source files, tests, and scripts related to the project.
- Clean up package.json and associated scripts.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-02 19:54:24 +02:00
a70869a941 docs: reflect changes to changelog 2025-04-02 22:20:58 +02:00
7a6253a386 docs: Push version to 1.2.0 2025-04-02 22:20:58 +02:00
31de73db00 docs(api): add full JSDoc for all inject() overloads with examples 2025-04-02 22:20:58 +02:00
688a9d4ee4 docs: reflect changes to changelog 2025-04-02 22:01:10 +02:00
39dbd6d816 docs: Push version to 1.1.0 2025-04-02 22:01:10 +02:00
0718ff9d68 feat(api): expose inject() function via index.ts 2025-04-02 22:01:10 +02:00
115b3181e0 docs: reflect changes to changelog 2025-04-02 21:52:36 +02:00
c7fa78270c chore: update package-lock.json 2025-04-02 21:52:36 +02:00
3401656219 test(inject): add test suite to verify behavior of new inject() function 2025-04-02 21:52:36 +02:00
7e7fd996b3 feat(core): add inject() function to programmatically inject dependencies 2025-04-02 21:52:36 +02:00
a6fabc329b refactor(resolve): rename necessary parameter to isNecessary for clarity 2025-04-02 21:52:36 +02:00
81fb7f0071 refactor(inject): rename necessary parameter to isNecessary for clarity 2025-04-02 21:52:36 +02:00
2e89cdd619 test(config): modernize jest setup for ESM 2025-04-02 21:52:36 +02:00
b157f48261 fix: imports in tests 2025-04-02 21:52:36 +02:00
bbf68bff34 docs: reflect changes to changelog 2025-04-02 21:52:36 +02:00
b994a074c0 docs: Push version to 1.0.0 2025-04-02 21:52:36 +02:00
1da8efff94 feat(cli): add --without-extension flag to control import path formatting 2025-04-02 21:52:36 +02:00
c6e9fbd2a3 feat!: switch to native ESM with NodeNext module resolution and .js import paths
BREAKING CHANGE: Consumers must use ESM-compatible environments.
All import paths now include .js extensions.
CommonJS (require) is no longer supported.
2025-04-02 21:52:36 +02:00
9bd899581f docs: reflect new export to changelog 2025-04-02 20:22:07 +02:00
b43e4ee12f docs: push version to 0.4.0 2025-04-02 20:22:07 +02:00
ef4a2295bf feat: Export ImplementsStatic helper function 2025-04-02 20:22:07 +02:00
f4f8c7b78e docs: Fix changelog links 2025-03-14 13:52:53 +01:00
beaad4f65e docs: Fix changelog links 2025-03-14 13:51:17 +01:00
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
2942e15fcf docs: Reflect changes to changelog 2024-08-24 03:13:56 +02:00
1d94b33542 ci: Add a build option for pre release buils from dev/* branches 2024-08-24 03:13:56 +02:00
08d58b2d41 build: Add new complete prepare deploy command 2024-08-24 03:13:56 +02:00
19f7be1e3d ci: Disable branch validation workflow 2024-08-23 21:34:18 +02:00
Max P.
a490ea980a Revert "feat: Update Identifier documentation in TypeScript"
This reverts commit 6e6a521c1f.
2024-08-23 21:23:56 +02:00
Max P.
d69eacf9be Revert "docs: Add Identifiers Changelog entry"
This reverts commit 099c8dbfa3.
2024-08-23 21:23:56 +02:00
099c8dbfa3 docs: Add Identifiers Changelog entry 2024-08-23 19:24:47 +02:00
6e6a521c1f feat: Update Identifier documentation in TypeScript 2024-08-23 19:24:47 +02:00
ac99f7d306 ci: Fix release naming in workflow 2024-08-23 00:25:57 +02:00
58767b85f7 docs: Reflect Version change in Changelog 2024-08-23 00:23:56 +02:00
5e095d7b09 docs: Push version to 0.1.0 2024-08-23 00:17:14 +02:00
32126c0784 docs: Add changelog entrys 2024-08-23 00:17:14 +02:00
5df69c219e test: Add additional tests for Inject decorator 2024-08-23 00:17:14 +02:00
5bc9aef9ad feat: Add initialization error handling and refactor Inject 2024-08-23 00:17:14 +02:00
ae9f25fe94 feat: Add new Error InitializationError to reflect errors during initialization of a dependency 2024-08-23 00:17:14 +02:00
4a97a46aed docs: Added **ChangeLog** file 2024-08-23 00:17:14 +02:00
e0542214c0 docs: Add Identifiers and Jest Sections to the README.md file 2024-08-23 00:17:14 +02:00
e6e304dfc7 ci: changed version format to prefix the version wth v 2024-08-23 00:17:14 +02:00
75333b3310 Add error handling and constructor checks to Inject
- Import additional exception classes from `src/interfaces/Exceptions`
- Modify `Inject` function to:
  - Accept `init` parameter as a function or `true` for instantiation
  - Throw specific errors: `DependencyResolutionError`, `InjectorError`, `NoInstantiationMethodError`
  - Ensure necessary dependencies are handled properly
  - Define property with `Object.defineProperty` for performance
- Add `hasConstructor` helper function to check if an object has a constructor
2024-08-23 00:17:14 +02:00
ba9ec70c77 Add InjectorError and NoInstantiationMethodError classes
- Added `InjectorError` class for handling injector errors
- Added `NoInstantiationMethodError` class for missing instantiation methods
- Both classes extend `TSinjexError` and provide detailed error messages
2024-08-23 00:17:14 +02:00
6f20913e4a Update DependencyResolutionError to use Identifier type
- Import `Identifier` from `src/types/Identifier`
- Change `identifier` parameter type in `DependencyResolutionError` constructor from `string` to `Identifier`
- Update error message to call `identifier.toString()`
2024-08-23 00:17:14 +02:00
c5ea21356c Update Identifier type to include symbol
- Modified `Identifier` type in `src/types/Identifier.ts` from `string` to `string | symbol`
2024-08-23 00:17:14 +02:00
9a4fdecaac Update warning message in TSinjex class
- Converted `identifier` to string in the deprecation warning.
2024-08-23 00:17:14 +02:00
6f189942f6 Ignore test files in Istanbul coverage reporting
- Added `/* istanbul ignore file */` comment to `Decorators.spec.ts`, `Functions.spec.ts`, `ITSinjex.spec.ts`
2024-08-23 00:17:14 +02:00
27cdbeb37b Release version 0.0.13
Bumped version from 0.0.12 to 0.0.13 in package.json to reflect recent updates and improvements. No other changes to code or dependencies.
2024-08-16 18:48:26 +02:00
567d1c5bd2 Refactor import paths for 'Identifier' type
Unified the import paths of the 'Identifier' type across multiple files to ensure consistency. The 'Identifier' type is now imported from '../types/Identifier' instead of 'src/types/Identifier'. This change reduces ambiguity and aligns the import pattern throughout the codebase.
2024-08-16 18:48:26 +02:00
942e1079f6 Merge branch 'main' of https://github.com/PxaMMaxP/TSinjex 2024-08-16 18:43:24 +02:00
37b2eb4da2 Bump package version to 0.0.12
Updated the package version from 0.0.11 to 0.0.12 to reflect recent changes and improvements in the codebase. This helps ensure version clarity and proper dependency management.
2024-08-16 18:43:01 +02:00
a8fd55befd Bump package version to 0.0.12
Updated the package version from 0.0.11 to 0.0.12 to reflect recent changes and improvements in the codebase. This helps ensure version clarity and proper dependency management.
2024-08-16 18:41:45 +02:00
52 changed files with 793 additions and 8824 deletions

View File

@@ -1,13 +0,0 @@
node_modules/
main.js
**/*.js
*.js
**/*.mjs
*.mjs
dist/*
*.cjs

299
.eslintrc
View File

@@ -1,299 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": {
"node": true
},
"plugins": [
"@typescript-eslint",
"deprecation",
"prettier",
"import",
"jsdoc",
"override",
"@stylistic"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jsdoc/recommended-typescript"
],
"parserOptions": {
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"override/require-override": "error",
"override/require-static-override": "off",
"prettier/prettier": "warn",
"array-callback-return": [
"error"
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"args": "none"
}
],
"no-warning-comments": [
"warn",
{
"terms": [
"@todo"
],
"location": "anywhere"
}
],
"no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"deprecation/deprecation": "warn",
"no-console": "off",
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "classProperty",
"modifiers": [
"private"
],
"format": [],
"custom": {
"regex": "^(_{1,2}I[A-Z][a-zA-Z0-9]*_?|_{1,2}[a-z][a-zA-Z0-9]*)$",
"match": true
}
},
{
"selector": "classProperty",
"modifiers": [
"protected"
],
"format": [],
"custom": {
"regex": "^(_{1,2}I[A-Z][a-zA-Z0-9]*_?|_{1,2}[a-z][a-zA-Z0-9]*)$",
"match": true
}
},
{
"selector": [
"typeProperty",
"classProperty"
],
"types": [
"boolean"
],
"format": [
"PascalCase"
],
"prefix": [
"is",
"has",
"can",
"did",
"will",
"should"
],
"leadingUnderscore": "allow"
},
{
"selector": "memberLike",
"modifiers": [
"public"
],
"format": [
"camelCase"
],
"leadingUnderscore": "forbid",
"filter": {
"regex": "^(Events|Styles|Classes|Then)$",
"match": false
}
},
{
"selector": "typeProperty",
"modifiers": [
"public"
],
"format": null,
"filter": {
"regex": ".*-event$",
"match": true
},
"custom": {
"regex": "^[a-z]+(-[a-z]+)*-event$",
"match": true
}
},
{
"selector": "typeProperty",
"modifiers": [
"public"
],
"format": [
"camelCase"
],
"filter": {
"regex": ".*-event$|^(Events|Styles|Classes|Then)$",
"match": false
}
}
],
"@typescript-eslint/no-unused-expressions": "off",
"@stylistic/padding-line-between-statements": [
"warn",
{
"blankLine": "always",
"prev": "*",
"next": [
"return",
"if",
"multiline-const",
"function",
"multiline-expression",
"multiline-let",
"block-like"
]
},
{
"blankLine": "always",
"prev": [
"function"
],
"next": "*"
}
],
"import/order": [
"warn",
{
"groups": [
[
"builtin",
"external"
],
[
"internal"
],
[
"parent",
"sibling"
]
],
"newlines-between": "never",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"jsdoc/no-undefined-types": [
"warn",
{
"disableReporting": true,
"markVariablesAsUsed": true
}
],
"jsdoc/require-jsdoc": [
"warn",
{
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true,
"ArrowFunctionExpression": false,
"FunctionExpression": false
},
"minLineCount": 10
}
],
"jsdoc/require-param": [
"warn",
{
"exemptedBy": [
"deprecated",
"inheritdoc"
]
}
],
"jsdoc/require-description": [
"warn",
{
"contexts": [
"FunctionDeclaration",
"MethodDefinition",
"ClassDeclaration",
"ClassExpression"
],
"descriptionStyle": "body",
"exemptedBy": [
"deprecated",
"inheritdoc"
]
}
],
"jsdoc/require-returns": [
"warn",
{
"checkGetters": false,
"exemptedBy": [
"deprecated",
"inheritdoc"
]
}
],
"jsdoc/check-tag-names": [
"warn",
{
"definedTags": [
"remarks",
"jest-environment",
"singleton"
]
}
],
"jsdoc/check-alignment": "warn",
"jsdoc/check-indentation": "warn",
"jsdoc/no-restricted-syntax": [
"error",
{
"contexts": [
{
"context": "MethodDefinition[kind='get']",
"comment": "JsdocBlock:has(JsdocTag[tag='returns'])",
"message": "JSDoc @returns comments are not allowed in getters."
}
]
}
],
"@typescript-eslint/prefer-readonly": "warn",
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true,
"allowTypedFunctionExpressions": true,
"allowHigherOrderFunctions": true
}
],
"@typescript-eslint/prefer-string-starts-ends-with": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/prefer-optional-chain": "warn"
},
"overrides": [
{
"files": [
"*.test.ts",
"*.spec.ts"
],
"rules": {
"jsdoc/require-jsdoc": "off",
"jsdoc/require-param": "off",
"jsdoc/require-description": "off",
"jsdoc/require-returns": "off",
"@typescript-eslint/explicit-function-return-type": "off"
}
}
]
}

View File

@@ -1,12 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
open-pull-requests-limit: 10
schedule:
interval: "weekly"

View File

@@ -1,123 +0,0 @@
name: Create Release
on:
push:
branches:
- main
paths:
- 'package.json'
workflow_dispatch: # Allows manual execution of the workflow.
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20.8.0'
- name: Install Dependencies
run: npm install
- name: Run Tests
run: npm run test:verbose
- name: Build the Project
run: npm run build:tsc
- name: Get the version
id: get_version
run: |
VERSION=$(npm run version:show | tail -n 1)
echo "VERSION=$VERSION" >> $GITHUB_ENV
shell: bash
- name: Get previous release tag
id: get_previous_release
run: |
echo "Fetching previous release tag..."
previous_tag=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$previous_tag" ]; then
echo "No previous tag found, using initial commit."
previous_tag=$(git rev-list --max-parents=0 HEAD)
fi
echo "Previous tag: $previous_tag"
echo "PREVIOUS_TAG=$previous_tag" >> $GITHUB_ENV
shell: bash
- name: Check if version changed
id: check_version
run: |
# Check if the version already exists as a tag
if git rev-parse "refs/tags/${{ env.VERSION }}" >/dev/null 2>&1; then
echo "skip_release=true" >> $GITHUB_OUTPUT
echo "Version ${{ env.VERSION }} already exists as a tag. No release will be created."
exit 0
fi
# Compare current version with previous tag
if [ "${{ env.VERSION }}" == "${{ env.PREVIOUS_TAG }}" ]; then
echo "skip_release=true" >> $GITHUB_OUTPUT
echo "Version has not changed. No release will be created."
exit 0
fi
echo "skip_release=false" >> $GITHUB_OUTPUT
shell: bash
- name: Generate release notes
id: generate_notes
if: steps.check_version.outputs.skip_release == 'false'
run: |
echo "Generating release notes from ${{ env.PREVIOUS_TAG }} to HEAD..."
repo_url=$(git config --get remote.origin.url)
notes=$(git log ${{ env.PREVIOUS_TAG }}..HEAD --pretty=format:"- [\`%h\`]($repo_url/commit/%H): %s%n")
echo "Release notes:"
echo "$notes"
echo "### Changes in this release" > release_notes.md
echo "$notes" >> release_notes.md
shell: bash
- name: Set Git user
if: steps.check_version.outputs.skip_release == 'false'
run: |
git config --local user.name "GitHub Actions"
git config --local user.email "actions@github.com"
shell: bash
# - name: Create temporary branch
# id: create_temp_branch
# if: steps.check_version.outputs.skip_release == 'false'
# run: |
# git checkout --orphan release/v${{ env.VERSION }}
# git reset
# rm -f .gitignore
# git add README.md package.json LICENSE dist/ src/ tsconfig.json
# git commit -m "Prepare files for release ${{ env.VERSION }}"
# shell: bash
- name: Create and push tag
id: create_tag
if: steps.check_version.outputs.skip_release == 'false'
run: |
git tag ${{ env.VERSION }}
git push origin ${{ env.VERSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
- name: Release
if: steps.check_version.outputs.skip_release == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.VERSION }}
name: Release ${{ env.VERSION }}
body_path: release_notes.md
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,44 +0,0 @@
name: Deploy Documentation
on:
push:
branches:
- main
workflow_dispatch: # Allows manual execution of the workflow.
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20.8.0'
- name: Install Dependencies
run: npm install
- name: Run TypeDoc Generation (TypeDoc, Test Coverage, fixes and badges)
run: npm run docs:generate
- name: Deploy to GitHub Pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git clone --single-branch --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} gh-pages
rm -rf gh-pages/*
cp -r .locale/docs/* gh-pages/
mkdir -p gh-pages/coverage
cp -r .locale/coverage/* gh-pages/coverage/
cd gh-pages
git add .
git commit -m 'Deploy documentation and coverage'
git push origin gh-pages

View File

@@ -1,31 +0,0 @@
name: Run Build and Tests on Pull Request
on:
pull_request:
branches:
- main
workflow_dispatch: # Allows manual execution of the workflow.
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20.8.0'
- name: Install Dependencies
run: npm install
- name: Run Tests
run: npm run test:verbose
- name: Build the Project
run: npm run build:tsc

View File

@@ -1,23 +0,0 @@
name: Validate Branch Name on Pull Request
on:
pull_request:
branches:
- main
jobs:
validate-branch-name-on-pull-request:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v2
- name: Validate Branch Name on Pull Request
run: |
BRANCH_NAME=${GITHUB_HEAD_REF}
if [[ ! "$BRANCH_NAME" =~ ^(feature\/|fix\/|refactoring\/|testing\/|dependabot\/|gh-pages) ]]; then
echo "Invalid branch name: $BRANCH_NAME"
echo "Branch name must start with 'feature/', 'fix/', 'refactoring/', 'testing/', dependabot/" or "gh-pages"
exit 1
fi

10
.gitignore vendored
View File

@@ -1,9 +1 @@
# Internal
.vscode
.locale
.VSCodeCounter
*.ignore.*
dist/*
node_modules/*
coverage/

View File

@@ -1,20 +0,0 @@
{
"printWidth": 80,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"useTabs": false,
"tabWidth": 4,
"endOfLine": "auto",
"overrides": [
{
"files": [
".prettierrc",
".eslintrc"
],
"options": {
"parser": "json"
}
}
]
}

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Deno Tests",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "/home/maxp/.deno/bin/deno",
"runtimeArgs": [
"test",
"--inspect-brk",
"--allow-all",
"tests/*.ts"
],
"attachSimplePort": 9229,
"console": "integratedTerminal"
}
]
}

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"editor.tabSize": 4,
"editor.insertSpaces": true,
"deno.enable": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.detectIndentation": false,
"editor.indentSize": "tabSize"
}

156
CHANGELOG.md Normal file
View File

@@ -0,0 +1,156 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
### Deprecated
### Removed
### Fixed
### Security
## [1.2.0]
### Added
- docs: added complete Typedoc-style documentation for all `inject()` overloads
Includes parameter descriptions, usage examples, and detailed error annotations for each variant.
## [1.1.0]
### Added
- feat: exported `inject()` function from the public API via `index.ts`
The function is now available as part of the main module exports.
## [1.0.0]
### Added
- feat: Enable native ESM support using `"type": "module"` and `moduleResolution: "NodeNext"` in the compiler settings.
- feat: All internal imports now explicitly include `.js` extensions for full Node.js ESM compatibility.
- feat: Updated `tsconfig.json` to reflect changes for ESM builds (`module: "NodeNext"`, `target: "ES2020"`, etc.).
- feat(cli): add `--without-extension` (`-x`) flag to optionally omit file extensions in generated import paths
- feat: introduce `inject()` function as a programmatic alternative to the `@Inject` decorator
Supports optional initializers and constructor instantiation for resolved dependencies.
Designed for cases where decorators are not suitable or dynamic resolution is needed.
- test: added comprehensive test suite for `inject()` function, covering resolution, initialization, error cases and instantiation behavior
### Changed
- All source files using relative or internal imports were updated to use `.js` extensions to support Node.js ESM runtime resolution.
- test: update Jest config for ts-jest ESM compatibility and .js import support
- renamed internal parameter `necessary``isNecessary` for naming clarity
### Removed
- Removed implicit support for CommonJS-style imports without file extensions.
### Deprecated
- Support for CommonJS consumers using `require()` is no longer available. Use `import` with an ESM-compatible environment instead.
### Fixed
### Security
### ⚠️ Breaking Changes
- **BREAKING CHANGE**: This version migrates the entire codebase to native ES modules.
- Consumers must use Node.js in ESM mode or compatible bundlers.
- Import paths now include `.js` extensions.
- Using `require()` (CommonJS) to load this library will no longer work.
- All consuming projects must either:
- Use `"type": "module"` in their `package.json`, or
- Use an ESM-aware bundler (e.g. Webpack, Vite, etc.)
## [0.4.0]
### Added
- feat: Export ImplementsStatic helper function
### Deprecated
### Removed
### Fixed
### 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]
### Added
- Added **ChangeLog** file and format it according to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Added reference to **Semantic Versioning** in the changelog file. (History will be updated on time).
- Version format is now `v0.0.0` instead of `0.0.0`. Changes to this are also reflected in the workflos.
- Add `Identifiers` and `Jest` Sections to the `README.md` file.
- feat: Add new Error `InitializationError` to reflect errors during initialization of a dependency.
- feat: Add initialization error handling and refactor Inject.
- feat: After injecting a dependency, the lazzy loading getter will be replaced with the dependency itself.
- feat: remove the use of a private property to store the injected dependencies. Now the dependencies are stored in the property itself.
- test: Add tests for the new features.
### Deprecated
- Deprecated the old version format `0.0.0`.
### Removed
### Fixed
### Security
---
[unreleased]: https://github.com/20Max01/TSinjex/compare/v1.0.0...HEAD
[1.1.0]: https://github.com/20Max01/TSinjex/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/20Max01/TSinjex/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/20Max01/TSinjex/compare/v0.4.0...v1.0.0
[0.4.0]: https://github.com/20Max01/TSinjex/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/20Max01/TSinjex/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/20Max01/TSinjex/compare/v0.0.14...v0.2.0
[0.0.14]: https://github.com/20Max01/TSinjex/compare/v0.0.13...v0.0.14

View File

@@ -1,28 +0,0 @@
![Time](https://waka.mpassarello.de/api/badge/MaxP/interval:any/project:TSinjex?label=Project%20time)
[![Statements](https://pxammaxp.github.io/TSinjex/coverage/badges/badge-statements.svg) ![Branches](https://pxammaxp.github.io/TSinjex/coverage/badges/badge-branches.svg) ![Functions](https://pxammaxp.github.io/TSinjex/coverage/badges/badge-functions.svg) ![Lines](https://pxammaxp.github.io/TSinjex/coverage/badges/badge-lines.svg) ](https://pxammaxp.github.io/TSinjex/coverage/lcov-report/index.html)
# TSinjex
## Configuration
### Jest
#### Example jest setup
```ts
module.exports = {
setupFilesAfterEnv: ['./scripts/jest.setup.js'],
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(test).ts'],
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/src/$1', // Map src to the source folder
'^ts-injex$': '<rootDir>/node_modules/ts-injex/src', // Map ts-injex to the source folder
},
transformIgnorePatterns: [
'node_modules/(?!ts-injex)' // **Dont** ignore ts-injex on preset `ts-jest`
],
};
```

35
deno.jsonc Normal file
View File

@@ -0,0 +1,35 @@
{
"version": "2.0.0",
"compilerOptions": {},
"lint": {
"files": {
"include": [
"src/"
],
}
},
"fmt": {
"files": {
"include": [
"src/",
"tests/"
],
},
"options": {
"lineWidth": 100,
"indentWidth": 4,
"useTabs": false
}
},
"test": {
"include": [
"tests/"
],
},
"tasks": {
"test": "deno test --coverage tests/*.ts",
"lint": "deno lint",
"fmt": "deno fmt",
"build": "echo 'Kein Build nötig bei Deno'"
}
}

39
deno.lock generated Normal file
View File

@@ -0,0 +1,39 @@
{
"version": "5",
"remote": {
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
"https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c"
}
}

View File

@@ -1,22 +0,0 @@
module.exports = {
setupFilesAfterEnv: ['./scripts/jest.setup.js'],
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(test).ts'],
testPathIgnorePatterns: ['\\.spec\\.ts$', '\\.performance\\.test\\.ts$'],
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/src/$1',
},
collectCoverage: true,
coverageDirectory: '.locale/coverage',
coverageReporters: ['text', 'lcov'],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};

View File

@@ -1,30 +0,0 @@
module.exports = {
setupFilesAfterEnv: ['./scripts/jest.setup.js'],
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(test).ts'],
testPathIgnorePatterns: ['\\.spec\\.ts$', '\\.performance\\.test\\.ts$'],
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/src/$1',
},
collectCoverage: true,
coverageDirectory: '.locale/coverage',
coverageReporters: ['text', ['lcov', { projectRoot: '..' }], 'json-summary'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.performance.test.ts',
'!src/**/*.spec.ts',
'!src/**/*.test.ts',
'!src/auto-imports.ts'
],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
};

7169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
{
"name": "ts-injex",
"version": "0.0.11",
"description": "Simple boilerplate code free dependency injection system for TypeScript.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"prepare": "npm run build",
"build": "npm run build:tsc",
"build:tsc": "tsc",
"lint": "eslint --ext .ts .",
"lint:fix": "eslint --fix --ext .ts .",
"test": "jest",
"test:watch": "jest --watch --onlyChanged",
"test:file": "jest --watch --onlyChanged --coverage=true --verbose",
"test:verbose": "jest --verbose",
"test:coverage": "jest --config jest.config.coverage.cjs --coverage",
"docs": "typedoc",
"docs:generate": "npm run docs && npm run docs:generate:coverage && npm run docs:fix:coverage && npm run docs:generate:badge && npm run docs:fix:escape",
"docs:generate:coverage": "npm run test:coverage || exit 0",
"docs:fix:coverage": "node scripts/fix-coverage-paths.cjs",
"docs:generate:badge": "node scripts/generate-badge.cjs",
"docs:fix:escape": "node scripts/replace-doc-escaping.cjs",
"version:show": "node -e \"console.log(require('./package.json').version)\""
},
"repository": {
"type": "git",
"url": "https://github.com/PxaMMaxP/TSinjex.git"
},
"keywords": [],
"author": "Max P. (@Github: PxaMMaxP)",
"license": "MIT",
"devDependencies": {
"typescript": "^5.5.4",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.11",
"@stylistic/eslint-plugin": "^2.6.2",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint-plugin-deprecation": "^3.0.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",
"ts-jest": "^29.2.3",
"typedoc": "^0.26.5",
"istanbul-badges-readme": "^1.9.0",
"axios": "^1.7.2"
},
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
"jest-environment-jsdom": "^29.7.0"
},
"files": [
"dist/**/*",
"src/**/*",
"README.md",
"LICENSE",
"package.json"
]
}

View File

@@ -1,46 +0,0 @@
const fs = require('fs');
const path = require('path');
const coverageDir = path.join(__dirname, '..', '.locale', 'coverage');
const typedocUrl = '../../';
const getAllFiles = (dir, files = []) => {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
getAllFiles(fullPath, files);
} else {
files.push(fullPath);
}
});
return files;
};
// Alle HTML-Dateien im coverage-Ordner finden
const htmlFiles = getAllFiles(coverageDir).filter(file => file.endsWith('.html'));
// Alle HTML-Dateien bearbeiten
htmlFiles.forEach(filePath => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(`Error reading file ${filePath}:`, err);
return;
}
// Relative Pfade anpassen
let fixedData = data.replace(/(src|href)="(?!\.)/g, '$1="./');
// Link zur TypeDoc-Dokumentation hinzufügen
const linkHtml = `<div style="position: fixed; bottom: 10px; right: 10px;"><a href="${typedocUrl}">Zur TypeDoc-Dokumentation</a></div>`;
fixedData = fixedData.replace('</body>', `${linkHtml}</body>`);
fs.writeFile(filePath, fixedData, 'utf8', (err) => {
if (err) {
console.error(`Error writing file ${filePath}:`, err);
return;
}
console.log(`Fixed paths and added link in ${filePath}`);
});
});
});

View File

@@ -1,55 +0,0 @@
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const axios = require('axios');
// Step 1: Create README.md in the coverage directory
const coverageReadmePath = path.join(__dirname, '..', '.locale', 'coverage', 'README.md');
const readmeContent = `
![Statements](#statements#)
![Branches](#branches#)
![Functions](#functions#)
![Lines](#lines#)
`;
fs.writeFileSync(coverageReadmePath, readmeContent, 'utf8');
// Step 2: Execute the istanbul-badges-readme tool
exec('npx istanbul-badges-readme --coverageDir=./.locale/coverage --readmeDir=./.locale/coverage', (err, stdout, stderr) => {
if (err) {
console.error(`Error executing istanbul-badges-readme: ${stderr}`);
return;
}
console.log('Badges generated successfully.');
// Step 3: Extract the badge links from README.md
const updatedReadmeContent = fs.readFileSync(coverageReadmePath, 'utf8');
const badgeLines = updatedReadmeContent.split('\n').filter(line => line.includes('https://img.shields.io'));
// Ensure the target directory exists
const badgesDir = path.join(__dirname, '..', '.locale', 'coverage', 'badges');
if (!fs.existsSync(badgesDir)) {
fs.mkdirSync(badgesDir, { recursive: true });
}
// Badge types and their order
const badgeTypes = ['statements', 'branches', 'functions', 'lines'];
// Save the badge images
badgeLines.forEach(async (line, index) => {
const match = line.match(/\((https:\/\/img\.shields\.io\/badge\/[^)]+)\)/);
if (match) {
const url = match[1];
const response = await axios.get(url, { responseType: 'arraybuffer' });
const buffer = Buffer.from(response.data, 'binary');
const fileName = `badge-${badgeTypes[index]}.svg`;
const filePath = path.join(badgesDir, fileName);
fs.writeFileSync(filePath, buffer);
console.log(`Saved ${fileName}`);
}
});
// Step 4: Delete the README.md file
fs.unlinkSync(coverageReadmePath);
console.log('README.md file deleted.');
});

View File

@@ -1,17 +0,0 @@
// jest.setup.js
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
global.customJSONStringify = (object) => {
return JSON.stringify(object, getCircularReplacer());
};

View File

@@ -1,42 +0,0 @@
const fs = require('fs');
const path = require('path');
const docsDir = path.join(__dirname, '..', '.locale', 'docs');
const getAllFiles = (dir, files = []) => {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
getAllFiles(fullPath, files);
} else {
files.push(fullPath);
}
});
return files;
};
// Alle HTML-Dateien im docs-Ordner finden
const htmlFiles = getAllFiles(docsDir).filter(file => file.endsWith('.html'));
// Alle HTML-Dateien bearbeiten
htmlFiles.forEach(filePath => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(`Error reading file ${filePath}:`, err);
return;
}
// `\@` durch `@` ersetzen
let fixedData = data.replace(/\\@/g, '@');
fs.writeFile(filePath, fixedData, 'utf8', (err) => {
if (err) {
console.error(`Error writing file ${filePath}:`, err);
return;
}
console.log(`Fixed escaping in ${filePath}`);
});
});
});

View File

@@ -1,262 +0,0 @@
/* 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 { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex';
/**
* Test the Inject decorator.
* @param Container The implementation to test.
* @param inject The Inject decorator to test.
*/
export function test_InjectDecorator(
Container: ITSinjex_,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
inject: Function,
): void {
describe('Inject Decorator Tests', () => {
let container: ITSinjex;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_instance'] = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_dependencies'] = undefined;
container = Container.getInstance();
});
it('should inject dependency when necessary is true', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier')
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value');
});
it('should inject dependency and run initializer', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier', (x: string) => {
(x as unknown as { value: string }).value =
'test-value-init';
return x;
})
dependency!: any;
public getDependency() {
return this.dependency;
}
}
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;
container.register('InitThrowDependencie', {
value: 'test-value',
});
try {
class TestClass {
@Inject(
'InitThrowDependencie',
() => {
throw new Error('Initializer error');
},
true,
)
dependency!: any;
public getDependency() {
return this.dependency;
}
}
const _instance = new TestClass();
console.log(_instance.getDependency());
} catch (error) {
_error = error;
}
expect(_error).toBeInstanceOf(Error);
});
it('should throw an error when necessary is true and dependency is not found', () => {
let _error: Error | undefined = undefined;
try {
class TestClass {
@Inject('NonExistentDependencyIdentifier')
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
const _instance = new TestClass();
console.log(_instance.getDependency());
} catch (error) {
_error = error;
}
expect(_error).toBeInstanceOf(DependencyResolutionError);
});
});
}
export function test_RegisterDecorator(
Container: ITSinjex_,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
register: Function,
): void {
describe('Register Decorator Tests', () => {
let container: ITSinjex;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_instance'] = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_dependencies'] = undefined;
container = Container.getInstance();
});
it('should register a dependency', () => {
@register('MockDependencyIdentifier')
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(container.resolve('MockDependencyIdentifier')).toBe(
TestClass,
);
});
});
}
export function test_RegisterInstanceDecorator(
Container: ITSinjex_,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
registerInstance: Function,
): void {
describe('RegisterInstance Decorator Tests', () => {
let container: ITSinjex;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_instance'] = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_dependencies'] = undefined;
container = Container.getInstance();
});
it('should register an instance of a dependency', () => {
@registerInstance('InstanceIdentifier')
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = 'instance';
}
expect(
container.resolve<TestClass>('InstanceIdentifier').mark,
).toBe('instance');
});
it('should register an instance of a dependency an run the init function', () => {
@registerInstance(
'InstanceIdentifier',
(x: ForceConstructor<TestClass>) => {
const instance = new x();
instance.mark = 'init';
return instance;
},
)
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = 'instance';
}
expect(
container.resolve<TestClass>('InstanceIdentifier').mark,
).toBe('init');
});
it('should register an instance of a dependency and get it on set', () => {
@registerInstance('InstanceIdentifier')
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = 'instance';
public test: string = 'test';
}
container.resolve<TestClass>('InstanceIdentifier').test = 'test2';
expect(
container.resolve<TestClass>('InstanceIdentifier').test,
).toBe('test2');
});
it('should register an instance of a dependency an run the init function on set', () => {
@registerInstance(
'InstanceIdentifier',
(x: ForceConstructor<TestClass>) => {
const instance = new x();
instance.mark = 'init';
return instance;
},
)
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = 'instance';
public test: string = 'test';
}
container.resolve<TestClass>('InstanceIdentifier').test = 'test2';
expect(
container.resolve<TestClass>('InstanceIdentifier').test,
).toBe('test2');
});
});
}

View File

@@ -1,15 +0,0 @@
import { TSinjex } from 'src/classes/TSinjex';
import { Inject } from 'src/decorators/Inject';
import { Register } from 'src/decorators/Register';
import { RegisterInstance } from 'src/decorators/RegisterInstance';
import {
test_InjectDecorator,
test_RegisterDecorator,
test_RegisterInstanceDecorator,
} from './Decorators.spec';
test_InjectDecorator(TSinjex, Inject);
test_RegisterDecorator(TSinjex, Register);
test_RegisterInstanceDecorator(TSinjex, RegisterInstance);

View File

@@ -1,70 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ITSinjex, ITSinjex_ } from 'src/interfaces/ITSinjex';
export function test_RegisterFunction(
Container: ITSinjex_,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
register: Function,
): void {
describe('Register Function Tests', () => {
let container: ITSinjex;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_instance'] = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_dependencies'] = undefined;
container = Container.getInstance();
});
it('should register a dependency', () => {
const identifier = 'MockDependencyIdentifier';
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
register(identifier, TestClass, false);
const resolvedDependency = container.resolve(identifier);
expect(resolvedDependency).toBe(TestClass);
});
});
}
export function test_ResolveFunction(
Container: ITSinjex_,
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
resolve: Function,
): void {
describe('Resolve Function Tests', () => {
let container: ITSinjex;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_instance'] = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_dependencies'] = undefined;
container = Container.getInstance();
});
it('should resolve a dependency', () => {
const identifier = 'MockDependencyIdentifier';
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
container.register(identifier, TestClass);
const resolvedDependency = resolve(identifier);
expect(resolvedDependency).toBe(TestClass);
});
});
}

View File

@@ -1,7 +0,0 @@
import { TSinjex } from 'src/classes/TSinjex';
import { register } from 'src/functions/register';
import { resolve } from 'src/functions/resolve';
import { test_RegisterFunction, test_ResolveFunction } from './Functions.spec';
test_RegisterFunction(TSinjex, register);
test_ResolveFunction(TSinjex, resolve);

View File

@@ -1,78 +0,0 @@
import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex';
/**
* Test the implementation of the `ITSinjex` interface.
* @param Container The implementation to test.
* Must implement {@link ITSinjex}, {@link ITSinjex_}
*/
export function test_ITSinjex(Container: ITSinjex_): void {
describe('IDIContainer Implementation Tests', () => {
let container: ITSinjex;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_instance'] = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Container as any)['_dependencies'] = undefined;
container = Container.getInstance();
});
it('should register and resolve a dependency', () => {
const identifier = 'myDependency';
const dependency = { value: 42 };
container.register(identifier, dependency);
const resolvedDependency =
container.resolve<typeof dependency>(identifier);
expect(resolvedDependency).toBe(dependency);
});
it('should register and resolve a dependency static', () => {
const identifier = 'myDependency';
const dependency = { value: 42 };
Container.register(identifier, dependency);
const resolvedDependency =
Container.resolve<typeof dependency>(identifier);
expect(resolvedDependency).toBe(dependency);
});
it('should throw an error when resolving a non-registered dependency static', () => {
const identifier = 'nonExistentDependency';
expect(() => Container.resolve<unknown>(identifier)).toThrow();
});
it('should return undefined when resolving a non-registered, non-necessary dependency', () => {
const resolvedDependency = Container.resolve<unknown>(
'nonExistentDependency',
false,
);
expect(resolvedDependency).toBe(undefined);
});
it('should warn when resolving a deprecated dependency', () => {
const identifier = 'deprecatedDependency';
const dependency = { value: 42 };
// Spy on console.warn
const warnSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
Container.register(identifier, dependency, true);
const resolvedDependency =
Container.resolve<typeof dependency>(identifier);
expect(resolvedDependency).toBe(dependency);
// Expect console.warn to be called
expect(warnSpy).toHaveBeenCalled();
// Restore the original console.warn
warnSpy.mockRestore();
});
});
}

View File

@@ -1,4 +0,0 @@
import { test_ITSinjex } from './ITSinjex.spec';
import { TSinjex } from '../classes/TSinjex';
test_ITSinjex(TSinjex);

View File

@@ -1,14 +1,3 @@
import { Identifier } from 'src/types/Identifier';
import type { Inject } from '../decorators/Inject';
import type { Register } from '../decorators/Register';
import type { RegisterInstance } from '../decorators/RegisterInstance';
import type { register } from '../functions/register';
import type { resolve } from '../functions/resolve';
import { ImplementsStatic } from '../helper/ImplementsStatic';
import { DependencyResolutionError } from '../interfaces/Exceptions';
import { IDependency } from '../interfaces/IDependency';
import { ITSinjex, ITSinjex_ } from '../interfaces/ITSinjex';
/**
* # TSinjex
* The main class for the Dependency Injection Container **TSinjex**.
@@ -21,6 +10,11 @@ import { ITSinjex, ITSinjex_ } from '../interfaces/ITSinjex';
* @see {@link register} for registering a dependency (class or instance) as a function.
* @see {@link resolve} for resolving a dependency as a function.
*/
import { ImplementsStatic } from "../helper/mod.ts";
import { DependencyResolutionError, IDependency, ITSinjex, ITSinjex_ } from "../interfaces/mod.ts";
import { Identifier } from "../types/mod.ts";
@ImplementsStatic<ITSinjex_>()
export class TSinjex implements ITSinjex {
/**
@@ -114,7 +108,7 @@ export class TSinjex implements ITSinjex {
}
if (dependency.deprecated) {
console.warn(`Dependency ${identifier} is deprecated`);
console.warn(`Dependency ${identifier.toString()} is deprecated`);
// Remove the deprecation warning; it should only be logged once.
dependency.deprecated = false;
@@ -123,5 +117,9 @@ export class TSinjex implements ITSinjex {
return dependency.dependency as T;
}
public clear(): void {
this._dependencies.clear();
}
//#endregion
}

1
src/classes/mod.ts Normal file
View File

@@ -0,0 +1 @@
export { TSinjex } from "./TSinjex.ts";

View File

@@ -1,77 +1,54 @@
import { Identifier } from 'src/types/Identifier';
import { TSinjex } from '../classes/TSinjex';
import { InitDelegate } from '../types/InitDelegate';
import { TSinjex } from "../classes/mod.ts";
import { InitializationError } from "../interfaces/mod.ts";
import { Identifier, InitDelegate } from "../types/mod.ts";
/**
* 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 An optional initializer function to transform the dependency before injection.
* @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 A {@link DependencyResolutionError} if the dependency is not found and necessary.
* @example
* ```ts
* class MyClass {
* \@Inject<MyDependency>('MyDependencyIdentifier')
* private myDependency!: MyDependency;
* }
* ```
* @example
* ```ts
* class MyClass {
* \@Inject('ILogger_', (x: ILogger_) => x.getLogger('Tags'), false)
* private _logger?: ILogger;
* }
* ```
*/
export function Inject<T, U>(
export function Inject<InstanzType, DependencyType, FieldType extends object>(
identifier: Identifier,
init?: InitDelegate<T, U>,
necessary = true,
) {
return function (target: unknown, propertyKey: string | symbol): void {
// Unique symbol to store the private property
const privatePropertyKey: unique symbol = Symbol();
// Get the DI container instance
const diContainer = TSinjex.getInstance();
init?: InitDelegate<DependencyType, FieldType>,
isNecessary = true,
): (
target: undefined,
context: ClassFieldDecoratorContext<InstanzType, FieldType>,
) => (initialValue: FieldType) => FieldType {
return function (
_target: undefined,
context: ClassFieldDecoratorContext<InstanzType, FieldType>,
): (initialValue: FieldType) => FieldType {
if (context.kind !== "field") {
throw new Error("Inject decorator can only be used on fields.");
}
// Function to evaluate the dependency lazily
// to avoid circular dependencies, not found dependencies, etc.
const evaluate = (): T | undefined => {
return diContainer.resolve<T>(identifier, necessary);
const initializer = () => {
let instance: DependencyType | FieldType | undefined;
const dependency: DependencyType | undefined = TSinjex.getInstance()
.resolve<DependencyType>(identifier, isNecessary);
if (init == null || dependency == null) {
instance = dependency;
} else {
try {
instance = init(dependency);
} catch (error) {
if (isNecessary) {
throw new InitializationError(
identifier,
error instanceof Error ? error : new Error(String(error)),
);
} else {
console.warn(
`Error initializing not necessary dependency ${identifier.toString()}: ${error}`,
);
instance = undefined;
}
}
}
return instance as FieldType;
};
// Define the property
Object.defineProperty(target, propertyKey, {
get() {
// If the property is not defined, evaluate the dependency
if (!this.hasOwnProperty(privatePropertyKey)) {
if (init) {
try {
this[privatePropertyKey] = init(evaluate() as T);
} catch (error) {
if (necessary) {
throw error;
}
}
} else {
this[privatePropertyKey] = evaluate();
}
}
return this[privatePropertyKey];
},
// Not necessary to set the property
// set(value: PropertieType) {
// this[privatePropertyKey] = value;
// },
enumerable: true,
configurable: false,
});
return function (_initialValue: FieldType): FieldType {
return initializer();
};
};
}

View File

@@ -1,30 +1,88 @@
import { Identifier } from 'src/types/Identifier';
import { TSinjex } from '../classes/TSinjex';
import { TSinjex } from "../classes/mod.ts";
import { ClassConstructor, Identifier, InitDelegate } from "../types/mod.ts";
export function Register<ClassType extends ClassConstructor>(
identifier: Identifier,
init?: InitDelegate<ClassType, InstanceType<ClassType>>,
deprecated?: boolean,
): (target: ClassType, context: ClassDecoratorContext<ClassType>) => void {
return function (
target: ClassType,
context: ClassDecoratorContext<ClassType>,
): void {
if (context.kind !== "class") {
throw new Error("Register decorator can only be used on classes.");
}
/**
* 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.
* @example
* ```ts
* \@Register('MyClassIdentifier')
* class MyClass {
* // ...
* }
* ```
*/
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();
let _instance: InstanceType<ClassType> | undefined;
// Register the class in the DI container
diContainer.register(identifier, constructor, deprecated);
if (init == undefined) {
diContainer.register(identifier, target, deprecated);
} else {
diContainer.register(
identifier,
createLazyProxy(
_instance,
init,
target,
),
deprecated,
);
}
};
}
function createLazyProxy<ClassType extends ClassConstructor>(
instance: InstanceType<ClassType> | undefined,
init: InitDelegate<ClassType, InstanceType<ClassType>>,
constructor: ClassType,
): InstanceType<ClassType> {
const initializeInstance = (
instance: InstanceType<ClassType> | undefined,
init: InitDelegate<ClassType, InstanceType<ClassType>>,
constructor: ClassType,
) => {
if (instance == undefined) {
if (init != undefined) {
instance = init(constructor);
} else {
instance = new constructor() as InstanceType<ClassType>;
}
}
return { instance: instance, lazyProxy: instance };
};
let lazyProxy = new Proxy(
{} as InstanceType<ClassType>,
{
get(_target, prop, _receiver) {
({ instance, lazyProxy } = initializeInstance(
instance,
init,
constructor,
));
if (!instance) throw new Error("Instance is not defined");
// Return the requested property of the instance
return instance[prop as keyof InstanceType<ClassType>];
},
set(_target, prop, value, _receiver) {
({ instance, lazyProxy } = initializeInstance(
instance,
init,
constructor,
));
if (!instance) throw new Error("Instance is not defined");
// Set the requested property of the instance
return (instance[prop as keyof InstanceType<ClassType>] = value);
},
},
);
return lazyProxy;
}

View File

@@ -1,73 +0,0 @@
import { Identifier } from 'src/types/Identifier';
import { TSinjex } from '../classes/TSinjex';
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
* 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.
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
* class MyClass {
* // ...
* }
* ```
*/
export function RegisterInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
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
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) {
if (instance == null) {
if (init) {
instance = init(constructor);
} else {
instance = new constructor(...args);
}
}
lazyProxy = instance;
// 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 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);
};
}

2
src/decorators/mod.ts Normal file
View File

@@ -0,0 +1,2 @@
export { Register } from "./Register.ts";
export { Inject } from "./Inject.ts";

0
src/functions/mod.ts Normal file
View File

View File

@@ -1,39 +0,0 @@
import { Identifier } from 'src/types/Identifier';
import { TSinjex } from '../classes/TSinjex';
/**
* Register a dependency.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers..
* @param dependency The dependency to register.
*/
export function register<T>(identifier: Identifier, dependency: T): void;
/**
* Register a dependency.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param dependency The dependency to register.
* @param deprecated A warning is logged when the dependency is resolved.
*/
export function register<T>(
identifier: Identifier,
dependency: T,
deprecated?: true,
): void;
/**
* Register a dependency.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param dependency The dependency to register.
* @param deprecated If true, the dependency is deprecated => a warning
* is logged when the dependency is resolved.
*/
export function register<T>(
identifier: Identifier,
dependency: T,
deprecated?: boolean,
): void {
TSinjex.getInstance().register(identifier, dependency, deprecated);
}

View File

@@ -1,40 +0,0 @@
import { Identifier } from 'src/types/Identifier';
import { TSinjex } from '../classes/TSinjex';
import { DependencyResolutionError } from '../interfaces/Exceptions';
/**
* Resolve a dependency.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @returns The resolved dependency.
* @throws A {@link DependencyResolutionError} if the dependency is not found.
*/
export function resolve<T>(identifier: Identifier): T;
/**
* Resolve a dependency
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param necessary The dependency is **not** necessary.
* @returns The resolved dependency or undefined if the dependency is not found.
*/
export function resolve<T>(
identifier: Identifier,
necessary: false,
): T | undefined;
/**
* Resolve a dependency.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @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 A {@link DependencyResolutionError} if the dependency is not found and necessary.
*/
export function resolve<T>(
identifier: Identifier,
necessary?: boolean,
): T | undefined {
return TSinjex.getInstance().resolve<T>(identifier, necessary);
}

View File

@@ -6,5 +6,5 @@
* @returns A decorator function
*/
export function ImplementsStatic<I>() {
return <T extends I>(constructor: T, ...args: unknown[]) => {};
return <T extends I>(_constructor: T, ..._args: unknown[]) => {};
}

1
src/helper/mod.ts Normal file
View File

@@ -0,0 +1 @@
export { ImplementsStatic } from "./ImplementsStatic.ts";

View File

@@ -1,18 +0,0 @@
/* istanbul ignore file */
// Main
export * from './classes/TSinjex';
// Decorators
export * from './decorators/Inject';
export * from './decorators/Register';
export * from './decorators/RegisterInstance';
// Functions
export * from './functions/resolve';
export * from './functions/register';
// Interfaces & Types
export type * from './interfaces/ITSinjex';
export type * from './types/InitDelegate';
export type * from './types/GenericContructor';

View File

@@ -1,4 +1,4 @@
import { ITSinjex } from './ITSinjex';
import { Identifier } from "../types/mod.ts";
/**
* General error class for {@link ITSinjex} interface.
@@ -10,7 +10,7 @@ export class TSinjexError extends Error {
*/
constructor(message: string) {
super(message);
this.name = 'TSinjex';
this.name = "TSinjex";
}
}
@@ -23,8 +23,61 @@ export class DependencyResolutionError extends TSinjexError {
* Creates a new instance of {@link DependencyResolutionError}
* @param identifier **The identifier of the dependency**
*/
constructor(identifier: string) {
super(`Dependency ${identifier} could not be resolved.`);
this.name = 'TSinjexResolutionError';
constructor(identifier: Identifier) {
super(`Dependency ${identifier.toString()} could not be resolved.`);
this.name = "TSinjexResolutionError";
}
}
/**
* Error class for Injector errors in {@link ITSinjex}.
* @see {@link ITSinjex.inject}
*/
export class InjectorError extends TSinjexError {
/**
* Creates a new instance of {@link InjectorError}
* @param identifier **The identifier of the dependency**
* @param originalError **The original error that caused the injection error**
*/
constructor(identifier: Identifier, originalError?: Error) {
super(
`Error injecting dependency ${identifier.toString()} with error: "${originalError}"`,
);
this.name = "TSinjexInjectorError";
}
}
/**
* Error class for missing instantiation methods in {@link ITSinjex}.
* @see {@link ITSinjex.inject}
*/
export class NoInstantiationMethodError extends TSinjexError {
/**
* Creates a new instance of {@link NoInstantiationMethodError}
* @param identifier **The identifier of the dependency**
*/
constructor(identifier: Identifier) {
super(
`No instantiation method found for dependency ${identifier.toString()}.`,
);
this.name = "TSinjexNoInstantiationMethodError";
}
}
/**
* Error class for errors during the initialization of a dependency in {@link ITSinjex}.
* @see {@link ITSinjex.inject}
*/
export class InitializationError extends TSinjexError {
/**
* Creates a new instance of {@link InitializationError}
* @param identifier **The identifier of the dependency**
* @param originalError **The original error that caused the initialization error**
*/
constructor(identifier: Identifier, originalError?: Error) {
super(
`Error initializing dependency ${identifier.toString()} with error: "${originalError}"`,
);
this.name = "TSinjexInitializationError";
}
}

View File

@@ -1,5 +1,4 @@
import { Identifier } from 'src/types/Identifier';
import { DependencyResolutionError } from './Exceptions';
import { Identifier } from "../types/mod.ts";
/**
* Static TSInjex Interface

9
src/interfaces/mod.ts Normal file
View File

@@ -0,0 +1,9 @@
export type { ITSinjex, ITSinjex_ } from "./ITSinjex.ts";
export type { IDependency } from "./IDependency.ts";
export {
DependencyResolutionError,
InitializationError,
InjectorError,
NoInstantiationMethodError,
TSinjexError,
} from "./Exceptions.ts";

5
src/mod.ts Normal file
View File

@@ -0,0 +1,5 @@
export { TSinjex } from "./classes/mod.ts";
export type { Identifier, InitDelegate } from "./types/mod.ts";
export type { ITSinjex, ITSinjex_ } from "./interfaces/mod.ts";
export { Inject, Register } from "./decorators/mod.ts";
export type { ImplementsStatic } from "./helper/mod.ts";

View File

@@ -11,3 +11,8 @@ export type GenericConstructor<
* This type is used to force a class to has a constructor.
*/
export type ForceConstructor<T> = new (...args: unknown[]) => T;
/**
* Represents any concrete (non-abstract) class constructor.
*/
export type ClassConstructor = new (...args: unknown[]) => object;

View File

@@ -8,4 +8,4 @@
* I.e. a class `ClassA` that implements the interface `IClassA` and is
* registered as a dependent class is registered under the interface name `IClassA`.
*/
export type Identifier = string;
export type Identifier = string | symbol;

5
src/types/mod.ts Normal file
View File

@@ -0,0 +1,5 @@
// Export all type aliases for external use
export type { Identifier } from "./Identifier.ts";
export type { InitDelegate } from "./InitDelegate.ts";
export type { ClassConstructor, ForceConstructor, GenericConstructor } from "./Constructor.ts";

229
tests/Decorators.ts Normal file
View File

@@ -0,0 +1,229 @@
// deno-coverage-ignore-file
// deno-lint-ignore-file no-explicit-any
import {
assertEquals,
assertInstanceOf,
assertStrictEquals,
assertThrows,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { TSinjex } from "../src/classes/mod.ts";
import { Inject, Register } from "../src/decorators/mod.ts";
import { DependencyResolutionError } from "../src/interfaces/mod.ts";
const container = TSinjex.getInstance() as TSinjex;
Deno.test("should inject dependency when necessary is true", () => {
container.clear();
container.register("MockDependencyIdentifier", { value: "test-value" });
class TestClass {
@Inject("MockDependencyIdentifier")
private _dependency!: any;
public getDependency() {
return this._dependency;
}
}
const instance = new TestClass();
assertEquals(instance.getDependency().value, "test-value");
});
Deno.test("should inject dependency and run initializer", () => {
container.clear();
container.register("MockDependencyIdentifier", { value: "test-value" });
class TestClass {
@Inject("MockDependencyIdentifier", (x: any) => {
x.value = "test-value-init";
return x;
})
dependency!: any;
public getDependency() {
return this.dependency;
}
}
const instance = new TestClass();
assertEquals(instance.getDependency().value, "test-value-init");
});
Deno.test("should throw error if initializer fails and dependency is necessary", () => {
container.clear();
container.register("InitThrowDependencie", { value: "test-value" });
class TestClass {
@Inject<TestClass, any, any>("InitThrowDependencie", () => {
throw new Error("Initializer error");
}, true)
dependency!: any;
public getDependency() {
return this.dependency;
}
}
assertThrows(
() => {
const instance = new TestClass();
instance.getDependency()();
},
Error,
"Initializer error",
);
});
Deno.test("should throw DependencyResolutionError if dependency not found", () => {
container.clear();
class TestClass {
@Inject("NonExistentDependencyIdentifier")
private _dependency!: any;
public getDependency() {
return this._dependency;
}
}
assertThrows(() => {
const instance = new TestClass();
instance.getDependency()();
}, DependencyResolutionError);
});
Deno.test("should replace the property with the resolved dependency", () => {
container.clear();
container.register("MockDependencyIdentifier", { value: "test-value" });
class TestClass {
@Inject("MockDependencyIdentifier")
private _dependency!: any;
public getDependency() {
return this._dependency;
}
public isDependencyTypeofFunction() {
return typeof this._dependency === "function";
}
}
const instance = new TestClass();
assertEquals(instance.getDependency().value, "test-value");
assertEquals(instance.isDependencyTypeofFunction(), false);
assertEquals(instance.getDependency().value, "test-value");
});
Deno.test("Register Decorator: should register a dependency", () => {
container.clear();
@Register("MockDependencyIdentifier")
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
assertStrictEquals(
container.resolve("MockDependencyIdentifier"),
TestClass,
);
});
Deno.test("RegisterInstance: should register an instance of a dependency", () => {
container.clear();
@Register("InstanceIdentifier", (x) => new x())
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = "instance";
}
const resolved = container.resolve<TestClass>("InstanceIdentifier");
assertEquals(resolved?.mark, "instance");
});
Deno.test("RegisterInstance: should run init function during registration", () => {
container.clear();
@Register("InstanceIdentifier", (x: new () => TestClass) => {
const instance = new x();
instance.mark = "init";
return instance;
})
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = "instance";
}
const resolved = container.resolve<TestClass>("InstanceIdentifier");
assertEquals(resolved?.mark, "init");
});
Deno.test("RegisterInstance: instance should persist modifications", () => {
container.clear();
@Register("InstanceIdentifier", (x) => new x())
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = "instance";
public test: string = "test";
}
const instance1 = container.resolve<TestClass>("InstanceIdentifier");
if (instance1 == null) {
throw new Error("Instance1 is null");
}
instance1.test = "test2";
const instance2 = container.resolve<TestClass>("InstanceIdentifier");
if (instance2 == null) {
throw new Error("Instance2 is null");
}
assertEquals(instance2.test, "test2");
});
Deno.test("RegisterInstance: init function should persist modifications", () => {
container.clear();
@Register("InstanceIdentifier", (x: new () => TestClass) => {
const instance = new x();
instance.mark = "init";
return instance;
})
class TestClass {
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public mark: string = "instance";
public test: string = "test";
}
const instance1 = container.resolve<TestClass>("InstanceIdentifier");
if (instance1 == null) {
throw new Error("Instance1 is null");
}
instance1.test = "test2";
const instance2 = container.resolve<TestClass>("InstanceIdentifier");
if (instance2 == null) {
throw new Error("Instance2 is null");
}
assertEquals(instance2.test, "test2");
});

78
tests/TSInjex.ts Normal file
View File

@@ -0,0 +1,78 @@
// deno-coverage-ignore-file
// deno-lint-ignore-file
import {
assertEquals,
assertStrictEquals,
assertThrows,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { TSinjex } from "../src/classes/mod.ts";
const container = TSinjex.getInstance() as TSinjex;
Deno.test("should register and resolve a dependency (instance)", () => {
container.clear();
const identifier = "myDependency";
const dependency = { value: 42 };
container.register(identifier, dependency);
const resolved = container.resolve<typeof dependency>(identifier);
assertStrictEquals(resolved, dependency);
});
Deno.test("should register and resolve a dependency (static)", () => {
container.clear();
const identifier = "myDependency";
const dependency = { value: 42 };
TSinjex.register(identifier, dependency);
const resolved = TSinjex.resolve<typeof dependency>(identifier);
assertStrictEquals(resolved, dependency);
});
Deno.test("should throw error when resolving non-registered dependency (static)", () => {
container.clear();
const identifier = "nonExistentDependency";
assertThrows(() => {
TSinjex.resolve<unknown>(identifier);
});
});
Deno.test("should return undefined when resolving non-necessary dependency", () => {
container.clear();
const result = TSinjex.resolve<unknown>("nonExistentDependency", false);
assertEquals(result, undefined);
});
Deno.test("should warn when resolving a deprecated dependency", () => {
container.clear();
const identifier = "deprecatedDependency";
const dependency = { value: 42 };
// Mock console.warn
const originalWarn = console.warn;
let wasCalled = false;
let lastMessage = "";
console.warn = (msg: string) => {
wasCalled = true;
lastMessage = msg;
};
try {
TSinjex.register(identifier, dependency, true);
const resolved = TSinjex.resolve<typeof dependency>(identifier);
assertStrictEquals(resolved, dependency);
if (!wasCalled) {
throw new Error("console.warn was not called");
}
} finally {
console.warn = originalWarn; // Restore
}
});

View File

@@ -1,42 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": false,
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"importHelpers": true,
"isolatedModules": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2021.WeakRef"
],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,18 +0,0 @@
{
"entryPoints": [
"src/**/*.ts"
],
"out": ".locale/docs",
"tsconfig": "tsconfig.json",
"excludePrivate": false,
"excludeProtected": false,
"excludeExternals": false,
"includeVersion": true,
"readme": "README.md",
"exclude": [
"**/*.test.ts",
"**/*.spec.ts"
],
"theme": "default",
"hideGenerator": true
}