decorator-dependency-injection 1.0.3 → 1.0.5
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 +307 -16
- package/eslint.config.js +77 -0
- package/index.d.ts +227 -0
- package/index.js +207 -143
- package/package.json +46 -7
- package/src/Container.js +211 -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/src/Container.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
/** @type {boolean} Enable debug logging */
|
|
22
|
+
#debug = false
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enable or disable debug logging.
|
|
26
|
+
* When enabled, logs when instances are created.
|
|
27
|
+
* @param {boolean} enabled Whether to enable debug mode
|
|
28
|
+
*/
|
|
29
|
+
setDebug(enabled) {
|
|
30
|
+
this.#debug = enabled
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Log a debug message if debug mode is enabled.
|
|
35
|
+
* @param {string} message The message to log
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
#log(message) {
|
|
39
|
+
if (this.#debug) {
|
|
40
|
+
console.log(`[DI] ${message}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register a class as a singleton.
|
|
46
|
+
* @param {Function} clazz The class constructor
|
|
47
|
+
* @param {string} [name] Optional name key
|
|
48
|
+
*/
|
|
49
|
+
registerSingleton(clazz, name) {
|
|
50
|
+
this.#register(clazz, 'singleton', name)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register a class as a factory.
|
|
55
|
+
* @param {Function} clazz The class constructor
|
|
56
|
+
* @param {string} [name] Optional name key
|
|
57
|
+
*/
|
|
58
|
+
registerFactory(clazz, name) {
|
|
59
|
+
this.#register(clazz, 'factory', name)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Internal registration logic.
|
|
64
|
+
* @param {Function} clazz The class constructor
|
|
65
|
+
* @param {'singleton'|'factory'} type The registration type
|
|
66
|
+
* @param {string} [name] Optional name key
|
|
67
|
+
* @private
|
|
68
|
+
*/
|
|
69
|
+
#register(clazz, type, name) {
|
|
70
|
+
const key = name ?? clazz
|
|
71
|
+
if (this.#instances.has(key)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'A different class is already registered under this name. ' +
|
|
74
|
+
'This may be a circular dependency. Try using @InjectLazy'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
this.#instances.set(key, {clazz, type})
|
|
78
|
+
this.#log(`Registered ${type}: ${name || clazz.name}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the context for a given class or name.
|
|
83
|
+
* @param {string|Function} clazzOrName The class or name to look up
|
|
84
|
+
* @returns {InstanceContext}
|
|
85
|
+
* @throws {Error} If the context is not found
|
|
86
|
+
*/
|
|
87
|
+
getContext(clazzOrName) {
|
|
88
|
+
if (this.#instances.has(clazzOrName)) {
|
|
89
|
+
return this.#instances.get(clazzOrName)
|
|
90
|
+
}
|
|
91
|
+
const available = Array.from(this.#instances.keys())
|
|
92
|
+
.map(k => typeof k === 'string' ? k : k.name)
|
|
93
|
+
.join(', ')
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Cannot find injection source for "${clazzOrName?.name || clazzOrName}". ` +
|
|
96
|
+
`Available: [${available}]`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a class or name is registered.
|
|
102
|
+
* @param {string|Function} clazzOrName The class or name to check
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
has(clazzOrName) {
|
|
106
|
+
return this.#instances.has(clazzOrName)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get or create an instance based on the context.
|
|
111
|
+
* @param {InstanceContext} instanceContext The instance context
|
|
112
|
+
* @param {Array} params Constructor parameters
|
|
113
|
+
* @returns {Object} The instance
|
|
114
|
+
*/
|
|
115
|
+
getInstance(instanceContext, params) {
|
|
116
|
+
if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
|
|
117
|
+
this.#log(`Returning cached singleton: ${instanceContext.clazz.name}`)
|
|
118
|
+
return instanceContext.instance
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let instance
|
|
122
|
+
try {
|
|
123
|
+
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
124
|
+
instance = new instanceContext.clazz(...params)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof RangeError) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Circular dependency detected for ${instanceContext.clazz.name || instanceContext.clazz}. ` +
|
|
129
|
+
`Use @InjectLazy to break the cycle.`
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
throw err
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (instanceContext.proxy && instanceContext.originalClazz) {
|
|
136
|
+
const originalInstance = new instanceContext.originalClazz(...params)
|
|
137
|
+
instance = createProxy(instance, originalInstance)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (instanceContext.type === 'singleton') {
|
|
141
|
+
instanceContext.instance = instance
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return instance
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Register a mock for an existing class.
|
|
149
|
+
* @param {string|Function} targetClazzOrName The class or name to mock
|
|
150
|
+
* @param {Function} mockClazz The mock class
|
|
151
|
+
* @param {boolean} [useProxy=false] Whether to proxy unmocked methods to original
|
|
152
|
+
*/
|
|
153
|
+
registerMock(targetClazzOrName, mockClazz, useProxy = false) {
|
|
154
|
+
const instanceContext = this.getContext(targetClazzOrName)
|
|
155
|
+
if (instanceContext.originalClazz) {
|
|
156
|
+
throw new Error('Mock already defined, reset before mocking again')
|
|
157
|
+
}
|
|
158
|
+
instanceContext.originalClazz = instanceContext.clazz
|
|
159
|
+
instanceContext.proxy = useProxy
|
|
160
|
+
instanceContext.clazz = mockClazz
|
|
161
|
+
const targetName = typeof targetClazzOrName === 'string' ? targetClazzOrName : targetClazzOrName.name
|
|
162
|
+
this.#log(`Mocked ${targetName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reset a specific mock to its original class.
|
|
167
|
+
* @param {string|Function} clazzOrName The class or name to reset
|
|
168
|
+
* @throws {Error} If the class or name is not registered
|
|
169
|
+
*/
|
|
170
|
+
resetMock(clazzOrName) {
|
|
171
|
+
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Reset all mocks to their original classes.
|
|
176
|
+
*/
|
|
177
|
+
resetAllMocks() {
|
|
178
|
+
for (const instanceContext of this.#instances.values()) {
|
|
179
|
+
this.#restoreOriginal(instanceContext)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Clear all registered instances and mocks.
|
|
185
|
+
* Useful for test isolation.
|
|
186
|
+
*/
|
|
187
|
+
clear() {
|
|
188
|
+
this.#instances.clear()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Internal function to restore an instance context to its original.
|
|
193
|
+
* @param {InstanceContext} instanceContext The instance context to reset
|
|
194
|
+
* @param {string|Function} [clazzOrName] Optional identifier for error messages
|
|
195
|
+
* @throws {Error} If instanceContext is null or undefined
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
#restoreOriginal(instanceContext, clazzOrName) {
|
|
199
|
+
if (!instanceContext) {
|
|
200
|
+
const name = clazzOrName?.name || clazzOrName || 'unknown'
|
|
201
|
+
throw new Error(`Cannot reset mock for "${name}": not registered`)
|
|
202
|
+
}
|
|
203
|
+
if (instanceContext.originalClazz) {
|
|
204
|
+
instanceContext.clazz = instanceContext.originalClazz
|
|
205
|
+
delete instanceContext.instance
|
|
206
|
+
delete instanceContext.originalClazz
|
|
207
|
+
delete instanceContext.originalInstance
|
|
208
|
+
delete instanceContext.proxy
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
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
|
-
|
package/babel.config.json
DELETED
package/test/injection.test.js
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import {Factory, Inject, resetMocks, Singleton} from '../index.js'
|
|
2
|
-
|
|
3
|
-
describe('Injection via fields', () => {
|
|
4
|
-
@Singleton()
|
|
5
|
-
class TestSingleton {
|
|
6
|
-
static calls = 0
|
|
7
|
-
|
|
8
|
-
constructor() {
|
|
9
|
-
TestSingleton.calls++
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
it('should inject singleton', () => {
|
|
14
|
-
class TestInjection {
|
|
15
|
-
@Inject(TestSingleton) testSingleton
|
|
16
|
-
|
|
17
|
-
constructor() {
|
|
18
|
-
expect(this.testSingleton).toBeInstanceOf(TestSingleton)
|
|
19
|
-
expect(TestSingleton.calls).toBe(1)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
class TestInjection2 {
|
|
24
|
-
@Inject(TestSingleton) testSingleton
|
|
25
|
-
|
|
26
|
-
constructor() {
|
|
27
|
-
expect(this.testSingleton).toBeInstanceOf(TestSingleton)
|
|
28
|
-
expect(TestSingleton.calls).toBe(1)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
new TestInjection()
|
|
33
|
-
new TestInjection2()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
@Factory()
|
|
37
|
-
class TestFactory {
|
|
38
|
-
static calls = 0
|
|
39
|
-
params
|
|
40
|
-
|
|
41
|
-
@Inject(TestSingleton) testSingleton
|
|
42
|
-
|
|
43
|
-
constructor(...params) {
|
|
44
|
-
TestFactory.calls++
|
|
45
|
-
this.params = params
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
TestFactory.calls = 0
|
|
51
|
-
TestSingleton.calls = 0
|
|
52
|
-
resetMocks()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should inject factory', () => {
|
|
56
|
-
class TestInjectionFactory {
|
|
57
|
-
@Inject(TestFactory) testFactory
|
|
58
|
-
|
|
59
|
-
constructor() {
|
|
60
|
-
expect(this.testFactory).toBeInstanceOf(TestFactory)
|
|
61
|
-
expect(TestFactory.calls).toBe(1)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class TestInjectionFactory2 {
|
|
66
|
-
@Inject(TestFactory) testFactory
|
|
67
|
-
|
|
68
|
-
constructor() {
|
|
69
|
-
expect(this.testFactory).toBeInstanceOf(TestFactory)
|
|
70
|
-
expect(TestFactory.calls).toBe(2)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const result = new TestInjectionFactory()
|
|
75
|
-
new TestInjectionFactory2()
|
|
76
|
-
expect(result.testFactory.testSingleton).toBeInstanceOf(TestSingleton)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('should inject factory with parameters', () => {
|
|
80
|
-
class TestInjectionFactoryParams {
|
|
81
|
-
@Inject(TestFactory, 'param1', 'param2') testFactory
|
|
82
|
-
|
|
83
|
-
constructor() {
|
|
84
|
-
expect(this.testFactory).toBeInstanceOf(TestFactory)
|
|
85
|
-
expect(this.testFactory.params).toEqual(['param1', 'param2'])
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
new TestInjectionFactoryParams()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('should cache factory instance on repeated accesses', () => {
|
|
93
|
-
class TestRepeatedFactoryAccess {
|
|
94
|
-
@Inject(TestFactory) testFactory
|
|
95
|
-
|
|
96
|
-
constructor() {
|
|
97
|
-
const instance1 = this.testFactory
|
|
98
|
-
const instance2 = this.testFactory
|
|
99
|
-
expect(instance1).toBe(instance2)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
new TestRepeatedFactoryAccess()
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('should create distinct factory instances for different fields in the same object', () => {
|
|
107
|
-
class TestMultipleFactoryInjection {
|
|
108
|
-
@Inject(TestFactory) testFactory1
|
|
109
|
-
@Inject(TestFactory) testFactory2
|
|
110
|
-
|
|
111
|
-
constructor() {
|
|
112
|
-
// Access both properties to trigger initialization.
|
|
113
|
-
const one = this.testFactory1
|
|
114
|
-
const two = this.testFactory2
|
|
115
|
-
expect(one).not.toBe(two)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
new TestMultipleFactoryInjection()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('should inject the same singleton instance for different fields in the same object', () => {
|
|
123
|
-
class TestMultipleSingletonInjection {
|
|
124
|
-
@Inject(TestSingleton) testSingleton1
|
|
125
|
-
@Inject(TestSingleton) testSingleton2
|
|
126
|
-
|
|
127
|
-
constructor() {
|
|
128
|
-
// Access both properties to trigger initialization.
|
|
129
|
-
const one = this.testSingleton1
|
|
130
|
-
const two = this.testSingleton2
|
|
131
|
-
expect(one).toBe(two)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
new TestMultipleSingletonInjection()
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
@Singleton('named')
|
|
139
|
-
class NamedSingleton {
|
|
140
|
-
static calls = 0
|
|
141
|
-
|
|
142
|
-
constructor() {
|
|
143
|
-
NamedSingleton.calls++
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
it('should inject named singleton', () => {
|
|
148
|
-
class TestInjectionNamedSingleton {
|
|
149
|
-
@Inject('named') namedSingleton
|
|
150
|
-
|
|
151
|
-
constructor() {
|
|
152
|
-
expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
|
|
153
|
-
expect(NamedSingleton.calls).toBe(1)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
class TestInjectionNamedSingleton2 {
|
|
158
|
-
@Inject('named') namedSingleton
|
|
159
|
-
|
|
160
|
-
constructor() {
|
|
161
|
-
expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
|
|
162
|
-
expect(NamedSingleton.calls).toBe(1)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
new TestInjectionNamedSingleton()
|
|
167
|
-
new TestInjectionNamedSingleton2()
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
@Factory('named2')
|
|
171
|
-
class NamedFactory {
|
|
172
|
-
static calls = 0
|
|
173
|
-
params
|
|
174
|
-
|
|
175
|
-
constructor(...params) {
|
|
176
|
-
NamedFactory.calls++
|
|
177
|
-
this.params = params
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
it('should inject named factory', () => {
|
|
182
|
-
class TestInjectionNamedFactory {
|
|
183
|
-
@Inject('named2') namedFactory
|
|
184
|
-
|
|
185
|
-
constructor() {
|
|
186
|
-
expect(this.namedFactory).toBeInstanceOf(NamedFactory)
|
|
187
|
-
expect(NamedFactory.calls).toBe(1)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
class TestInjectionNamedFactory2 {
|
|
192
|
-
@Inject('named2') namedFactory
|
|
193
|
-
|
|
194
|
-
constructor() {
|
|
195
|
-
expect(this.namedFactory).toBeInstanceOf(NamedFactory)
|
|
196
|
-
expect(NamedFactory.calls).toBe(2)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const result = new TestInjectionNamedFactory()
|
|
201
|
-
new TestInjectionNamedFactory2()
|
|
202
|
-
expect(result.namedFactory.params).toEqual([])
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('should cache named factory instance on repeated accesses', () => {
|
|
206
|
-
class TestRepeatedNamedFactoryAccess {
|
|
207
|
-
@Inject('named2') namedFactory
|
|
208
|
-
|
|
209
|
-
constructor() {
|
|
210
|
-
const instance1 = this.namedFactory
|
|
211
|
-
const instance2 = this.namedFactory
|
|
212
|
-
expect(instance1).toBe(instance2)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
new TestRepeatedNamedFactoryAccess()
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('should throw if @Inject is applied to a method', () => {
|
|
220
|
-
expect(() => {
|
|
221
|
-
// noinspection JSUnusedLocalSymbols
|
|
222
|
-
class BadInjection {
|
|
223
|
-
@Inject('something')
|
|
224
|
-
someMethod() {
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}).toThrow()
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('should handle circular dependencies gracefully', () => {
|
|
231
|
-
@Singleton()
|
|
232
|
-
class A {
|
|
233
|
-
@Inject('B') b
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
@Singleton('B')
|
|
237
|
-
class B {
|
|
238
|
-
@Inject(A) a
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
expect(() => new A()).toThrow(/Circular dependency detected.*@InjectLazy/)
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('should throw if decorator is used on non-class object', () => {
|
|
245
|
-
expect(() => {
|
|
246
|
-
const obj = {}
|
|
247
|
-
Inject('something')(obj, 'field')
|
|
248
|
-
}).toThrow()
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('should throw a helpful error for eager circular dependencies', () => {
|
|
252
|
-
@Factory()
|
|
253
|
-
class A2 {
|
|
254
|
-
@Inject('B2') b
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
@Singleton('B2')
|
|
258
|
-
class B2 {
|
|
259
|
-
@Inject(A2) a
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
expect(() => new A2()).toThrow(/Circular dependency detected.*@InjectLazy/)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('should inject into symbol-named fields', () => {
|
|
266
|
-
const sym = Symbol('sym')
|
|
267
|
-
|
|
268
|
-
@Singleton()
|
|
269
|
-
class S {
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
class Test {
|
|
273
|
-
@Inject(S) [sym]
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const t = new Test()
|
|
277
|
-
expect(t[sym]).toBeInstanceOf(S)
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('should not leak injected properties to prototype', () => {
|
|
281
|
-
@Singleton()
|
|
282
|
-
class S {
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
class Test {
|
|
286
|
-
@Inject(S) dep
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// noinspection JSUnusedLocalSymbols
|
|
290
|
-
const t = new Test()
|
|
291
|
-
expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
it('should handle undefined/null/complex params in factory', () => {
|
|
295
|
-
@Factory()
|
|
296
|
-
class F {
|
|
297
|
-
constructor(...params) {
|
|
298
|
-
this.params = params
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
class Test {
|
|
303
|
-
@Inject(F, undefined, null, {a: 1}) dep
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const t = new Test()
|
|
307
|
-
expect(t.dep.params).toEqual([undefined, null, {a: 1}])
|
|
308
|
-
})
|
|
309
|
-
})
|