decorator-dependency-injection 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -16
- package/eslint.config.js +73 -0
- package/index.d.ts +146 -0
- package/index.js +56 -126
- package/package.json +46 -7
- package/src/Container.js +184 -0
- package/src/proxy.js +42 -0
- package/.github/workflows/release.yml +0 -129
- package/babel.config.json +0 -6
- package/test/injection.test.js +0 -309
- package/test/injectionLazy.test.js +0 -249
- package/test/mock.test.js +0 -295
- package/test/proxy.test.js +0 -130
package/package.json
CHANGED
|
@@ -1,34 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decorator-dependency-injection",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A simple library for dependency injection using decorators",
|
|
5
5
|
"author": "Ravi Gairola <mallox@pyxzl.net>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mallocator/decorator-dependency-injection.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/mallocator/decorator-dependency-injection/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/mallocator/decorator-dependency-injection#readme",
|
|
7
15
|
"keywords": [
|
|
8
16
|
"dependency-injection",
|
|
9
17
|
"di",
|
|
10
18
|
"decorators",
|
|
11
|
-
"mocking"
|
|
19
|
+
"mocking",
|
|
20
|
+
"singleton",
|
|
21
|
+
"factory"
|
|
12
22
|
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
13
26
|
"main": "index.js",
|
|
27
|
+
"types": "index.d.ts",
|
|
14
28
|
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./index.d.ts",
|
|
32
|
+
"import": "./index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
15
35
|
"scripts": {
|
|
16
36
|
"start": "babel-node index.js",
|
|
17
|
-
"test": "jest"
|
|
37
|
+
"test": "jest --no-watchman",
|
|
38
|
+
"test:coverage": "jest --no-watchman --coverage",
|
|
39
|
+
"typecheck": "tsc test/types.check.ts --noEmit --esModuleInterop --skipLibCheck",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"lint:fix": "eslint . --fix"
|
|
18
42
|
},
|
|
19
43
|
"jest": {
|
|
20
44
|
"transform": {
|
|
21
45
|
"^.+\\.js$": "babel-jest"
|
|
22
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"coverageThreshold": {
|
|
48
|
+
"global": {
|
|
49
|
+
"branches": 80,
|
|
50
|
+
"functions": 80,
|
|
51
|
+
"lines": 80,
|
|
52
|
+
"statements": 80
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"collectCoverageFrom": [
|
|
56
|
+
"index.js",
|
|
57
|
+
"src/**/*.js"
|
|
58
|
+
]
|
|
23
59
|
},
|
|
24
60
|
"devDependencies": {
|
|
25
61
|
"@babel/cli": "^7.26.4",
|
|
26
62
|
"@babel/core": "^7.26.9",
|
|
63
|
+
"@babel/eslint-parser": "^7.28.6",
|
|
27
64
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
28
|
-
"@babel/polyfill": "^7.12.1",
|
|
29
65
|
"@babel/preset-env": "^7.26.9",
|
|
30
66
|
"@babel/register": "^7.25.9",
|
|
67
|
+
"@eslint/js": "^9.39.2",
|
|
31
68
|
"@types/jest": "^29.5.14",
|
|
32
|
-
"
|
|
69
|
+
"eslint": "^9.39.2",
|
|
70
|
+
"jest": "^29.7.0",
|
|
71
|
+
"typescript": "^5.9.3"
|
|
33
72
|
}
|
|
34
|
-
}
|
|
73
|
+
}
|
package/src/Container.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} InstanceContext
|
|
3
|
+
* @property {'singleton'|'factory'} type - The type of the instance.
|
|
4
|
+
* @property {Function} clazz - The class constructor for the instance.
|
|
5
|
+
* @property {Function} [originalClazz] - The original class if this is a mock.
|
|
6
|
+
* @property {Object} [instance] - The singleton instance, if created.
|
|
7
|
+
* @property {Object} [originalInstance] - The original instance if this is a mock.
|
|
8
|
+
* @property {boolean} [proxy=false] - If true, the mock will proxy to the original class for undefined methods/properties.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {createProxy} from './proxy.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A dependency injection container that manages singleton and factory instances.
|
|
15
|
+
* Supports mocking for testing purposes.
|
|
16
|
+
*/
|
|
17
|
+
export class Container {
|
|
18
|
+
/** @type {Map<string|Function, InstanceContext>} */
|
|
19
|
+
#instances = new Map()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a class as a singleton.
|
|
23
|
+
* @param {Function} clazz The class constructor
|
|
24
|
+
* @param {string} [name] Optional name key
|
|
25
|
+
*/
|
|
26
|
+
registerSingleton(clazz, name) {
|
|
27
|
+
this.#register(clazz, 'singleton', name)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a class as a factory.
|
|
32
|
+
* @param {Function} clazz The class constructor
|
|
33
|
+
* @param {string} [name] Optional name key
|
|
34
|
+
*/
|
|
35
|
+
registerFactory(clazz, name) {
|
|
36
|
+
this.#register(clazz, 'factory', name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Internal registration logic.
|
|
41
|
+
* @param {Function} clazz The class constructor
|
|
42
|
+
* @param {'singleton'|'factory'} type The registration type
|
|
43
|
+
* @param {string} [name] Optional name key
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
#register(clazz, type, name) {
|
|
47
|
+
const key = name ?? clazz
|
|
48
|
+
if (this.#instances.has(key)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'A different class is already registered under this name. ' +
|
|
51
|
+
'This may be a circular dependency. Try using @InjectLazy'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
this.#instances.set(key, {clazz, type})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the context for a given class or name.
|
|
59
|
+
* @param {string|Function} clazzOrName The class or name to look up
|
|
60
|
+
* @returns {InstanceContext}
|
|
61
|
+
* @throws {Error} If the context is not found
|
|
62
|
+
*/
|
|
63
|
+
getContext(clazzOrName) {
|
|
64
|
+
if (this.#instances.has(clazzOrName)) {
|
|
65
|
+
return this.#instances.get(clazzOrName)
|
|
66
|
+
}
|
|
67
|
+
const available = Array.from(this.#instances.keys())
|
|
68
|
+
.map(k => typeof k === 'string' ? k : k.name)
|
|
69
|
+
.join(', ')
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Cannot find injection source for "${clazzOrName?.name || clazzOrName}". ` +
|
|
72
|
+
`Available: [${available}]`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a class or name is registered.
|
|
78
|
+
* @param {string|Function} clazzOrName The class or name to check
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
has(clazzOrName) {
|
|
82
|
+
return this.#instances.has(clazzOrName)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get or create an instance based on the context.
|
|
87
|
+
* @param {InstanceContext} instanceContext The instance context
|
|
88
|
+
* @param {Array} params Constructor parameters
|
|
89
|
+
* @returns {Object} The instance
|
|
90
|
+
*/
|
|
91
|
+
getInstance(instanceContext, params) {
|
|
92
|
+
if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
|
|
93
|
+
return instanceContext.instance
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let instance
|
|
97
|
+
try {
|
|
98
|
+
instance = new instanceContext.clazz(...params)
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof RangeError) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Circular dependency detected for ${instanceContext.clazz.name || instanceContext.clazz}. ` +
|
|
103
|
+
`Use @InjectLazy to break the cycle.`
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
throw err
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (instanceContext.proxy && instanceContext.originalClazz) {
|
|
110
|
+
const originalInstance = new instanceContext.originalClazz(...params)
|
|
111
|
+
instance = createProxy(instance, originalInstance)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (instanceContext.type === 'singleton') {
|
|
115
|
+
instanceContext.instance = instance
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return instance
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register a mock for an existing class.
|
|
123
|
+
* @param {string|Function} targetClazzOrName The class or name to mock
|
|
124
|
+
* @param {Function} mockClazz The mock class
|
|
125
|
+
* @param {boolean} [useProxy=false] Whether to proxy unmocked methods to original
|
|
126
|
+
*/
|
|
127
|
+
registerMock(targetClazzOrName, mockClazz, useProxy = false) {
|
|
128
|
+
const instanceContext = this.getContext(targetClazzOrName)
|
|
129
|
+
if (instanceContext.originalClazz) {
|
|
130
|
+
throw new Error('Mock already defined, reset before mocking again')
|
|
131
|
+
}
|
|
132
|
+
instanceContext.originalClazz = instanceContext.clazz
|
|
133
|
+
instanceContext.proxy = useProxy
|
|
134
|
+
instanceContext.clazz = mockClazz
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reset a specific mock to its original class.
|
|
139
|
+
* @param {string|Function} clazzOrName The class or name to reset
|
|
140
|
+
* @throws {Error} If the class or name is not registered
|
|
141
|
+
*/
|
|
142
|
+
resetMock(clazzOrName) {
|
|
143
|
+
const key = typeof clazzOrName === 'string' ? clazzOrName : clazzOrName
|
|
144
|
+
this.#restoreOriginal(this.#instances.get(key), clazzOrName)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Reset all mocks to their original classes.
|
|
149
|
+
*/
|
|
150
|
+
resetAllMocks() {
|
|
151
|
+
for (const instanceContext of this.#instances.values()) {
|
|
152
|
+
this.#restoreOriginal(instanceContext)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Clear all registered instances and mocks.
|
|
158
|
+
* Useful for test isolation.
|
|
159
|
+
*/
|
|
160
|
+
clear() {
|
|
161
|
+
this.#instances.clear()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Internal function to restore an instance context to its original.
|
|
166
|
+
* @param {InstanceContext} instanceContext The instance context to reset
|
|
167
|
+
* @param {string|Function} [clazzOrName] Optional identifier for error messages
|
|
168
|
+
* @throws {Error} If instanceContext is null or undefined
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
#restoreOriginal(instanceContext, clazzOrName) {
|
|
172
|
+
if (!instanceContext) {
|
|
173
|
+
const name = clazzOrName?.name || clazzOrName || 'unknown'
|
|
174
|
+
throw new Error(`Cannot reset mock for "${name}": not registered`)
|
|
175
|
+
}
|
|
176
|
+
if (instanceContext.originalClazz) {
|
|
177
|
+
instanceContext.clazz = instanceContext.originalClazz
|
|
178
|
+
delete instanceContext.instance
|
|
179
|
+
delete instanceContext.originalClazz
|
|
180
|
+
delete instanceContext.originalInstance
|
|
181
|
+
delete instanceContext.proxy
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/proxy.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a proxy that delegates to the mock first, then falls back to the original.
|
|
3
|
+
* This allows partial mocking where only specific methods are overridden.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} mock The mock instance
|
|
6
|
+
* @param {Object} original The original instance to fall back to
|
|
7
|
+
* @returns {Proxy} A proxy that delegates appropriately
|
|
8
|
+
*/
|
|
9
|
+
export function createProxy(mock, original) {
|
|
10
|
+
return new Proxy(mock, {
|
|
11
|
+
get(target, prop, receiver) {
|
|
12
|
+
if (prop in target) {
|
|
13
|
+
return Reflect.get(target, prop, receiver)
|
|
14
|
+
}
|
|
15
|
+
return Reflect.get(original, prop, original)
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
set(target, prop, value, receiver) {
|
|
19
|
+
if (prop in target) {
|
|
20
|
+
return Reflect.set(target, prop, value, receiver)
|
|
21
|
+
}
|
|
22
|
+
return Reflect.set(original, prop, value, original)
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
has(target, prop) {
|
|
26
|
+
return prop in target || prop in original
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
ownKeys(target) {
|
|
30
|
+
const mockKeys = Reflect.ownKeys(target)
|
|
31
|
+
const originalKeys = Reflect.ownKeys(original)
|
|
32
|
+
return [...new Set([...mockKeys, ...originalKeys])]
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
36
|
+
if (prop in target) {
|
|
37
|
+
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
38
|
+
}
|
|
39
|
+
return Reflect.getOwnPropertyDescriptor(original, prop)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ "main" ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ "main" ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
node-version: [22.x, 23.x]
|
|
15
|
-
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v4
|
|
18
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
19
|
-
uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: ${{ matrix.node-version }}
|
|
22
|
-
cache: 'npm'
|
|
23
|
-
- run: npm ci
|
|
24
|
-
- run: npm run build --if-present
|
|
25
|
-
- run: npm test
|
|
26
|
-
|
|
27
|
-
release:
|
|
28
|
-
needs: build
|
|
29
|
-
runs-on: ubuntu-latest
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
steps:
|
|
33
|
-
- name: Checkout Repository
|
|
34
|
-
uses: actions/checkout@v4
|
|
35
|
-
|
|
36
|
-
- name: Install Node.js
|
|
37
|
-
uses: actions/setup-node@v4
|
|
38
|
-
with:
|
|
39
|
-
node-version: '22'
|
|
40
|
-
|
|
41
|
-
- name: Get package.json version
|
|
42
|
-
id: package_version
|
|
43
|
-
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
|
44
|
-
|
|
45
|
-
- name: Check if tag exists
|
|
46
|
-
id: check_tag
|
|
47
|
-
run: |
|
|
48
|
-
TAG_EXISTS=$(git ls-remote --tags origin "v${VERSION}" | wc -l)
|
|
49
|
-
echo "TAG_EXISTS=${TAG_EXISTS}" >> $GITHUB_ENV
|
|
50
|
-
|
|
51
|
-
- name: Get commit messages
|
|
52
|
-
id: commit_messages
|
|
53
|
-
run: |
|
|
54
|
-
COMMITS=$(git log --pretty=format:"%h - %s" $(git describe --tags --abbrev=0 @^)..@ | while read -r hash msg; do echo "[${hash}](https://github.com/${{ github.repository }}/commit/${hash}) - ${msg}"; done)
|
|
55
|
-
echo "COMMITS=${COMMITS}" >> $GITHUB_ENV
|
|
56
|
-
|
|
57
|
-
- name: Create Tag and Release
|
|
58
|
-
uses: actions/github-script@v7
|
|
59
|
-
with:
|
|
60
|
-
script: |
|
|
61
|
-
const version = process.env.VERSION
|
|
62
|
-
const tag = `v${version}`
|
|
63
|
-
const commits = process.env.COMMITS
|
|
64
|
-
const { data: tags } = await github.rest.repos.listTags({
|
|
65
|
-
owner: context.repo.owner,
|
|
66
|
-
repo: context.repo.repo,
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
if (!tags.find(t => t.name === tag)) {
|
|
70
|
-
await github.rest.git.createRef({
|
|
71
|
-
owner: context.repo.owner,
|
|
72
|
-
repo: context.repo.repo,
|
|
73
|
-
ref: `refs/tags/${tag}`,
|
|
74
|
-
sha: context.sha,
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
await github.rest.repos.createRelease({
|
|
78
|
-
owner: context.repo.owner,
|
|
79
|
-
repo: context.repo.repo,
|
|
80
|
-
tag_name: tag,
|
|
81
|
-
name: `Release ${tag}`,
|
|
82
|
-
body: `Automated release of version ${tag}\n\nCommits:\n${commits}`,
|
|
83
|
-
draft: false,
|
|
84
|
-
prerelease: false,
|
|
85
|
-
})
|
|
86
|
-
core.notice(`Release ${tag} created.`)
|
|
87
|
-
} else {
|
|
88
|
-
core.notice(`Release ${tag} already exists. No new release created.`)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
publish:
|
|
92
|
-
needs: release
|
|
93
|
-
runs-on: ubuntu-latest
|
|
94
|
-
steps:
|
|
95
|
-
- name: Checkout Repository
|
|
96
|
-
uses: actions/checkout@v4
|
|
97
|
-
|
|
98
|
-
- name: Install Node.js
|
|
99
|
-
uses: actions/setup-node@v4
|
|
100
|
-
with:
|
|
101
|
-
node-version: '22'
|
|
102
|
-
registry-url: 'https://registry.npmjs.org/'
|
|
103
|
-
|
|
104
|
-
- name: Get package.json version
|
|
105
|
-
id: package_version
|
|
106
|
-
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
|
107
|
-
|
|
108
|
-
- name: Check if version exists on npm
|
|
109
|
-
id: check_npm_version
|
|
110
|
-
run: |
|
|
111
|
-
if npm show decorator-dependency-injection@${{ env.VERSION }} > /dev/null 2>&1; then
|
|
112
|
-
echo "VERSION_EXISTS=true" >> $GITHUB_ENV
|
|
113
|
-
else
|
|
114
|
-
echo "VERSION_EXISTS=false" >> $GITHUB_ENV
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
- name: Publish to npm
|
|
118
|
-
if: env.VERSION_EXISTS == 'false'
|
|
119
|
-
run: |
|
|
120
|
-
npm publish --access public
|
|
121
|
-
echo "::notice::Published new version ${{ env.VERSION }} to npm."
|
|
122
|
-
env:
|
|
123
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
124
|
-
|
|
125
|
-
- name: Notice Publish Status
|
|
126
|
-
if: env.VERSION_EXISTS == 'true'
|
|
127
|
-
run: echo "::notice::Version ${{ env.VERSION }} already exists on npm. No new version published."
|
|
128
|
-
|
|
129
|
-
|