diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index a1c3f9f9c9d..bedec448513 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -34,13 +34,14 @@ describe("@Inject()", () => { test: InjectorService; } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(Test); + const instance = inject(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); }); it("should inject service and async factory", async () => { + // const inj = injector({rebuild: true}); + // GIVEN class Test { constructor(public type: string) {} @@ -69,12 +70,10 @@ describe("@Inject()", () => { test: Test; } - const inj = injector({rebuild: true}); - - await inj.load(); + await injector().load(); - const parent1 = await inj.invoke(Parent1); - const parent2 = await inj.invoke(Parent2); + const parent1 = inject(Parent1); + const parent2 = inject(Parent2); expect(parent1.test).toBeInstanceOf(Test); expect(parent2.test).toBeInstanceOf(Test); @@ -87,8 +86,7 @@ describe("@Inject()", () => { test: InjectorService; } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(Test); + const instance = inject(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -101,8 +99,7 @@ describe("@Inject()", () => { test: InjectorService; } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(Test); + const instance = inject(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -151,11 +148,9 @@ describe("@Inject()", () => { instances: InterfaceGroup[]; } - const inj = injector({rebuild: true}); + await injector().load(); - await inj.load(); - - const instance = await inj.invoke(MyInjectable); + const instance = inject(MyInjectable); expect(instance.instances).toBeInstanceOf(Array); expect(instance.instances).toHaveLength(3); @@ -212,8 +207,7 @@ describe("@Inject()", () => { constructor(@Inject(InjectorService) readonly injector: InjectorService) {} } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(MyInjectable); + const instance = inject(MyInjectable); expect(instance.injector).toBeInstanceOf(InjectorService); }); @@ -263,11 +257,9 @@ describe("@Inject()", () => { constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {} } - const inj = injector({rebuild: true}); - - await inj.load(); + await injector().load(); - const instance = await inj.invoke(MyInjectable); + const instance = inject(MyInjectable); expect(instance.instances).toBeInstanceOf(Array); expect(instance.instances).toHaveLength(3); diff --git a/packages/di/src/common/fn/injectable.ts b/packages/di/src/common/fn/injectable.ts index d9b261a65b3..70226c77bbb 100644 --- a/packages/di/src/common/fn/injectable.ts +++ b/packages/di/src/common/fn/injectable.ts @@ -1,78 +1,7 @@ -import "../registries/ProviderRegistry.js"; - -import {Store, type Type} from "@tsed/core"; - import {ControllerProvider} from "../domain/ControllerProvider.js"; import type {Provider} from "../domain/Provider.js"; import {ProviderType} from "../domain/ProviderType.js"; -import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; -import type {TokenProvider} from "../interfaces/TokenProvider.js"; -import {GlobalProviders} from "../registries/GlobalProviders.js"; - -type ProviderBuilder = { - [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: (value: T[K]) => ProviderBuilder; -} & { - inspect(): BaseProvider; - store(): Store; - token(): Token; - factory(f: (...args: unknown[]) => unknown): ProviderBuilder; - asyncFactory(f: (...args: unknown[]) => Promise): ProviderBuilder; - value(v: unknown): ProviderBuilder; - class(c: Type): ProviderBuilder; -}; - -export function providerBuilder(props: string[], baseOpts: Partial> = {}) { - return ( - token: Token, - options: Partial> = {} - ): ProviderBuilder> => { - const provider = GlobalProviders.merge(token, { - ...options, - ...baseOpts, - provide: token - }); - - return props.reduce( - (acc, prop) => { - return { - ...acc, - [prop]: function (value: any) { - (provider as any)[prop] = value; - return this; - } - }; - }, - { - factory(factory: any) { - provider.useFactory = factory; - return this; - }, - asyncFactory(asyncFactory: any) { - provider.useAsyncFactory = asyncFactory; - return this; - }, - value(value: any) { - provider.useValue = value; - provider.type = ProviderType.VALUE; - return this; - }, - class(k: any) { - provider.useClass = k; - return this; - }, - store() { - return provider.store; - }, - inspect() { - return provider; - }, - token() { - return provider.token as Token; - } - } as ProviderBuilder> - ); - }; -} +import {providerBuilder} from "../utils/providerBuilder.js"; type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "imports" | "configuration" | "priority"; diff --git a/packages/di/src/common/registries/GlobalProviders.ts b/packages/di/src/common/registries/GlobalProviders.ts index 7c142df537c..e0ba1e5c62c 100644 --- a/packages/di/src/common/registries/GlobalProviders.ts +++ b/packages/di/src/common/registries/GlobalProviders.ts @@ -1,13 +1,10 @@ import {getClassOrSymbol, Type} from "@tsed/core"; -import type {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; import {ProviderType} from "../domain/ProviderType.js"; import {ProviderOpts} from "../interfaces/ProviderOpts.js"; import {RegistrySettings} from "../interfaces/RegistrySettings.js"; -import {ResolvedInvokeOptions} from "../interfaces/ResolvedInvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; -import type {InjectorService} from "../services/InjectorService.js"; export class GlobalProviderRegistry extends Map { #settings: Map = new Map(); @@ -47,6 +44,10 @@ export class GlobalProviderRegistry extends Map { * @param options */ merge(target: TokenProvider, options: Partial) { + if (options.global === false) { + return GlobalProviders.createProvider(target, options); + } + const meta = this.createIfNotExists(target, options); Object.keys(options).forEach((key) => { @@ -99,18 +100,21 @@ export class GlobalProviderRegistry extends Map { ); } + protected createProvider(key: TokenProvider, options: Partial>) { + const type = options.type || ProviderType.PROVIDER; + const {model = Provider} = this.#settings.get(type) || {}; + + return new model(key, options); + } + /** * * @param key * @param options */ protected createIfNotExists(key: TokenProvider, options: Partial): Provider { - const type = options.type || ProviderType.PROVIDER; - if (!this.has(key)) { - const {model = Provider} = this.#settings.get(type) || {}; - - const item = new model(key, options); + const item = this.createProvider(key, options); this.set(key, item); } diff --git a/packages/di/src/common/registries/ProviderRegistry.spec.ts b/packages/di/src/common/registries/ProviderRegistry.spec.ts index 5b1b26c360e..1b465864235 100644 --- a/packages/di/src/common/registries/ProviderRegistry.spec.ts +++ b/packages/di/src/common/registries/ProviderRegistry.spec.ts @@ -17,7 +17,8 @@ describe("ProviderRegistry", () => { registerProvider({provide: Test}); expect(GlobalProviders.merge).toHaveBeenCalledWith(Test, { - provide: Test + provide: Test, + global: true }); }); }); diff --git a/packages/di/src/common/registries/ProviderRegistry.ts b/packages/di/src/common/registries/ProviderRegistry.ts index f99177ac320..f7d7b9ec67d 100644 --- a/packages/di/src/common/registries/ProviderRegistry.ts +++ b/packages/di/src/common/registries/ProviderRegistry.ts @@ -1,5 +1,6 @@ import {ControllerProvider} from "../domain/ControllerProvider.js"; import {ProviderType} from "../domain/ProviderType.js"; +import {injectable} from "../fn/injectable.js"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; import {GlobalProviders} from "./GlobalProviders.js"; @@ -7,8 +8,8 @@ GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider); /** * Register a provider configuration. - * @param {ProviderOpts} provider + * @param {ProviderOpts} opts */ -export function registerProvider(provider: Partial> & Pick, "provide">) { - return GlobalProviders.merge(provider.provide, provider); +export function registerProvider(opts: Partial> & Pick, "provide">) { + return injectable(opts.provide, opts as unknown as Partial).inspect(); } diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 2f494d2defc..b1eb732abaa 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -44,6 +44,7 @@ export class InjectorService extends Container { public logger: DILogger = console; private resolvedConfiguration: boolean = false; #cache = new LocalsContainer(); + #loaded: boolean = false; constructor() { super(); @@ -59,6 +60,10 @@ export class InjectorService extends Container { this.#cache.set(Configuration, settings); } + isLoaded() { + return this.#loaded; + } + /** * Return a list of instance build by the injector. */ @@ -248,6 +253,10 @@ export class InjectorService extends Container { * @param container */ async load(container: Container = createContainer()) { + // avoid provider registration in the GlobalContainer during the loading phase + // using injectable() or providerBuilder() + this.#loaded = true; + await $asyncEmit("$beforeInit"); this.bootstrap(container); diff --git a/packages/di/src/common/utils/providerBuilder.ts b/packages/di/src/common/utils/providerBuilder.ts new file mode 100644 index 00000000000..d942c4a5239 --- /dev/null +++ b/packages/di/src/common/utils/providerBuilder.ts @@ -0,0 +1,81 @@ +import "../registries/ProviderRegistry.js"; + +import {Store, type Type} from "@tsed/core"; + +import {ProviderType} from "../domain/ProviderType.js"; +import {hasInjector, injector} from "../fn/injector.js"; +import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; +import type {TokenProvider} from "../interfaces/TokenProvider.js"; +import {GlobalProviders} from "../registries/GlobalProviders.js"; + +type ProviderBuilder = { + [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: (value: T[K]) => ProviderBuilder; +} & { + inspect(): BaseProvider; + store(): Store; + token(): Token; + factory(f: (...args: unknown[]) => unknown): ProviderBuilder; + asyncFactory(f: (...args: unknown[]) => Promise): ProviderBuilder; + value(v: unknown): ProviderBuilder; + class(c: Type): ProviderBuilder; +}; + +export function providerBuilder(props: string[], baseOpts: Partial> = {}) { + return ( + token: Token, + options: Partial> = {} + ): ProviderBuilder> => { + const merged = { + global: !hasInjector() || injector().isLoaded(), + ...options, + ...baseOpts, + provide: token + }; + + const provider = GlobalProviders.merge(token, merged); + + if (!merged.global) { + injector().setProvider(token, provider); + } + + return props.reduce( + (acc, prop) => { + return { + ...acc, + [prop]: function (value: any) { + (provider as any)[prop] = value; + return this; + } + }; + }, + { + factory(factory: any) { + provider.useFactory = factory; + return this; + }, + asyncFactory(asyncFactory: any) { + provider.useAsyncFactory = asyncFactory; + return this; + }, + value(value: any) { + provider.useValue = value; + provider.type = ProviderType.VALUE; + return this; + }, + class(k: any) { + provider.useClass = k; + return this; + }, + store() { + return provider.store; + }, + inspect() { + return provider; + }, + token() { + return provider.token as Token; + } + } as ProviderBuilder> + ); + }; +}