Compare commits

...

30 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
52 changed files with 687 additions and 9395 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,117 +0,0 @@
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 }}

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:
- 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,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"
}

View File

@@ -9,15 +9,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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
@@ -41,22 +106,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add pre release building to release workflow on dev/* branches an version changes.
- 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]
@@ -73,23 +134,23 @@ 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/PxaMMaxP/TSinjex/compare/0.0.14...HEAD
[0.0.14]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.13...v0.0.14
[0.2.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...v0.2.0
[0.3.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.2.0...v0.3.0
[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,34 +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
### 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,111 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const yargs = require('yargs');
// CLI argument parsing
const argv = yargs
.option('src', {
alias: 's',
type: 'string',
description: 'Directory to search for files',
default: 'src',
})
.option('output', {
alias: 'o',
type: 'string',
description: 'Path to the output file',
default: 'src/auto-imports.ts',
})
.option('pattern', {
alias: 'p',
type: 'string',
description: 'File pattern to search for (e.g., .ts, .js)',
default: '.ts',
})
.help()
.argv;
// Fixed RegEx patterns for decorator detection
const SEARCH_PATTERNS = [
/^@Register(?:<(.+)?>)?\(\s*["']{1}(.+)?["']{1}\s*,?\s*(true|false)?\s*\)/m, // Matches @Register(...)
/^@RegisterInstance(?:<(.+)?>)?\(\s*["']{1}(.+)?["']{1}\s*,?\s*(.+)?\s*\)/m, // Matches @RegisterInstance(...)
];
const FILE_PATTERN = argv.pattern.startsWith('.') ? argv.pattern : `.${argv.pattern}`; // Ensure the pattern starts with a dot
/**
* Recursively searches for all files in a directory matching the specified pattern.
* @param {string} dirPath - The directory to search.
* @returns {string[]} - List of matching files.
*/
function getAllFiles(dirPath) {
let files = fs.readdirSync(dirPath);
let arrayOfFiles = [];
files.forEach((file) => {
const fullPath = path.join(dirPath, file);
if (fs.statSync(fullPath).isDirectory()) {
arrayOfFiles = arrayOfFiles.concat(getAllFiles(fullPath));
} else if (file.endsWith(FILE_PATTERN)) {
arrayOfFiles.push(fullPath);
}
});
return arrayOfFiles;
}
/**
* Filters files that contain at least one of the specified regex patterns.
* @param {string[]} files - List of files to check.
* @returns {string[]} - Files that contain at least one of the specified patterns.
*/
function findFilesWithPattern(files) {
return files.filter((file) => {
const content = fs.readFileSync(file, 'utf8');
return SEARCH_PATTERNS.some((pattern) => pattern.test(content));
});
}
/**
* Generates an import file containing imports for all found files.
* @param {string[]} files - List of relevant files.
* @returns {string} - Generated import code.
*/
function generateImports(files) {
return files.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n') + '\n';
}
/**
* Main function that executes the script.
*/
function main() {
try {
const srcDir = path.resolve(process.cwd(), argv.src);
const outputFile = path.resolve(process.cwd(), argv.output);
if (!fs.existsSync(srcDir)) {
console.error(`❌ Error: The directory '${srcDir}' does not exist.`);
process.exit(1);
}
const allFiles = getAllFiles(srcDir);
const filesWithPattern = findFilesWithPattern(allFiles);
if (filesWithPattern.length === 0) {
console.log(`ℹ️ No ${FILE_PATTERN} files found matching the specified decorator patterns.`);
return;
}
const importContent = generateImports(filesWithPattern);
fs.writeFileSync(outputFile, importContent);
console.log(`✅ Imports successfully generated: ${outputFile}`);
} catch (error) {
console.error('❌ An error occurred:', error.message);
process.exit(1);
}
}
main();

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,66 +0,0 @@
{
"name": "ts-injex",
"version": "0.3.0",
"description": "Simple boilerplate code free dependency injection system for TypeScript.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"tsinjex-generate": "./bin/generate-imports.cjs"
},
"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.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,394 +0,0 @@
/* 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,
mode: 'instance' | 'standalone' = 'standalone',
): 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',
mode === 'instance' ? 'instance' : undefined,
)
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',
mode === 'instance' ? 'instance' : undefined,
)
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,18 +0,0 @@
/* eslint-disable deprecation/deprecation */
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);
test_RegisterInstanceDecorator(TSinjex, Register, 'instance');

View File

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

@@ -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,79 +0,0 @@
/* 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

@@ -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 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**.
@@ -21,6 +10,11 @@ import { Identifier } from '../types/Identifier';
* @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 {
/**
@@ -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,151 +1,54 @@
import {
DependencyResolutionError,
InitializationError,
InjectorError,
NoInstantiationMethodError,
} from 'src/interfaces/Exceptions';
import { TSinjex } from '../classes/TSinjex';
import { Identifier } from '../types/Identifier';
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 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>(
export function Inject<InstanzType, DependencyType, FieldType extends object>(
identifier: Identifier,
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);
};
Object.defineProperty(target, propertyKey, {
get() {
let instance: T | U | undefined;
const dependency: T | undefined = tryAndCatch(
() => resolve(),
necessary,
identifier,
DependencyResolutionError,
);
if (dependency != null) {
const initFunction: (() => U) | undefined =
typeof init === 'function' && dependency != null
? (): U => init(dependency)
: init === true && hasConstructor(dependency)
? (): U => new dependency() as U
: undefined;
if (init == null) instance = dependency;
else if (initFunction != null)
instance = tryAndCatch(
initFunction,
necessary,
identifier,
InitializationError,
);
else if (necessary)
throw new NoInstantiationMethodError(identifier);
} else if (necessary)
throw new DependencyResolutionError(identifier);
/**
* Replace itself with the resolved dependency
* for performance reasons.
*/
Object.defineProperty(this, propertyKey, {
value: instance,
writable: false,
enumerable: false,
configurable: false,
});
return instance;
},
/**
* Make the property configurable to allow replacing it
*/
configurable: true,
});
};
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.");
}
/**
* 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 {
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 {
return fn();
instance = init(dependency);
} catch (error) {
if (necessary)
throw new (errorClass != null ? errorClass : error)(
identifier ?? 'not specified',
error,
if (isNecessary) {
throw new InitializationError(
identifier,
error instanceof Error ? error : new Error(String(error)),
);
else return undefined;
} else {
console.warn(
`Error initializing not necessary dependency ${identifier.toString()}: ${error}`,
);
instance = undefined;
}
}
}
/**
* 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 instance as FieldType;
};
return (
_obj?.prototype != null &&
typeof _obj.prototype.constructor === 'function'
);
return function (_initialValue: FieldType): FieldType {
return initializer();
};
};
}

View File

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

View File

@@ -1,39 +0,0 @@
import { Register } from './Register';
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.
* @param deprecated If true, the dependency is deprecated and a warning
* is logged only once upon the first resolution of the dependency.
* @see {@link InitDelegate} for more information on initializer functions.
* @returns The decorator function to be applied on the class.
* @example
* ```ts
* \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor())
* class MyClass {
* // ...
* }
* ```
* @deprecated Use {@link Register} instead. This decorator already uses the {@link Register} decorator internally.
*/
export function RegisterInstance<
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
>(
identifier: Identifier,
init?: InitDelegate<
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
InstanceType<TargetType>
>,
deprecated?: boolean,
): (constructor: TargetType, ...args: unknown[]) => void {
const initDelegate = typeof init === 'function' ? init : undefined;
if (initDelegate) return Register(identifier, initDelegate, deprecated);
else return Register(identifier, 'instance', deprecated);
}

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 { 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);
}

View File

@@ -1,40 +0,0 @@
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[]) => {};
}

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

View File

@@ -1,5 +1,4 @@
import { DependencyResolutionError } from './Exceptions';
import { Identifier } from '../types/Identifier';
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;

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
}