@joist/di 4.0.0-next.1 → 4.0.0-next.11

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.
Files changed (66) hide show
  1. package/README.md +52 -34
  2. package/package.json +4 -14
  3. package/src/lib/dom-injector.test.ts +16 -18
  4. package/src/lib/dom-injector.ts +3 -4
  5. package/src/lib/inject.test.ts +33 -56
  6. package/src/lib/inject.ts +4 -6
  7. package/src/lib/injectable-el.test.ts +130 -0
  8. package/src/lib/injectable-el.ts +63 -0
  9. package/src/lib/injectable.test.ts +19 -116
  10. package/src/lib/injectable.ts +21 -57
  11. package/src/lib/injector.test.ts +132 -130
  12. package/src/lib/injector.ts +33 -19
  13. package/src/lib/lifecycle.test.ts +68 -64
  14. package/src/lib/lifecycle.ts +19 -4
  15. package/src/lib/metadata.ts +12 -0
  16. package/src/lib/provider.ts +16 -8
  17. package/src/lib.ts +1 -1
  18. package/target/lib/dom-injector.js +3 -4
  19. package/target/lib/dom-injector.js.map +1 -1
  20. package/target/lib/dom-injector.test.js +16 -18
  21. package/target/lib/dom-injector.test.js.map +1 -1
  22. package/target/lib/inject.js +3 -3
  23. package/target/lib/inject.js.map +1 -1
  24. package/target/lib/inject.test.js +58 -90
  25. package/target/lib/inject.test.js.map +1 -1
  26. package/target/lib/injectable-el.d.ts +334 -0
  27. package/target/lib/injectable-el.js +40 -0
  28. package/target/lib/injectable-el.js.map +1 -0
  29. package/target/lib/injectable-el.test.js +238 -0
  30. package/target/lib/injectable-el.test.js.map +1 -0
  31. package/target/lib/injectable.d.ts +5 -7
  32. package/target/lib/injectable.js +13 -42
  33. package/target/lib/injectable.js.map +1 -1
  34. package/target/lib/injectable.test.js +49 -204
  35. package/target/lib/injectable.test.js.map +1 -1
  36. package/target/lib/injector.d.ts +1 -1
  37. package/target/lib/injector.js +18 -14
  38. package/target/lib/injector.js.map +1 -1
  39. package/target/lib/injector.test.js +215 -216
  40. package/target/lib/injector.test.js.map +1 -1
  41. package/target/lib/lifecycle.d.ts +2 -4
  42. package/target/lib/lifecycle.js +15 -4
  43. package/target/lib/lifecycle.js.map +1 -1
  44. package/target/lib/lifecycle.test.js +142 -123
  45. package/target/lib/lifecycle.test.js.map +1 -1
  46. package/target/lib/metadata.d.ts +6 -0
  47. package/target/lib/metadata.js +5 -0
  48. package/target/lib/metadata.js.map +1 -0
  49. package/target/lib/provider.d.ts +6 -5
  50. package/target/lib/provider.js +10 -4
  51. package/target/lib/provider.js.map +1 -1
  52. package/target/lib.d.ts +1 -1
  53. package/target/lib.js +1 -1
  54. package/target/lib.js.map +1 -1
  55. package/src/lib/injectable-map.test.ts +0 -18
  56. package/src/lib/injectable-map.ts +0 -3
  57. package/src/lib/injector.test-node.ts +0 -187
  58. package/target/lib/injectable-map.d.ts +0 -3
  59. package/target/lib/injectable-map.js +0 -3
  60. package/target/lib/injectable-map.js.map +0 -1
  61. package/target/lib/injectable-map.test.js +0 -15
  62. package/target/lib/injectable-map.test.js.map +0 -1
  63. package/target/lib/injector.test-node.d.ts +0 -1
  64. package/target/lib/injector.test-node.js +0 -231
  65. package/target/lib/injector.test-node.js.map +0 -1
  66. /package/target/lib/{injectable-map.test.d.ts → injectable-el.test.d.ts} +0 -0
@@ -1,128 +1,31 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { assert } from 'chai';
2
2
 
3
3
  import { injectable } from './injectable.js';
4
4
  import { inject } from './inject.js';
5
+ import { injectables } from './injector.js';
5
6
 
6
- describe('@injectable()', () => {
7
- it('should allow a custom element to be injected with deps', () => {
8
- class Foo {}
9
- class Bar {}
7
+ it('should locally override a provider', () => {
8
+ class Foo {}
10
9
 
11
- @injectable
12
- class MyElement extends HTMLElement {
13
- foo = inject(Foo);
14
- bar = inject(Bar);
15
- }
10
+ class Bar extends Foo {}
16
11
 
17
- customElements.define('injectable-1', MyElement);
12
+ @injectable({
13
+ providers: [{ provide: Foo, use: Bar }]
14
+ })
15
+ class MyService {
16
+ foo = inject(Foo);
17
+ }
18
18
 
19
- const el = document.createElement('injectable-1') as MyElement;
19
+ const el = new MyService();
20
20
 
21
- expect(el.foo()).to.be.instanceOf(Foo);
22
- });
23
-
24
- it('should locally override a provider', () => {
25
- class Foo {}
26
-
27
- class Bar extends Foo {}
28
-
29
- const MyElement = injectable(
30
- class {
31
- static providers = [{ provide: Foo, use: Bar }];
32
-
33
- foo = inject(Foo);
34
- }
35
- );
36
-
37
- const el = new MyElement();
38
-
39
- expect(el.foo()).to.be.instanceOf(Bar);
40
- });
41
-
42
- it('should handle parent HTML Injectors', async () => {
43
- @injectable
44
- class A {}
45
-
46
- const B = injectable(
47
- class {
48
- a = inject(A);
49
- }
50
- );
51
-
52
- class AltA implements A {}
53
-
54
- @injectable
55
- class Parent extends HTMLElement {
56
- static providers = [
57
- { provide: B, use: B },
58
- { provide: A, use: AltA }
59
- ];
60
- }
61
-
62
- @injectable
63
- class Child extends HTMLElement {
64
- b = inject(B);
65
- }
66
-
67
- customElements.define('injectable-parent-1', Parent);
68
- customElements.define('injectable-child-1', Child);
69
-
70
- const el = await fixture(html`
71
- <injectable-parent-1>
72
- <injectable-child-1></injectable-child-1>
73
- </injectable-parent-1>
74
- `);
75
-
76
- const child = el.querySelector<Child>('injectable-child-1')!;
77
-
78
- expect(child.b().a()).to.be.instanceOf(AltA);
79
- });
80
-
81
- it('should handle changing contexts', async () => {
82
- class A {}
83
- class AltA implements A {}
84
-
85
- @injectable
86
- class Ctx1 extends HTMLElement {
87
- static providers = [{ provide: A, use: A }];
88
- }
89
-
90
- @injectable
91
- class Ctx2 extends HTMLElement {
92
- static providers = [{ provide: A, use: AltA }];
93
- }
94
-
95
- @injectable
96
- class Child extends HTMLElement {
97
- a = inject(A);
98
- }
99
-
100
- customElements.define('ctx-1', Ctx1);
101
- customElements.define('ctx-2', Ctx2);
102
- customElements.define('ctx-child', Child);
103
-
104
- const el = await fixture(html`
105
- <div>
106
- <ctx-1>
107
- <ctx-child></ctx-child>
108
- </ctx-1>
109
-
110
- <ctx-2></ctx-2>
111
- </div>
112
- `);
113
-
114
- const ctx2 = el.querySelector('ctx-2')!;
115
-
116
- let child = el.querySelector<Child>('ctx-child')!;
117
-
118
- expect(child.a()).to.be.instanceOf(A);
119
-
120
- child.remove();
21
+ assert.instanceOf(el.foo(), Bar);
22
+ });
121
23
 
122
- ctx2.append(child);
24
+ it('should define an injector for a service instance', () => {
25
+ @injectable()
26
+ class MyService {}
123
27
 
124
- child = el.querySelector<Child>('ctx-child')!;
28
+ const instance = new MyService();
125
29
 
126
- expect(child.a()).to.be.instanceOf(AltA);
127
- });
30
+ assert.ok(injectables.has(instance));
128
31
  });
@@ -1,69 +1,33 @@
1
- import { ConstructableToken } from './provider.js';
2
- import { Injector } from './injector.js';
3
- import { InjectableMap } from './injectable-map.js';
1
+ (Symbol as any).metadata ??= Symbol('Symbol.metadata');
4
2
 
5
- export const INJECTABLE_MAP = new InjectableMap();
3
+ import { ConstructableToken, Provider } from './provider.js';
4
+ import { injectables, Injector } from './injector.js';
5
+ import { injectableEl } from './injectable-el.js';
6
6
 
7
- export function injectable<T extends ConstructableToken<any>>(Base: T, _?: unknown) {
8
- return class InjectableNode extends Base {
9
- constructor(..._: any[]) {
10
- super();
11
-
12
- // Define a new Injector and assiciate it with this instance of the service
13
- const injector = new Injector(Base.providers);
14
- INJECTABLE_MAP.set(this, injector);
7
+ export interface InjectableOpts {
8
+ providers?: Provider<unknown>[];
9
+ }
15
10
 
16
- // If the current injectable instance is a HTMLElement preform additional startup logic
17
- // this will find and attach parent injectors
18
- if ('HTMLElement' in globalThis && this instanceof HTMLElement) {
19
- this.addEventListener('finddiroot', (e) => {
20
- const parentInjector = findInjectorRoot(e);
11
+ export function injectable(opts?: InjectableOpts) {
12
+ return function injectableDecorator<T extends ConstructableToken<any>>(
13
+ Base: T,
14
+ ctx: ClassDecoratorContext
15
+ ) {
16
+ class Injectable extends Base {
17
+ constructor(..._: any[]) {
18
+ super();
21
19
 
22
- if (parentInjector !== null) {
23
- injector.setParent(parentInjector);
24
- }
25
- });
20
+ injectables.set(this, new Injector(opts?.providers));
26
21
  }
27
22
  }
28
23
 
29
- connectedCallback() {
30
- if ('HTMLElement' in globalThis && this instanceof HTMLElement) {
31
- this.dispatchEvent(new Event('finddiroot'));
32
-
33
- if (super.connectedCallback) {
34
- super.connectedCallback();
35
- }
24
+ // Only apply custom element bootstrap logic if the decorated class is an HTMLElement
25
+ if ('HTMLElement' in globalThis) {
26
+ if (HTMLElement.prototype.isPrototypeOf(Base.prototype)) {
27
+ return injectableEl(Injectable, ctx);
36
28
  }
37
29
  }
38
30
 
39
- disconnectedCallback() {
40
- const injector = INJECTABLE_MAP.get(this);
41
-
42
- if (injector) {
43
- injector.setParent(undefined);
44
- }
45
-
46
- if (super.disconnectedCallback) {
47
- super.disconnectedCallback();
48
- }
49
- }
31
+ return Injectable;
50
32
  };
51
33
  }
52
-
53
- function findInjectorRoot(e: Event): Injector | null {
54
- const path = e.composedPath();
55
-
56
- // find firt parent
57
- // skips the first item which is the target
58
- for (let i = 1; i < path.length; i++) {
59
- const part = path[i];
60
-
61
- const injector = INJECTABLE_MAP.get(part);
62
-
63
- if (injector) {
64
- return injector;
65
- }
66
- }
67
-
68
- return null;
69
- }
@@ -1,184 +1,186 @@
1
- import { expect } from '@open-wc/testing';
1
+ import { assert } from 'chai';
2
2
 
3
3
  import { Injector } from './injector.js';
4
4
  import { inject } from './inject.js';
5
5
  import { injectable } from './injectable.js';
6
6
  import { Provider, StaticToken } from './provider.js';
7
7
 
8
- describe('Injector', () => {
9
- it('should create a new instance of a single provider', () => {
10
- class A {}
8
+ it('should create a new instance of a single provider', () => {
9
+ class A {}
11
10
 
12
- const app = new Injector();
11
+ const app = new Injector();
13
12
 
14
- expect(app.inject(A)).to.be.instanceOf(A);
15
- expect(app.inject(A)).to.equal(app.inject(A));
16
- });
13
+ assert(app.inject(A) instanceof A);
17
14
 
18
- it('should inject providers in the correct order', () => {
19
- class A {}
20
- class B {}
15
+ assert.equal(app.inject(A), app.inject(A));
16
+ });
21
17
 
22
- @injectable
23
- class MyService {
24
- a = inject(A);
25
- b = inject(B);
26
- }
18
+ it('should inject providers in the correct order', () => {
19
+ class A {}
20
+ class B {}
27
21
 
28
- const app = new Injector();
29
- const instance = app.inject(MyService);
22
+ @injectable()
23
+ class MyService {
24
+ a = inject(A);
25
+ b = inject(B);
26
+ }
30
27
 
31
- expect(instance.a()).to.be.instanceOf(A);
32
- expect(instance.b()).to.be.instanceOf(B);
33
- });
28
+ const app = new Injector();
29
+ const instance = app.inject(MyService);
34
30
 
35
- it('should create a new instance of a provider that has a full dep tree', () => {
36
- class A {}
31
+ assert(instance.a() instanceof A);
32
+ assert(instance.b() instanceof B);
33
+ });
37
34
 
38
- @injectable
39
- class B {
40
- a = inject(A);
41
- }
35
+ it('should create a new instance of a provider that has a full dep tree', () => {
36
+ class A {}
42
37
 
43
- @injectable
44
- class C {
45
- b = inject(B);
46
- }
38
+ @injectable()
39
+ class B {
40
+ a = inject(A);
41
+ }
47
42
 
48
- @injectable
49
- class D {
50
- c = inject(C);
51
- }
43
+ @injectable()
44
+ class C {
45
+ b = inject(B);
46
+ }
52
47
 
53
- @injectable
54
- class E {
55
- d = inject(D);
56
- }
48
+ @injectable()
49
+ class D {
50
+ c = inject(C);
51
+ }
57
52
 
58
- const app = new Injector();
59
- const instance = app.inject(E);
53
+ @injectable()
54
+ class E {
55
+ d = inject(D);
56
+ }
60
57
 
61
- expect(instance.d().c().b().a()).to.be.instanceOf(A);
62
- });
58
+ const app = new Injector();
59
+ const instance = app.inject(E);
63
60
 
64
- it('should override a provider if explicitly instructed', () => {
65
- class A {}
61
+ assert(instance.d().c().b().a() instanceof A);
62
+ });
66
63
 
67
- @injectable
68
- class B {
69
- a = inject(A);
70
- }
64
+ it('should override a provider if explicitly instructed', () => {
65
+ class A {}
71
66
 
72
- class AltA extends A {}
73
- const app = new Injector([{ provide: A, use: AltA }]);
67
+ @injectable()
68
+ class B {
69
+ a = inject(A);
70
+ }
74
71
 
75
- expect(app.inject(B).a()).to.be.instanceOf(AltA);
76
- });
72
+ class AltA extends A {}
73
+ const app = new Injector([{ provide: A, use: AltA }]);
77
74
 
78
- it('should return an existing instance from a parent injector', () => {
79
- class A {}
75
+ assert(app.inject(B).a() instanceof AltA);
76
+ });
80
77
 
81
- const parent = new Injector();
78
+ it('should return an existing instance from a parent injector', () => {
79
+ class A {}
82
80
 
83
- const app = new Injector([], parent);
81
+ const parent = new Injector();
84
82
 
85
- expect(parent.inject(A)).to.equal(app.inject(A));
86
- });
83
+ const app = new Injector([], parent);
87
84
 
88
- it('should use a factory if provided', () => {
89
- class Service {
90
- hello() {
91
- return 'world';
92
- }
85
+ assert.equal(parent.inject(A), app.inject(A));
86
+ });
87
+
88
+ it('should use a factory if provided', () => {
89
+ class Service {
90
+ hello() {
91
+ return 'world';
93
92
  }
93
+ }
94
94
 
95
- const injector = new Injector([
96
- {
97
- provide: Service,
98
- factory() {
99
- return {
100
- hello() {
101
- return 'you';
102
- }
103
- };
104
- }
95
+ const injector = new Injector([
96
+ {
97
+ provide: Service,
98
+ factory() {
99
+ return {
100
+ hello() {
101
+ return 'world';
102
+ }
103
+ };
105
104
  }
106
- ]);
105
+ }
106
+ ]);
107
107
 
108
- expect(injector.inject(Service).hello()).to.equal('you');
109
- });
108
+ assert.equal(injector.inject(Service).hello(), 'world');
109
+ });
110
110
 
111
- it('should throw an error if provider is missing both factory and use', () => {
112
- class Service {
113
- hello() {
114
- return 'world';
115
- }
111
+ it('should throw an error if provider is missing both factory and use', () => {
112
+ class Service {
113
+ hello() {
114
+ return 'world';
116
115
  }
116
+ }
117
117
 
118
- const injector = new Injector([
119
- {
120
- provide: Service
121
- }
122
- ]);
118
+ const injector = new Injector([
119
+ {
120
+ provide: Service
121
+ }
122
+ ]);
123
123
 
124
- expect(() => injector.inject(Service)).to.throw(
125
- "Provider for Service found but is missing either 'use' or 'factory'"
126
- );
127
- });
124
+ assert.throws(
125
+ () => injector.inject(Service),
126
+ "Provider for Service found but is missing either 'use' or 'factory'"
127
+ );
128
+ });
128
129
 
129
- it('should pass factories and instance of the injector', (done) => {
130
- class Service {
131
- hello() {
132
- return 'world';
133
- }
130
+ it('should pass factories and instance of the injector', async () => {
131
+ class Service {
132
+ hello() {
133
+ return 'world';
134
134
  }
135
+ }
135
136
 
136
- const injector = new Injector([
137
- {
138
- provide: Service,
139
- factory(i) {
140
- expect(i).to.equal(injector);
137
+ let factoryInjector: Injector | null = null;
141
138
 
142
- done();
143
- }
139
+ const injector = new Injector([
140
+ {
141
+ provide: Service,
142
+ factory(i) {
143
+ factoryInjector = i;
144
144
  }
145
- ]);
145
+ }
146
+ ]);
146
147
 
147
- injector.inject(Service);
148
- });
148
+ injector.inject(Service);
149
149
 
150
- it('should create an instance from a StaticToken factory', () => {
151
- const TOKEN = new StaticToken('test', () => 'Hello World');
152
- const injector = new Injector();
150
+ assert.equal(factoryInjector, injector);
151
+ });
153
152
 
154
- const res = injector.inject(TOKEN);
153
+ it('should create an instance from a StaticToken factory', () => {
154
+ const TOKEN = new StaticToken('test', () => 'Hello World');
155
+ const injector = new Injector();
155
156
 
156
- expect(res).to.equal('Hello World');
157
- });
157
+ const res = injector.inject(TOKEN);
158
158
 
159
- it('should create an instance from an async StaticToken factory', async () => {
160
- const TOKEN = new StaticToken('test', () => Promise.resolve('Hello World'));
161
- const injector = new Injector();
159
+ assert.equal(res, 'Hello World');
160
+ });
162
161
 
163
- const res = await injector.inject(TOKEN);
162
+ it('should create an instance from an async StaticToken factory', async () => {
163
+ const TOKEN = new StaticToken('test', () => Promise.resolve('Hello World'));
164
+ const injector = new Injector();
164
165
 
165
- expect(res).to.equal('Hello World');
166
- });
166
+ const res = await injector.inject(TOKEN);
167
167
 
168
- it('should allow static token to be overridden', () => {
169
- const TOKEN = new StaticToken<string>('test');
168
+ assert.equal(res, 'Hello World');
169
+ });
170
170
 
171
- const provider: Provider<string> = {
172
- provide: TOKEN,
173
- factory() {
174
- return 'Hello World';
175
- }
176
- };
171
+ it('should allow static token to be overridden', () => {
172
+ const TOKEN = new StaticToken<string>('test');
173
+
174
+ const provider: Provider<string> = {
175
+ provide: TOKEN,
176
+ factory() {
177
+ return 'Hello World';
178
+ }
179
+ };
177
180
 
178
- const injector = new Injector([provider]);
181
+ const injector = new Injector([provider]);
179
182
 
180
- const res = injector.inject(TOKEN);
183
+ const res = injector.inject(TOKEN);
181
184
 
182
- expect(res).to.equal('Hello World');
183
- });
185
+ assert.equal(res, 'Hello World');
184
186
  });
@@ -1,6 +1,16 @@
1
- import { INJECTABLE_MAP } from './injectable.js';
2
- import { LifeCycle } from './lifecycle.js';
3
- import { InjectionToken, Provider, StaticToken } from './provider.js';
1
+ import { readMetadata } from './metadata.js';
2
+ import {
3
+ ConstructableToken,
4
+ InjectionToken,
5
+ Provider,
6
+ ProviderFactory,
7
+ StaticToken
8
+ } from './provider.js';
9
+
10
+ /**
11
+ * Keeps track of all Injectable services and their Injector
12
+ */
13
+ export const injectables = new WeakMap<object, Injector>();
4
14
 
5
15
  /**
6
16
  * Injectors create and store instances of services.
@@ -31,17 +41,17 @@ export class Injector {
31
41
  this.providers = providers;
32
42
  }
33
43
 
34
- inject<T>(token: InjectionToken<T>): T {
35
- return this.get(token);
36
- }
37
-
38
44
  // resolves and retuns and instance of the requested service
39
- get<T>(token: InjectionToken<T>): T {
45
+ inject<T>(token: InjectionToken<T>): T {
40
46
  // check for a local instance
41
47
  if (this.#instances.has(token)) {
42
48
  const instance = this.#instances.get(token)!;
43
49
 
44
- callLifecycle(instance, LifeCycle.onInject);
50
+ const metadata = readMetadata(token as ConstructableToken<T>);
51
+
52
+ if (metadata) {
53
+ callLifecycle(instance, metadata.onInjected);
54
+ }
45
55
 
46
56
  return instance;
47
57
  }
@@ -89,7 +99,7 @@ export class Injector {
89
99
  this.#instances = new WeakMap();
90
100
  }
91
101
 
92
- #createAndCache<T>(token: InjectionToken<T>, factory: (injector: Injector) => T): T {
102
+ #createAndCache<T>(token: InjectionToken<T>, factory: ProviderFactory<T>): T {
93
103
  const instance = factory(this);
94
104
 
95
105
  this.#instances.set(token, instance);
@@ -98,7 +108,7 @@ export class Injector {
98
108
  * Only values that are objects are able to have associated injectors
99
109
  */
100
110
  if (typeof instance === 'object' && instance !== null) {
101
- const injector = INJECTABLE_MAP.get(instance);
111
+ const injector = injectables.get(instance);
102
112
 
103
113
  if (injector) {
104
114
  /**
@@ -114,8 +124,12 @@ export class Injector {
114
124
  * this ensures that services are initialized when the chain is settled
115
125
  * this is required since the parent is set after the instance is constructed
116
126
  */
117
- callLifecycle(instance, LifeCycle.onInit);
118
- callLifecycle(instance, LifeCycle.onInject);
127
+ const metadata = readMetadata(token as ConstructableToken<T>);
128
+
129
+ if (metadata) {
130
+ callLifecycle(instance, metadata.onCreated);
131
+ callLifecycle(instance, metadata.onInjected);
132
+ }
119
133
  }
120
134
 
121
135
  return instance;
@@ -136,12 +150,12 @@ export class Injector {
136
150
  }
137
151
  }
138
152
 
139
- function callLifecycle(instance: unknown, method: symbol) {
140
- if (typeof instance === 'object' && instance !== null) {
141
- const lifecycle = Reflect.get(instance, method);
142
-
143
- if (typeof lifecycle === 'function') {
144
- lifecycle.call(instance);
153
+ function callLifecycle(instance: object, methods?: unknown) {
154
+ if (Array.isArray(methods)) {
155
+ for (let cb of methods) {
156
+ if (typeof cb === 'function') {
157
+ cb.call(instance);
158
+ }
145
159
  }
146
160
  }
147
161
  }