@joist/di 3.9.0 → 3.9.1
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 +121 -137
- package/package.json +13 -2
- package/src/lib/inject.ts +2 -4
- package/src/lib/injectable.test.ts +0 -16
- package/src/lib/injectable.ts +19 -24
- package/src/lib/injector.test-node.ts +187 -0
- package/src/lib/injector.test.ts +4 -4
- package/src/lib/injector.ts +3 -4
- package/src/lib/lifecycle.test.ts +1 -0
- package/src/lib/provider.ts +0 -4
- package/src/lib.ts +3 -3
- package/target/lib/inject.js +1 -1
- package/target/lib/inject.js.map +1 -1
- package/target/lib/injectable.d.ts +0 -2
- package/target/lib/injectable.js +15 -23
- package/target/lib/injectable.js.map +1 -1
- package/target/lib/injectable.test.js +0 -27
- package/target/lib/injectable.test.js.map +1 -1
- package/target/lib/injector.d.ts +2 -1
- package/target/lib/injector.js +2 -4
- package/target/lib/injector.js.map +1 -1
- package/target/lib/injector.test-node.d.ts +1 -0
- package/target/lib/injector.test-node.js +231 -0
- package/target/lib/injector.test-node.js.map +1 -0
- package/target/lib/injector.test.js +4 -4
- package/target/lib/injector.test.js.map +1 -1
- package/target/lib/lifecycle.test.js.map +1 -1
- package/target/lib/provider.d.ts +0 -1
- package/target/lib/provider.js +0 -3
- package/target/lib/provider.js.map +1 -1
- package/target/lib.d.ts +3 -3
- package/target/lib.js +3 -3
- package/target/lib.js.map +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Di
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Small and efficient dependency injection.
|
|
4
4
|
|
|
5
5
|
Allows you to inject services into other class instances (including custom elements and node).
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
9
|
- [Installation](#installation)
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
10
|
+
- [Injectors](#injectors)
|
|
11
|
+
- [Services](#services)
|
|
12
|
+
- [Injectable Services](#injectable-services)
|
|
13
|
+
- [Defining Providers](#defining-providers)
|
|
14
|
+
- [StaticTokens](#statictokens)
|
|
15
|
+
- [LifeCycle](#lifecycle)
|
|
16
|
+
- [Hierarchical Injectors](#hierarchical-injectors)
|
|
16
17
|
- [Custom Elements](#custom-elements)
|
|
17
|
-
- [Environment](#environment)
|
|
18
18
|
|
|
19
19
|
## Installation:
|
|
20
20
|
|
|
@@ -22,73 +22,122 @@ Allows you to inject services into other class instances (including custom eleme
|
|
|
22
22
|
npm i @joist/di
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Injectors
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Injectors are what are used to construct new [services](#services). Injectors can manually [provide implementations](#defining-providers) of services. Injectors can also have [parents](#hierarchical-injectors), parent injectors can define services for all of it's children.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
## Services
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
At their simplest, services are classses. Services can be constructed via an `Injector` and treated are singletons (The same instance is returned for each call to Injector.inject()).
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const app = new Injector();
|
|
33
35
|
|
|
34
|
-
class
|
|
35
|
-
|
|
36
|
+
class Counter {
|
|
37
|
+
value = 0;
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
inc(val: number) {
|
|
40
|
+
this.value += val;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// these two calls will return the same instance
|
|
45
|
+
const foo = app.inject(Counter);
|
|
46
|
+
const bar = app.inject(Counter);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Injectable Services
|
|
45
50
|
|
|
51
|
+
Singleton services are great but the real benefit can be seen when passing instances of one service to another. Services are injected into other services using the `inject()` fuction. In order to use `inject()` classes must be decorated with `@injectable`.
|
|
52
|
+
|
|
53
|
+
`inject()` returns a function that will then return an instance of the requested service. This means that services are only created when they are needed and not when the class is constructed.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
46
56
|
@injectable
|
|
47
|
-
class
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return this.engine().accelerate();
|
|
57
|
+
class App {
|
|
58
|
+
#counter = inject(Counter);
|
|
59
|
+
|
|
60
|
+
update(val: number) {
|
|
61
|
+
const instance = this.#counter();
|
|
62
|
+
|
|
63
|
+
instance.inc(val);
|
|
55
64
|
}
|
|
56
65
|
}
|
|
66
|
+
```
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
const car1 = factory1.get(Car);
|
|
68
|
+
## Defining Providers
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
console.log(car1.accelerate(), car1.tires().size);
|
|
70
|
+
A big reason to use dependency injection is the ability to provide multiple implementations for a particular service. For example we probably want a different http client when running unit tests vs in our main application.
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
provide: Engine,
|
|
67
|
-
use: class extends Engine {
|
|
68
|
-
type = 'electric';
|
|
72
|
+
In the below example we have a defined HttpService that wraps fetch. but for our unit test we will use a custom implementation that returns just the data we want. This also has the benefit of avoiding test framework specific mocks.
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
```ts
|
|
75
|
+
// services.ts
|
|
76
|
+
|
|
77
|
+
class HttpService {
|
|
78
|
+
fetch(url: string, init?: RequestInit) {
|
|
79
|
+
return fetch(url, init);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@injectable
|
|
84
|
+
class ApiService {
|
|
85
|
+
#http = inject(HttpService);
|
|
86
|
+
|
|
87
|
+
getData() {
|
|
88
|
+
return this.#http()
|
|
89
|
+
.fetch('/api/v1/users')
|
|
90
|
+
.then((res) => res.json());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// services.test.ts
|
|
97
|
+
|
|
98
|
+
test('should return json', async () => {
|
|
99
|
+
class MockHttpService extends HttpService {
|
|
100
|
+
async fetch() {
|
|
101
|
+
return Response.json({ fname: 'Danny', lname: 'Blue' });
|
|
79
102
|
}
|
|
80
103
|
}
|
|
81
|
-
]);
|
|
82
104
|
|
|
83
|
-
const
|
|
105
|
+
const app = new Injector([{ provide: HttpService, use: MockHttpService }]);
|
|
106
|
+
const api = app.inject(ApiService);
|
|
107
|
+
|
|
108
|
+
const res = await api.getData();
|
|
109
|
+
|
|
110
|
+
assert.equals(res.fname, 'Danny');
|
|
111
|
+
assert.equals(res.lname, 'Blue');
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Service level providers
|
|
116
|
+
|
|
117
|
+
Under the hood, each service decorated with `@injectable` creates its own injector. This means that it is possible to defined providers from that level down.
|
|
118
|
+
|
|
119
|
+
The below example will use this particular instance of Logger as wall as any other services injected into this service.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
class Logger {
|
|
123
|
+
log(..._: any[]): void {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class ConsoleLogger implements Logger {
|
|
127
|
+
log(...args: any[]) {
|
|
128
|
+
console.log(...args);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
84
131
|
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
@injectable
|
|
133
|
+
class MyService {
|
|
134
|
+
static providers = [{ provide: Logger, use: ConsoleLogger }];
|
|
135
|
+
}
|
|
87
136
|
```
|
|
88
137
|
|
|
89
|
-
|
|
138
|
+
### Factories
|
|
90
139
|
|
|
91
|
-
In addition to defining providers with classes you can also use factory functions.
|
|
140
|
+
In addition to defining providers with classes you can also use factory functions. Factories allow for more flexibility for deciding exactly how a service is created. This is helpful when which instance that is provided depends on some runtime value.
|
|
92
141
|
|
|
93
142
|
```ts
|
|
94
143
|
class Logger {
|
|
@@ -99,15 +148,21 @@ const app = new Injector([
|
|
|
99
148
|
{
|
|
100
149
|
provide: Logger,
|
|
101
150
|
factory() {
|
|
102
|
-
|
|
151
|
+
const params = new URLSearchParams(window.location.search);
|
|
152
|
+
|
|
153
|
+
if (params.has('debug')) {
|
|
154
|
+
return console;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return new Logger(); // noop logger
|
|
103
158
|
}
|
|
104
159
|
}
|
|
105
160
|
]);
|
|
106
161
|
```
|
|
107
162
|
|
|
108
|
-
### Accessing the injector
|
|
163
|
+
### Accessing the injector in factories
|
|
109
164
|
|
|
110
|
-
Factories provide more flexibility but
|
|
165
|
+
Factories provide more flexibility but sometimes will require access to the injector itself. For this reason the factory method is passed the injector that is being used to construct the requested service.
|
|
111
166
|
|
|
112
167
|
```ts
|
|
113
168
|
class Logger {
|
|
@@ -128,13 +183,15 @@ const app = new Injector([
|
|
|
128
183
|
{
|
|
129
184
|
provide: Feature,
|
|
130
185
|
factory(i) {
|
|
131
|
-
|
|
186
|
+
const logger = i.inject(Logger);
|
|
187
|
+
|
|
188
|
+
return new Feature(logger);
|
|
132
189
|
}
|
|
133
190
|
}
|
|
134
191
|
]);
|
|
135
192
|
```
|
|
136
193
|
|
|
137
|
-
##
|
|
194
|
+
## StaticTokens
|
|
138
195
|
|
|
139
196
|
In most cases a token is any constructable class. There are cases where you might want to return other data types that aren't objects.
|
|
140
197
|
|
|
@@ -164,58 +221,16 @@ Static tokens can also leverage promises for cases when you need to async create
|
|
|
164
221
|
|
|
165
222
|
```ts
|
|
166
223
|
// StaticToken<Promise<string>>
|
|
167
|
-
const URL_TOKEN = new StaticToken('app_url', () =>
|
|
224
|
+
const URL_TOKEN = new StaticToken('app_url', async () => '/default-url/');
|
|
168
225
|
|
|
169
226
|
const app = new Injector();
|
|
170
227
|
|
|
171
|
-
const url = await app.
|
|
228
|
+
const url: string = await app.inject(URL_TOKEN);
|
|
172
229
|
```
|
|
173
230
|
|
|
174
|
-
##
|
|
231
|
+
## LifeCycle
|
|
175
232
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
```TS
|
|
179
|
-
import { Injector, injectable, inject } from '@joist/di';
|
|
180
|
-
|
|
181
|
-
@injectable
|
|
182
|
-
class HttpService {
|
|
183
|
-
fetch(url: string, init?: RequestInit) {
|
|
184
|
-
return fetch(url, init);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
@injectable
|
|
189
|
-
class ApiService {
|
|
190
|
-
#http = inject(HttpService);
|
|
191
|
-
|
|
192
|
-
getData() {
|
|
193
|
-
return this.#http()
|
|
194
|
-
.fetch('/api/v1/users')
|
|
195
|
-
.then((res) => res.json());
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// unit test
|
|
200
|
-
const testApp = new Injector([
|
|
201
|
-
{
|
|
202
|
-
provide: HttpService,
|
|
203
|
-
use: class extends HttpService {
|
|
204
|
-
async fetch() {
|
|
205
|
-
// return whatever response we like
|
|
206
|
-
return Response.json({ fname: 'Danny', lname: 'Blue' });
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
]);
|
|
211
|
-
|
|
212
|
-
// our test instance will be using our mock when making http requests
|
|
213
|
-
const api = testApp.get(ApiService);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Life Cycle
|
|
217
|
-
|
|
218
|
-
To helo provide more information to services that are being created, joist will call several life cycle hooks as services are created. These hooks are defined using the provided symbols so there is no risk of naming colisions.
|
|
233
|
+
To help provide more information to services that are being created, joist will call several life cycle hooks as services are created. These hooks are defined using the provided symbols so there is no risk of naming colisions.
|
|
219
234
|
|
|
220
235
|
```ts
|
|
221
236
|
class MyService {
|
|
@@ -229,7 +244,7 @@ class MyService {
|
|
|
229
244
|
}
|
|
230
245
|
```
|
|
231
246
|
|
|
232
|
-
##
|
|
247
|
+
## Hierarchical Injectors
|
|
233
248
|
|
|
234
249
|
Injectors can be defined with a parent element. The top most parent will (by default) be where services are constructed and cached. Only if manually defined providers are found earlier in the chain will services be constructed lower. The injector resolution algorithm behaves as following.
|
|
235
250
|
|
|
@@ -257,11 +272,9 @@ This behavior allows for services to be "scoped" within a certain branch of the
|
|
|
257
272
|
|
|
258
273
|
## Custom Elements:
|
|
259
274
|
|
|
260
|
-
Joist is built to work with custom elements. Since the document is a tree we can search up that tree for providers.
|
|
275
|
+
Joist is built to work with custom elements. Since the document is a tree we can search up that tree for providers. This is where Hierarchical Injectors can really shine as they allow you to defined React/Preact esq "context" elements.
|
|
261
276
|
|
|
262
277
|
```TS
|
|
263
|
-
import { injectable, inject } from '@joist/di';
|
|
264
|
-
|
|
265
278
|
class Colors {
|
|
266
279
|
primary = 'red';
|
|
267
280
|
secodnary = 'green';
|
|
@@ -307,7 +320,7 @@ customElements.define('my-element', MyElement);
|
|
|
307
320
|
</color-ctx>
|
|
308
321
|
```
|
|
309
322
|
|
|
310
|
-
|
|
323
|
+
### Environment
|
|
311
324
|
|
|
312
325
|
When using @joist/di with custom elements a default root injector is created dubbed 'environment'. This is the injector that all other injectors will eventually stop at.
|
|
313
326
|
If you need to define something in this environment you can do so with the `defineEnvironment` method.
|
|
@@ -317,32 +330,3 @@ import { defineEnvironment } from '@joist/di';
|
|
|
317
330
|
|
|
318
331
|
defineEnvironment([{ provide: MyService, use: SomeOtherService }]);
|
|
319
332
|
```
|
|
320
|
-
|
|
321
|
-
#### No decorators no problem:
|
|
322
|
-
|
|
323
|
-
While this library is built with decorators in mind it is designed so that it can be used without them.
|
|
324
|
-
|
|
325
|
-
```TS
|
|
326
|
-
import { Injector, injectable, inject } from '@joist/di';
|
|
327
|
-
|
|
328
|
-
class Engine {
|
|
329
|
-
type: 'gas' | 'electric' = 'gas';
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
class Tires {
|
|
333
|
-
size = 16;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const Car = injectable(
|
|
337
|
-
class {
|
|
338
|
-
engine = inject(Engine);
|
|
339
|
-
tires = inject(Tires);
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
const app = new Injector();
|
|
344
|
-
const car = app.get(Car);
|
|
345
|
-
|
|
346
|
-
// gas, 16
|
|
347
|
-
console.log(car.engine(), car.tires());
|
|
348
|
-
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joist/di",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./target/lib.js",
|
|
6
6
|
"module": "./target/lib.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"import": "./target/lib.js"
|
|
10
10
|
},
|
|
11
11
|
"./*": {
|
|
12
|
-
"import": "./target/lib
|
|
12
|
+
"import": "./target/lib/*"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
@@ -60,6 +60,17 @@
|
|
|
60
60
|
"target/**"
|
|
61
61
|
],
|
|
62
62
|
"output": [],
|
|
63
|
+
"dependencies": [
|
|
64
|
+
"build",
|
|
65
|
+
"test:node"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"test:node": {
|
|
69
|
+
"command": "node --test target/**/*.test-node.js",
|
|
70
|
+
"files": [
|
|
71
|
+
"target/**"
|
|
72
|
+
],
|
|
73
|
+
"output": [],
|
|
63
74
|
"dependencies": [
|
|
64
75
|
"build"
|
|
65
76
|
]
|
package/src/lib/inject.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { INJECTABLE_MAP } from './injector.js';
|
|
1
2
|
import { InjectionToken } from './provider.js';
|
|
2
|
-
import { INJECTABLE_MAP } from './injectable.js';
|
|
3
3
|
|
|
4
4
|
export type Injected<T> = () => T;
|
|
5
5
|
|
|
6
|
-
export function inject<This extends object, T>(
|
|
7
|
-
token: InjectionToken<T>
|
|
8
|
-
): Injected<T> {
|
|
6
|
+
export function inject<This extends object, T>(token: InjectionToken<T>): Injected<T> {
|
|
9
7
|
return function (this: This) {
|
|
10
8
|
const injector = INJECTABLE_MAP.get(this);
|
|
11
9
|
|
|
@@ -2,7 +2,6 @@ import { expect, fixture, html } from '@open-wc/testing';
|
|
|
2
2
|
|
|
3
3
|
import { injectable } from './injectable.js';
|
|
4
4
|
import { inject } from './inject.js';
|
|
5
|
-
import { Injector } from './injector.js';
|
|
6
5
|
|
|
7
6
|
describe('@injectable()', () => {
|
|
8
7
|
it('should allow a custom element to be injected with deps', () => {
|
|
@@ -40,21 +39,6 @@ describe('@injectable()', () => {
|
|
|
40
39
|
expect(el.foo()).to.be.instanceOf(Bar);
|
|
41
40
|
});
|
|
42
41
|
|
|
43
|
-
it('should call the onInject lifecycle hook', () => {
|
|
44
|
-
class A {}
|
|
45
|
-
|
|
46
|
-
@injectable
|
|
47
|
-
class B {
|
|
48
|
-
a = inject(A);
|
|
49
|
-
|
|
50
|
-
onInject() {
|
|
51
|
-
expect(this.a()).to.be.instanceOf(A);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
new Injector().inject(B);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
42
|
it('should handle parent HTML Injectors', async () => {
|
|
59
43
|
@injectable
|
|
60
44
|
class A {}
|
package/src/lib/injectable.ts
CHANGED
|
@@ -1,44 +1,39 @@
|
|
|
1
1
|
import { ConstructableToken } from './provider.js';
|
|
2
|
-
import { Injector } from './injector.js';
|
|
2
|
+
import { INJECTABLE_MAP, Injector } from './injector.js';
|
|
3
3
|
import { environment } from './environment.js';
|
|
4
|
-
import { InjectableMap } from './injectable-map.js';
|
|
5
|
-
|
|
6
|
-
export const INJECTABLE_MAP = new InjectableMap();
|
|
7
4
|
|
|
8
5
|
export function injectable<T extends ConstructableToken<any>>(Base: T, _?: unknown) {
|
|
9
6
|
return class InjectableNode extends Base {
|
|
10
7
|
constructor(..._: any[]) {
|
|
11
8
|
super();
|
|
12
9
|
|
|
10
|
+
// Define a new Injector and assiciate it with this instance of the service
|
|
13
11
|
const injector = new Injector(Base.providers);
|
|
14
|
-
|
|
15
12
|
INJECTABLE_MAP.set(this, injector);
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
// If the current injectable instance is a HTMLElement preform additional startup logic
|
|
15
|
+
// this will find and attach parent injectors
|
|
16
|
+
if ('HTMLElement' in globalThis && this instanceof HTMLElement) {
|
|
17
|
+
this.addEventListener('finddiroot', (e) => {
|
|
18
|
+
const parentInjector = findInjectorRoot(e);
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} catch {}
|
|
20
|
+
if (parentInjector !== null) {
|
|
21
|
+
injector.setParent(parentInjector);
|
|
22
|
+
} else {
|
|
23
|
+
injector.setParent(environment());
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
connectedCallback() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.dispatchEvent(new Event('finddiroot'));
|
|
30
|
+
if ('HTMLElement' in globalThis && this instanceof HTMLElement) {
|
|
31
|
+
this.dispatchEvent(new Event('finddiroot'));
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
33
|
+
if (super.connectedCallback) {
|
|
34
|
+
super.connectedCallback();
|
|
40
35
|
}
|
|
41
|
-
}
|
|
36
|
+
}
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
disconnectedCallback() {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import { Injector } from './injector.js';
|
|
5
|
+
import { inject } from './inject.js';
|
|
6
|
+
import { injectable } from './injectable.js';
|
|
7
|
+
import { Provider, StaticToken } from './provider.js';
|
|
8
|
+
|
|
9
|
+
test('should create a new instance of a single provider', () => {
|
|
10
|
+
class A {}
|
|
11
|
+
|
|
12
|
+
const app = new Injector();
|
|
13
|
+
|
|
14
|
+
assert(app.inject(A) instanceof A);
|
|
15
|
+
|
|
16
|
+
assert.equal(app.inject(A), app.inject(A));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should inject providers in the correct order', () => {
|
|
20
|
+
class A {}
|
|
21
|
+
class B {}
|
|
22
|
+
|
|
23
|
+
@injectable
|
|
24
|
+
class MyService {
|
|
25
|
+
a = inject(A);
|
|
26
|
+
b = inject(B);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const app = new Injector();
|
|
30
|
+
const instance = app.inject(MyService);
|
|
31
|
+
|
|
32
|
+
assert(instance.a() instanceof A);
|
|
33
|
+
assert(instance.b() instanceof B);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should create a new instance of a provider that has a full dep tree', () => {
|
|
37
|
+
class A {}
|
|
38
|
+
|
|
39
|
+
@injectable
|
|
40
|
+
class B {
|
|
41
|
+
a = inject(A);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@injectable
|
|
45
|
+
class C {
|
|
46
|
+
b = inject(B);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@injectable
|
|
50
|
+
class D {
|
|
51
|
+
c = inject(C);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@injectable
|
|
55
|
+
class E {
|
|
56
|
+
d = inject(D);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const app = new Injector();
|
|
60
|
+
const instance = app.inject(E);
|
|
61
|
+
|
|
62
|
+
assert(instance.d().c().b().a() instanceof A);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should override a provider if explicitly instructed', () => {
|
|
66
|
+
class A {}
|
|
67
|
+
|
|
68
|
+
@injectable
|
|
69
|
+
class B {
|
|
70
|
+
a = inject(A);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class AltA extends A {}
|
|
74
|
+
const app = new Injector([{ provide: A, use: AltA }]);
|
|
75
|
+
|
|
76
|
+
assert(app.inject(B).a() instanceof AltA);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should return an existing instance from a parent injector', () => {
|
|
80
|
+
class A {}
|
|
81
|
+
|
|
82
|
+
const parent = new Injector();
|
|
83
|
+
|
|
84
|
+
const app = new Injector([], parent);
|
|
85
|
+
|
|
86
|
+
assert.equal(parent.inject(A), app.inject(A));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should use a factory if provided', () => {
|
|
90
|
+
class Service {
|
|
91
|
+
hello() {
|
|
92
|
+
return 'world';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const injector = new Injector([
|
|
97
|
+
{
|
|
98
|
+
provide: Service,
|
|
99
|
+
factory() {
|
|
100
|
+
return {
|
|
101
|
+
hello() {
|
|
102
|
+
return 'world';
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
assert.equal(injector.inject(Service).hello(), 'world');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should throw an error if provider is missing both factory and use', () => {
|
|
113
|
+
class Service {
|
|
114
|
+
hello() {
|
|
115
|
+
return 'world';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const injector = new Injector([
|
|
120
|
+
{
|
|
121
|
+
provide: Service
|
|
122
|
+
}
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
assert.throws(
|
|
126
|
+
() => injector.inject(Service),
|
|
127
|
+
new Error("Provider for Service found but is missing either 'use' or 'factory'")
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should pass factories and instance of the injector', async () => {
|
|
132
|
+
class Service {
|
|
133
|
+
hello() {
|
|
134
|
+
return 'world';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let factoryInjector: Injector | null = null;
|
|
139
|
+
|
|
140
|
+
const injector = new Injector([
|
|
141
|
+
{
|
|
142
|
+
provide: Service,
|
|
143
|
+
factory(i) {
|
|
144
|
+
factoryInjector = i;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
injector.inject(Service);
|
|
150
|
+
|
|
151
|
+
assert.equal(factoryInjector, injector);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should create an instance from a StaticToken factory', () => {
|
|
155
|
+
const TOKEN = new StaticToken('test', () => 'Hello World');
|
|
156
|
+
const injector = new Injector();
|
|
157
|
+
|
|
158
|
+
const res = injector.inject(TOKEN);
|
|
159
|
+
|
|
160
|
+
assert.equal(res, 'Hello World');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should create an instance from an async StaticToken factory', async () => {
|
|
164
|
+
const TOKEN = new StaticToken('test', () => Promise.resolve('Hello World'));
|
|
165
|
+
const injector = new Injector();
|
|
166
|
+
|
|
167
|
+
const res = await injector.inject(TOKEN);
|
|
168
|
+
|
|
169
|
+
assert.equal(res, 'Hello World');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should allow static token to be overridden', () => {
|
|
173
|
+
const TOKEN = new StaticToken<string>('test');
|
|
174
|
+
|
|
175
|
+
const provider: Provider<string> = {
|
|
176
|
+
provide: TOKEN,
|
|
177
|
+
factory() {
|
|
178
|
+
return 'Hello World';
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const injector = new Injector([provider]);
|
|
183
|
+
|
|
184
|
+
const res = injector.inject(TOKEN);
|
|
185
|
+
|
|
186
|
+
assert.equal(res, 'Hello World');
|
|
187
|
+
});
|