Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
537215432e Bump ts-jest from 29.2.4 to 29.2.5
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.2.4 to 29.2.5.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.2.4...v29.2.5)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 13:00:12 +00:00
51 changed files with 9044 additions and 699 deletions

13
.eslintignore Normal file
View File

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

299
.eslintrc Normal file
View File

@@ -0,0 +1,299 @@
{
"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"
}
}
]
}

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# 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"

117
.github/workflows/CreateRelease.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
name: Create Release
on:
push:
branches:
- main
- 'dev/*'
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 tests and build
run: npm run prepare:deploy
- name: Get the version
id: get_version
run: |
VERSION=$(npm run version:show | tail -n 1)
echo "VERSION=v$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 "See [CHANGELOG.md](./CHANGELOG.md) for more details."
echo "$notes"
echo ""
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 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: Set Release Prerelease Flag
id: set_prerelease_flag
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "PRE_RELEASE=false" >> $GITHUB_ENV
elif [[ "${{ github.ref }}" == refs/heads/dev/* ]]; then
echo "PRE_RELEASE=true" >> $GITHUB_ENV
fi
- 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: ${{ env.PRE_RELEASE }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

44
.github/workflows/DeployTypeDoc.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
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

31
.github/workflows/PullRequestTest.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,23 @@
name: Validate Branch Name on Pull Request
on:
pull_request:
branches:
- UNDEFINED
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 +1,9 @@
coverage/
# Internal
.vscode
.locale
.VSCodeCounter
*.ignore.*
dist/*
node_modules/*

20
.prettierrc.json Normal file
View File

@@ -0,0 +1,20 @@
{
"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
View File

@@ -1,20 +0,0 @@
{
"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"
}
]
}

View File

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

View File

@@ -9,117 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
### Deprecated
- Add pre release building to release workflow on dev/* branches an version changes.
### 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
@@ -134,23 +38,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
[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

34
README.md Normal file
View File

@@ -0,0 +1,34 @@
![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
### Identifiers
Strings and symbols are possible for the **identifiers**.
### Jest
For the use of TSinjex with Jest, the corresponding source files can be found under `./src` of the TSinjex node_module folder. To use these files, the `moduleNameMapper` must be configured in the Jest configuration file. The following example shows how to configure the Jest configuration file to use the source files of TSinjex.
#### 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`
],
};
```

View File

@@ -1,35 +0,0 @@
{
"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
View File

@@ -1,39 +0,0 @@
{
"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"
}
}

22
jest.config.cjs Normal file
View File

@@ -0,0 +1,22 @@
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,
},
},
};

30
jest.config.coverage.cjs Normal file
View File

@@ -0,0 +1,30 @@
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 Normal file

File diff suppressed because it is too large Load Diff

63
package.json Normal file
View File

@@ -0,0 +1,63 @@
{
"name": "ts-injex",
"version": "0.1.0",
"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)\"",
"prepare:deploy": "npm install && npm run test:verbose && npm run build:tsc"
},
"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.5",
"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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,55 @@
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.');
});

17
scripts/jest.setup.js Normal file
View File

@@ -0,0 +1,17 @@
// 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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,387 @@
/* istanbul ignore file */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Inject } from 'src/decorators/Inject';
import { DependencyResolutionError } from 'src/interfaces/Exceptions';
import { ForceConstructor } from 'src/types/GenericContructor';
import { 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);
});
it('should replace the property with the resolved dependency', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier')
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
public isDependencyTypeofFunction() {
return typeof this._dependency === 'function';
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value');
expect(instance.isDependencyTypeofFunction()).toBe(false);
expect(instance.getDependency().value).toBe('test-value');
});
it('should use a empty initializer when none is provided but true', () => {
container.register(
'MockDependencyIdentifier',
class X {
public value: string = 'test-value';
constructor() {}
},
);
class TestClass {
@Inject('MockDependencyIdentifier', true)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
const instance = new TestClass();
expect(instance.getDependency().value).toBe('test-value');
});
it('should throw an error when the dependency has no instantiation method', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier', true)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).toThrow(new RegExp('No instantiation method found for.*'));
});
it('should not throw an error when the dependency has no instantiation method if not necessary', () => {
container.register('MockDependencyIdentifier', {
value: 'test-value',
});
class TestClass {
@Inject('MockDependencyIdentifier', true, false)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).not.toThrow(new RegExp('No instantiation method found for.*'));
});
it('should throw an error when the dependency cannot be resolved', () => {
container.register('MockDependencyIdentifier', null);
class TestClass {
@Inject('MockDependencyIdentifier', true)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).toThrow(new RegExp('.*could not be resolved.*'));
});
it('should not throw an error when the dependency cannot be resolved if not necessary', () => {
container.register('MockDependencyIdentifier', null);
class TestClass {
@Inject('MockDependencyIdentifier', true, false)
private readonly _dependency!: any;
public getDependency() {
return this._dependency;
}
}
expect(() => {
const instance = new TestClass();
instance.getDependency();
}).not.toThrow(new RegExp('.*could not be resolved.*'));
});
});
}
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,71 @@
/* istanbul ignore file */
/* 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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,79 @@
/* istanbul ignore file */
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

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

View File

@@ -1,3 +1,14 @@
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';
import { Identifier } from '../types/Identifier';
/**
* # TSinjex
* The main class for the Dependency Injection Container **TSinjex**.
@@ -10,11 +21,6 @@
* @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 {
/**
@@ -117,9 +123,5 @@ export class TSinjex implements ITSinjex {
return dependency.dependency as T;
}
public clear(): void {
this._dependencies.clear();
}
//#endregion
}

View File

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

View File

@@ -1,54 +1,151 @@
import { TSinjex } from "../classes/mod.ts";
import { InitializationError } from "../interfaces/mod.ts";
import { Identifier, InitDelegate } from "../types/mod.ts";
import {
DependencyResolutionError,
InitializationError,
InjectorError,
NoInstantiationMethodError,
} from 'src/interfaces/Exceptions';
import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate';
export function Inject<InstanzType, DependencyType, FieldType extends object>(
/**
* A decorator to inject a dependency from a DI (Dependency Injection) container into a class property.
* @template T The type of the dependency to be injected.
* @template U The type of the property to be injected.
* @param identifier The identifier used to resolve the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param init Optional an initializer function to transform the dependency before injection
* or true to instantiate the dependency if it has a constructor.
* @see {@link InitDelegate} for more information on initializer functions.
* @param necessary If true, throws an error if the dependency is not found.
* @returns The resolved dependency or undefined if the dependency is not necessary
* and not found, or throws an error if the dependency is necessary and not found.
* @throws **Only throws errors if the dependency is necessary.**
* @throws A {@link DependencyResolutionError} if the dependency is not found.
* @throws A {@link InjectorError} if an error occurs during the injection process.
* @throws A {@link NoInstantiationMethodError} if the dependency does not have a constructor.
* @throws An {@link InitializationError} if an error occurs during the initialization process.
* @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>(
identifier: Identifier,
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.");
}
init?: InitDelegate<T, U> | true,
necessary = true,
) {
return function (target: unknown, propertyKey: string | symbol): void {
/**
* Function to evaluate the dependency lazily
* to avoid circular dependencies, not found dependencies, etc.
* @returns The resolved dependency or undefined if the dependency is not found.
*/
const resolve = (): T | undefined => {
return TSinjex.getInstance().resolve<T>(identifier, necessary);
};
const initializer = () => {
let instance: DependencyType | FieldType | undefined;
Object.defineProperty(target, propertyKey, {
get() {
let instance: T | U | 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(
const dependency: T | undefined = tryAndCatch(
() => resolve(),
necessary,
identifier,
error instanceof Error ? error : new Error(String(error)),
DependencyResolutionError,
);
} else {
console.warn(
`Error initializing not necessary dependency ${identifier.toString()}: ${error}`,
if (dependency != null) {
const initFunction: (() => U) | undefined =
typeof init === 'function' && dependency != null
? (): U => init(dependency)
: init === true && hasConstructor(dependency)
? (): U => new dependency() as U
: undefined;
if (init == null) instance = dependency;
else if (initFunction != null)
instance = tryAndCatch(
initFunction,
necessary,
identifier,
InitializationError,
);
instance = undefined;
else if (necessary)
throw new NoInstantiationMethodError(identifier);
} else if (necessary)
throw new DependencyResolutionError(identifier);
/**
* Replace itself with the resolved dependency
* for performance reasons.
*/
Object.defineProperty(this, propertyKey, {
value: instance,
writable: false,
enumerable: false,
configurable: false,
});
return instance;
},
/**
* Make the property configurable to allow replacing it
*/
configurable: true,
});
};
}
/**
* Tries to execute a function and catches any errors that occur.
* If the function is necessary and an error occurs, it throws the error
* with the specified error class and identifier.
* @param fn The function to execute.
* @param necessary If true, throws an error if an error occurs.
* @param identifier The identifier of the dependency.
* @param errorClass The error class to throw if an error occurs.
* @returns The result of the function or undefined if an error occurs and the function is not necessary.
*/
function tryAndCatch<ReturnType, ErrorType>(
fn: () => ReturnType,
necessary: boolean,
identifier?: Identifier,
errorClass?: ErrorType,
): ReturnType | undefined {
try {
return fn();
} catch (error) {
if (necessary)
throw new (errorClass != null ? errorClass : error)(
identifier ?? 'not specified',
error,
);
else return undefined;
}
}
return instance as FieldType;
};
/**
* Checks if an object has a constructor.
* @param obj The object to check.
* @returns True if the object has a constructor, false otherwise.
*/
function hasConstructor<T>(obj: T): obj is T & { new (): unknown } {
const _obj = obj as unknown as { prototype?: { constructor?: unknown } };
return function (_initialValue: FieldType): FieldType {
return initializer();
};
};
return (
_obj?.prototype != null &&
typeof _obj.prototype.constructor === 'function'
);
}

View File

@@ -1,88 +1,30 @@
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.");
}
import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier';
/**
* A decorator to register a class in the **TSinjex** DI (Dependency Injection) container.
* @template TargetType The type of the class to be registered.
* @param identifier The identifier used to register the class in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @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;
if (init == undefined) {
diContainer.register(identifier, target, deprecated);
} else {
diContainer.register(
identifier,
createLazyProxy(
_instance,
init,
target,
),
deprecated,
);
}
// Register the class in the DI container
diContainer.register(identifier, constructor, 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

@@ -0,0 +1,73 @@
import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier';
import { InitDelegate } from '../types/InitDelegate';
/**
* A decorator to register an instance of a class in the DI (Dependency Injection) container.
* @template TargetType The type of the class whose instance is to be registered.
* @param identifier The identifier used to register the instance in the DI container.
* @see {@link Identifier} for more information on identifiers.
* @param init An optional initializer function which get the constructor of the class
* 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);
};
}

View File

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

View File

39
src/functions/register.ts Normal file
View File

@@ -0,0 +1,39 @@
import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier';
/**
* 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);
}

40
src/functions/resolve.ts Normal file
View File

@@ -0,0 +1,40 @@
import { TSinjex } from '../classes/TSinjex';
import { DependencyResolutionError } from '../interfaces/Exceptions';
import { Identifier } from '../types/Identifier';
/**
* 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[]) => {};
}

View File

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

18
src/index.ts Normal file
View File

@@ -0,0 +1,18 @@
/* 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,5 @@
import { Identifier } from "../types/mod.ts";
import { Identifier } from 'src/types/Identifier';
import { ITSinjex } from './ITSinjex';
/**
* General error class for {@link ITSinjex} interface.
@@ -10,7 +11,7 @@ export class TSinjexError extends Error {
*/
constructor(message: string) {
super(message);
this.name = "TSinjex";
this.name = 'TSinjex';
}
}
@@ -25,7 +26,7 @@ export class DependencyResolutionError extends TSinjexError {
*/
constructor(identifier: Identifier) {
super(`Dependency ${identifier.toString()} could not be resolved.`);
this.name = "TSinjexResolutionError";
this.name = 'TSinjexResolutionError';
}
}
@@ -43,7 +44,7 @@ export class InjectorError extends TSinjexError {
super(
`Error injecting dependency ${identifier.toString()} with error: "${originalError}"`,
);
this.name = "TSinjexInjectorError";
this.name = 'TSinjexInjectorError';
}
}
@@ -60,7 +61,7 @@ export class NoInstantiationMethodError extends TSinjexError {
super(
`No instantiation method found for dependency ${identifier.toString()}.`,
);
this.name = "TSinjexNoInstantiationMethodError";
this.name = 'TSinjexNoInstantiationMethodError';
}
}
@@ -78,6 +79,6 @@ export class InitializationError extends TSinjexError {
super(
`Error initializing dependency ${identifier.toString()} with error: "${originalError}"`,
);
this.name = "TSinjexInitializationError";
this.name = 'TSinjexInitializationError';
}
}

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
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,8 +11,3 @@ 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

@@ -1,5 +0,0 @@
// 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";

View File

@@ -1,229 +0,0 @@
// 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");
});

View File

@@ -1,78 +0,0 @@
// 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
}
});

42
tsconfig.json Normal file
View File

@@ -0,0 +1,42 @@
{
"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"
]
}

18
typedoc.json Normal file
View File

@@ -0,0 +1,18 @@
{
"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
}