Skip to content

Commit 4b3261e

Browse files
committed
fix(di): fix multi injection for dynamic load
1 parent 3f7b949 commit 4b3261e

File tree

8 files changed

+202
-8
lines changed

8 files changed

+202
-8
lines changed

packages/reactant-di/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const METADATA_KEY = {
22
optional: 'reactant:optional',
33
provide: 'reactant:provide',
4+
multiple: 'reactant:multiple',
45
lazy: 'reactant:lazy',
56
paramtypes: 'design:paramtypes',
67
inversifyParamtypes: 'inversify:paramtypes',

packages/reactant-di/src/decorators/multiInject.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { multiInject as multiInjectWithInversify, decorate } from 'inversify';
2+
import { METADATA_KEY } from '../constants';
23
import { ServiceIdentifier } from '../interfaces';
4+
import { setMetadata } from '../util';
35

46
export function multiInject(serviceIdentifier: ServiceIdentifier<any>) {
57
return (target: object, key?: string, index?: number) => {
8+
const paramtypes = Reflect.getMetadata(METADATA_KEY.paramtypes, target);
9+
setMetadata(METADATA_KEY.multiple, paramtypes[index!], serviceIdentifier);
610
decorate(
711
multiInjectWithInversify(serviceIdentifier) as ClassDecorator,
812
target,

packages/reactant-di/src/decorators/multiOptional.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function multiOptional(serviceIdentifier: ServiceIdentifier<any>) {
88
return (target: object, key?: string, index?: number) => {
99
const paramtypes = Reflect.getMetadata(METADATA_KEY.paramtypes, target);
1010
setMetadata(METADATA_KEY.optional, paramtypes[index!], serviceIdentifier);
11+
setMetadata(METADATA_KEY.multiple, paramtypes[index!], serviceIdentifier);
1112
decorate(multiInject(serviceIdentifier) as ClassDecorator, target, index);
1213
decorate(optionalWithInversify() as ClassDecorator, target, index);
1314
};

packages/reactant-di/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ export {
1212
export { Optional } from './optional';
1313
export { forwardRef } from './forwardRef';
1414
export { ModuleRef } from './moduleRef';
15+
export { METADATA_KEY } from './constants';
16+
export { getMetadata } from './util';
1517
export * from './interfaces';

packages/reactant-module/src/core/createStore.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
produce,
88
setAutoFreeze,
99
} from 'immer';
10-
import type {
10+
import {
1111
Container,
12+
getMetadata,
13+
METADATA_KEY,
1214
ModuleOptions,
1315
ServiceIdentifiersMap,
1416
} from 'reactant-di';
@@ -103,16 +105,23 @@ export function createStore<T = any>({
103105
ServiceIdentifiers.set(moduleIdentifier, []);
104106
}
105107
}
108+
const multipleInjectMap = getMetadata(METADATA_KEY.multiple);
106109
for (const [ServiceIdentifier] of ServiceIdentifiers) {
107110
// `Service` should be bound before `createStore`.
108-
if (
111+
const isMultipleInjection = multipleInjectMap.has(ServiceIdentifier);
112+
const shouldInstantiate =
109113
container.isBound(ServiceIdentifier) &&
110-
!loadedModules.has(ServiceIdentifier)
111-
) {
114+
(isMultipleInjection ||
115+
(!isMultipleInjection && !loadedModules.has(ServiceIdentifier)));
116+
if (shouldInstantiate) {
112117
const services: IService[] = container.getAll(ServiceIdentifier);
113118
loadedModules.add(ServiceIdentifier);
114119
services.forEach((service, index) => {
115-
if (typeof service !== 'object' || service === null) {
120+
if (
121+
typeof service !== 'object' ||
122+
service === null ||
123+
(service[modulesKey] && isMultipleInjection)
124+
) {
116125
return;
117126
}
118127
handlePlugin(service, pluginHooks);

packages/reactant-module/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export {
1010
Optional,
1111
forwardRef,
1212
ModuleRef,
13+
getMetadata,
14+
METADATA_KEY,
1315
} from 'reactant-di';
1416

1517
export type {

packages/reactant/src/createApp.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-nested-ternary */
12
/* eslint-disable no-shadow */
23
import React, { FunctionComponent, StrictMode, Context } from 'react';
34
import { Provider } from 'react-redux';
@@ -14,6 +15,8 @@ import {
1415
modulesKey,
1516
Service,
1617
unsubscriptionsKey,
18+
getMetadata,
19+
METADATA_KEY,
1720
} from 'reactant-module';
1821
import { Config, App, Renderer } from './interfaces';
1922

@@ -95,9 +98,26 @@ function createApp<T, S extends any[], R extends Renderer<S>>({
9598
const loadedModules = new Set();
9699

97100
const loader: Loader = (loadModules, beforeReplaceReducer) => {
98-
bindModules(container, loadModules);
101+
const multipleInjectMap = getMetadata(METADATA_KEY.multiple);
102+
const filteredModules = loadModules.filter((module) => {
103+
const serviceIdentifier =
104+
typeof module === 'function'
105+
? module
106+
: typeof module === 'object'
107+
? module.provide
108+
: undefined;
109+
if (serviceIdentifier) {
110+
return (
111+
multipleInjectMap.has(serviceIdentifier) ||
112+
(!multipleInjectMap.has(serviceIdentifier) &&
113+
!container.isBound(serviceIdentifier))
114+
);
115+
}
116+
return true;
117+
});
118+
bindModules(container, filteredModules);
99119
createStore({
100-
modules: loadModules,
120+
modules: filteredModules,
101121
container,
102122
ServiceIdentifiers,
103123
loadedModules,

packages/reactant/test/integration/load.test.tsx

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-use-before-define */
12
import {
23
createApp,
34
injectable,
@@ -9,6 +10,8 @@ import {
910
ModuleOptions,
1011
inject,
1112
ClassProvider,
13+
multiInject,
14+
multiOptional,
1215
} from '../..';
1316

1417
test('base `load`', async () => {
@@ -106,6 +109,47 @@ test('base `load` with multi-modules', async () => {
106109
}
107110
}
108111

112+
@injectable({
113+
name: 'Counter1',
114+
})
115+
class Counter1 {
116+
@state
117+
count1 = 0;
118+
}
119+
120+
@injectable({
121+
name: 'Counter2',
122+
})
123+
class Counter2 {
124+
constructor(public counter1: Counter1) {}
125+
126+
@state
127+
count2 = 0;
128+
}
129+
130+
interface ITodo {
131+
list: string[];
132+
add(text: string): void;
133+
counter1: Counter1;
134+
counter2: Counter2;
135+
}
136+
137+
@injectable()
138+
class Todo implements ITodo {
139+
constructor(
140+
public counter1: Counter1,
141+
@inject('counter2token') public counter2: Counter2
142+
) {}
143+
144+
@state
145+
list: string[] = [];
146+
147+
@action
148+
add(text: string) {
149+
this.list.push(text);
150+
}
151+
}
152+
109153
const app = createApp({
110154
modules: [{ provide: 'Counter0', useClass: Counter0 }],
111155
main: Counter,
@@ -114,6 +158,92 @@ test('base `load` with multi-modules', async () => {
114158
},
115159
});
116160

161+
expect(Object.values(app.store?.getState())).toEqual([
162+
{ count0: 0 },
163+
{ count: 0 },
164+
]);
165+
expect(app.instance.todo).toBeUndefined();
166+
await app.instance.loadTodoModule([
167+
{ provide: 'todo', useClass: Todo },
168+
{ provide: 'counter2token', useClass: Counter2 },
169+
]);
170+
expect(app.instance.todo).toBeInstanceOf(Todo);
171+
expect(app.instance.todoModule).toBe(app.instance.todo);
172+
expect(Object.values(app.store?.getState())).toEqual([
173+
{ count0: 0 },
174+
{ count: 0 },
175+
{
176+
list: [],
177+
},
178+
{ count2: 0 },
179+
{ count1: 0 },
180+
]);
181+
expect(app.instance.todo.counter2.counter1).toBeInstanceOf(Counter1);
182+
});
183+
184+
test('base `load` with multi-modules and multi injection', async () => {
185+
@injectable({
186+
name: 'Counter0',
187+
})
188+
class Counter0 {
189+
@state
190+
count0 = 0;
191+
}
192+
193+
@injectable({
194+
name: 'Counter3',
195+
})
196+
class Counter3 {
197+
@state
198+
count3 = 0;
199+
}
200+
201+
@injectable({
202+
name: 'Counter4',
203+
})
204+
class Counter4 {
205+
@state
206+
count4 = 0;
207+
}
208+
209+
@injectable({
210+
name: 'Counter5',
211+
})
212+
class Counter5 {
213+
@state
214+
count5 = 0;
215+
}
216+
217+
@injectable()
218+
class Counter {
219+
constructor(
220+
@inject('Counter0') public counter0: Counter0,
221+
@inject('Counter3') public counter3: Counter3,
222+
@multiInject('Counter4') public counter4: Counter4[],
223+
public moduleRef: ModuleRef,
224+
@optional('todo') public todo?: ITodo,
225+
@optional('todo1') public todo1?: ITodo,
226+
@multiOptional('Counter5') public counter5?: Counter5[]
227+
) {}
228+
229+
async loadTodoModule(modules: ModuleOptions[] = []) {
230+
const container = await load(this, modules);
231+
this.todo = container.get('todo');
232+
}
233+
234+
get todoModule() {
235+
return this.moduleRef.get('todo');
236+
}
237+
238+
@state
239+
count = 0;
240+
241+
@action
242+
increase() {
243+
this.count += 1;
244+
}
245+
}
246+
117247
@injectable({
118248
name: 'Counter1',
119249
})
@@ -154,25 +284,50 @@ test('base `load` with multi-modules', async () => {
154284
this.list.push(text);
155285
}
156286
}
287+
288+
const app = createApp({
289+
modules: [
290+
{ provide: 'Counter0', useClass: Counter0 },
291+
{ provide: 'Counter3', useClass: Counter3 },
292+
{ provide: 'Counter4', useClass: Counter4 },
293+
{ provide: 'Counter5', useClass: Counter5 },
294+
],
295+
main: Counter,
296+
render: () => {
297+
//
298+
},
299+
});
300+
157301
expect(Object.values(app.store?.getState())).toEqual([
158302
{ count0: 0 },
303+
{ count3: 0 },
304+
{ count4: 0 },
305+
{ count5: 0 },
159306
{ count: 0 },
160307
]);
161308
expect(app.instance.todo).toBeUndefined();
162309
await app.instance.loadTodoModule([
163310
{ provide: 'todo', useClass: Todo },
311+
{ provide: 'Counter3', useClass: Counter3 },
164312
{ provide: 'counter2token', useClass: Counter2 },
313+
{ provide: 'Counter4', useClass: Counter4 },
314+
{ provide: 'Counter5', useClass: Counter5 },
165315
]);
166316
expect(app.instance.todo).toBeInstanceOf(Todo);
167317
expect(app.instance.todoModule).toBe(app.instance.todo);
168318
expect(Object.values(app.store?.getState())).toEqual([
169319
{ count0: 0 },
320+
{ count3: 0 },
321+
{ count4: 0 },
322+
{ count5: 0 },
170323
{ count: 0 },
324+
{ count4: 0 },
325+
{ count5: 0 },
171326
{
172327
list: [],
173328
},
174329
{ count2: 0 },
175330
{ count1: 0 },
176331
]);
177-
expect(app.instance.todo.counter2.counter1).toBeInstanceOf(Counter1);
332+
expect(app.instance.todo!.counter2.counter1).toBeInstanceOf(Counter1);
178333
});

0 commit comments

Comments
 (0)