@deepkit/injector
Advanced tools
Comparing version 1.0.1-alpha.51 to 1.0.1-alpha.52
@@ -0,3 +1,5 @@ | ||
export * from './src/config'; | ||
export * from './src/decorator'; | ||
export * from './src/injector'; | ||
export * from './src/provider'; | ||
export * from './src/module'; | ||
export * from './src/provider'; |
@@ -13,5 +13,7 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./src/config"), exports); | ||
__exportStar(require("./src/decorator"), exports); | ||
__exportStar(require("./src/injector"), exports); | ||
__exportStar(require("./src/provider"), exports); | ||
__exportStar(require("./src/module"), exports); | ||
__exportStar(require("./src/provider"), exports); | ||
//# sourceMappingURL=index.js.map |
@@ -1,68 +0,6 @@ | ||
import { ClassSchema, ExtractClassDefinition, PlainSchemaProps, PropertySchema } from '@deepkit/type'; | ||
import { Provider, ProviderWithScope, TagRegistry } from './provider'; | ||
import { NormalizedProvider, ProviderWithScope, TagRegistry, Token } from './provider'; | ||
import { ClassType, CompilerContext, CustomError } from '@deepkit/core'; | ||
import { PropertySchema } from '@deepkit/type'; | ||
import { InjectorToken } from './decorator'; | ||
import { InjectorModule } from './module'; | ||
export declare class ConfigToken<T extends {}> { | ||
config: ConfigDefinition<T>; | ||
name: keyof T & string; | ||
constructor(config: ConfigDefinition<T>, name: keyof T & string); | ||
} | ||
export declare class ConfigSlice<T extends {}> { | ||
bag?: { | ||
[name: string]: any; | ||
}; | ||
config: ConfigDefinition<T>; | ||
names: (keyof T & string)[]; | ||
constructor(config: ConfigDefinition<T>, names: (keyof T & string)[]); | ||
valueOf(): this; | ||
} | ||
export declare class ConfigDefinition<T extends {}> { | ||
readonly schema: ClassSchema<T>; | ||
protected module?: InjectorModule; | ||
type: T; | ||
constructor(schema: ClassSchema<T>); | ||
setModule(module: InjectorModule): void; | ||
hasModule(): boolean; | ||
getModule(): InjectorModule; | ||
getConfigOrDefaults(): any; | ||
all(): ClassType<T>; | ||
slice<N extends (keyof T & string)[]>(names: N): ClassType<Pick<T, N[number]>>; | ||
token<N extends (keyof T & string)>(name: N): ConfigToken<T>; | ||
} | ||
export declare class InjectorReference { | ||
readonly to: any; | ||
constructor(to: any); | ||
} | ||
export declare function injectorReference<T>(classTypeOrToken: T): any; | ||
export declare function createConfig<T extends PlainSchemaProps>(config: T): ConfigDefinition<ExtractClassDefinition<T>>; | ||
export interface InjectDecorator { | ||
(target: object, property?: string, parameterIndexOrDescriptor?: any): any; | ||
/** | ||
* Mark as optional. | ||
*/ | ||
readonly optional: this; | ||
/** | ||
* Resolves the dependency token from the root injector. | ||
*/ | ||
readonly root: this; | ||
readonly options: { | ||
token: any; | ||
optional: boolean; | ||
root: boolean; | ||
}; | ||
} | ||
export declare type InjectOptions = { | ||
token: any | ForwardRef<any>; | ||
optional: boolean; | ||
root: boolean; | ||
}; | ||
declare type ForwardRef<T> = () => T; | ||
export declare function isInjectDecorator(v: any): v is InjectDecorator; | ||
export declare function inject(token?: any | ForwardRef<any>): InjectDecorator; | ||
export declare class InjectToken { | ||
readonly name: string; | ||
constructor(name: string); | ||
toString(): string; | ||
} | ||
export declare function injectable(): (target: object) => void; | ||
export declare class CircularDependencyError extends CustomError { | ||
@@ -75,101 +13,3 @@ } | ||
export declare function tokenLabel(token: any): string; | ||
export interface ConfigContainer { | ||
get(path: string): any; | ||
} | ||
export interface BasicInjector { | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: BasicInjector): R; | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | ||
export declare class Injector implements BasicInjector { | ||
protected providers: Provider[]; | ||
protected parents: (BasicInjector | Injector)[]; | ||
protected injectorContext: InjectorContext; | ||
protected configuredProviderRegistry: ConfiguredProviderRegistry | undefined; | ||
protected tagRegistry: TagRegistry; | ||
protected contextResolver?: { | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | undefined; | ||
circularCheck: boolean; | ||
protected resolved: any[]; | ||
protected retriever(injector: Injector, token: any, frontInjector?: Injector): any; | ||
constructor(providers?: Provider[], parents?: (BasicInjector | Injector)[], injectorContext?: InjectorContext, configuredProviderRegistry?: ConfiguredProviderRegistry | undefined, tagRegistry?: TagRegistry, contextResolver?: { | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | undefined); | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
/** | ||
* Creates a clone of this instance, maintains the provider structure, but drops provider instances. | ||
* Note: addProviders() in the new fork changes the origin, since providers array is not cloned. | ||
*/ | ||
fork(parents?: Injector[], injectorContext?: InjectorContext): Injector; | ||
/** | ||
* Changes the provider structure of this injector. | ||
* | ||
* Note: This is very performance sensitive. Every time you call this function a new dependency injector function | ||
* is generated, which si pretty slow. So, it's recommended to create a Injector with providers in the constructor | ||
* and not change it. | ||
*/ | ||
addProviders(...providers: Provider[]): void; | ||
isRoot(): boolean; | ||
protected createFactoryProperty(options: { | ||
name: string | number; | ||
token: any; | ||
optional: boolean; | ||
}, compiler: CompilerContext, ofName: string, argPosition: number, notFoundFunction: string): string; | ||
protected optionsFromProperty(property: PropertySchema): { | ||
token: any; | ||
name: string | number; | ||
optional: boolean; | ||
}; | ||
protected createFactory(compiler: CompilerContext, classType: ClassType): string; | ||
protected buildRetriever(): (injector: Injector, token: any, frontInjector?: Injector) => any; | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R; | ||
} | ||
export declare class MemoryInjector extends Injector { | ||
protected providers: ({ | ||
provide: any; | ||
useValue: any; | ||
} | { | ||
provide: any; | ||
useFactory: () => any; | ||
})[]; | ||
constructor(providers: ({ | ||
provide: any; | ||
useValue: any; | ||
} | { | ||
provide: any; | ||
useFactory: () => any; | ||
})[]); | ||
fork(parents?: Injector[]): Injector; | ||
protected retriever(injector: Injector, token: any): any; | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R; | ||
} | ||
export declare class ContextRegistry { | ||
contexts: Context[]; | ||
get size(): number; | ||
get(id: number): Context; | ||
set(id: number, value: Context): void; | ||
} | ||
export declare class ScopedContextScopeCaches { | ||
protected size: number; | ||
protected caches: { | ||
[name: string]: ScopedContextCache; | ||
}; | ||
constructor(size: number); | ||
getCache(scope: string): ScopedContextCache; | ||
} | ||
export declare class ScopedContextCache { | ||
protected size: number; | ||
protected injectors: (Injector | undefined)[]; | ||
constructor(size: number); | ||
get(contextId: number): Injector | undefined; | ||
set(contextId: number, injector: Injector): void; | ||
} | ||
export declare class Context { | ||
readonly module: InjectorModule; | ||
readonly id: number; | ||
readonly parent?: Context | undefined; | ||
providers: ProviderWithScope[]; | ||
constructor(module: InjectorModule, id: number, parent?: Context | undefined); | ||
} | ||
export declare type ConfiguredProviderCalls = { | ||
export declare type SetupProviderCalls = { | ||
type: 'call'; | ||
@@ -188,46 +28,90 @@ methodName: string | symbol | number; | ||
}; | ||
export declare class ConfiguredProviderRegistry { | ||
calls: Map<any, ConfiguredProviderCalls[]>; | ||
add(token: any, ...newCalls: ConfiguredProviderCalls[]): void; | ||
get(token: any): ConfiguredProviderCalls[]; | ||
clone(): ConfiguredProviderRegistry; | ||
export declare class SetupProviderRegistry { | ||
calls: Map<Token<any>, SetupProviderCalls[]>; | ||
add(token: any, ...newCalls: SetupProviderCalls[]): void; | ||
mergeInto(registry: SetupProviderRegistry): void; | ||
get(token: Token): SetupProviderCalls[]; | ||
} | ||
export declare type ConfigureProvider<T> = { | ||
[name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name]; | ||
}; | ||
interface Scope { | ||
name: string; | ||
instances: { | ||
[name: string]: any; | ||
}; | ||
} | ||
export declare type ResolveToken<T> = T extends ClassType<infer R> ? R : T extends InjectorToken<infer R> ? R : T; | ||
export declare function resolveToken(provider: ProviderWithScope): Token; | ||
export interface InjectorInterface { | ||
get<T>(token: T, scope?: Scope): ResolveToken<T>; | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* This is the actual dependency injection container. | ||
* Every module has its own injector. | ||
*/ | ||
export declare function setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, registry: ConfiguredProviderRegistry, order: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
export declare class InjectorContext implements BasicInjector { | ||
readonly contextManager: ContextRegistry; | ||
readonly scope: string; | ||
readonly configuredProviderRegistry: ConfiguredProviderRegistry; | ||
readonly parent: InjectorContext | undefined; | ||
readonly additionalInjectorParent: Injector | undefined; | ||
readonly modules: { | ||
[name: string]: InjectorModule; | ||
export declare class Injector implements InjectorInterface { | ||
readonly module: InjectorModule; | ||
private buildContext; | ||
private resolver?; | ||
private setter?; | ||
/** | ||
* All unscoped provider instances. Scoped instances are attached to `Scope`. | ||
*/ | ||
private instances; | ||
constructor(module: InjectorModule, buildContext: BuildContext); | ||
static from(providers: ProviderWithScope[], parent?: Injector): Injector; | ||
static fromModule(module: InjectorModule, parent?: Injector): Injector; | ||
get<T>(token: T, scope?: Scope): ResolveToken<T>; | ||
set<T>(token: T, value: any, scope?: Scope): void; | ||
clear(): void; | ||
protected build(buildContext: BuildContext): void; | ||
protected buildProvider(buildContext: BuildContext, compiler: CompilerContext, name: string, accessor: string, scope: string, provider: NormalizedProvider, resolveDependenciesFrom: InjectorModule[]): string; | ||
protected createFactory(provider: NormalizedProvider, resolvedName: string, compiler: CompilerContext, classType: ClassType, resolveDependenciesFrom: InjectorModule[]): { | ||
code: string; | ||
dependencies: number; | ||
}; | ||
protected createFactoryProperty(options: { | ||
name: string | number; | ||
token: any; | ||
optional: boolean; | ||
}, fromProvider: NormalizedProvider, compiler: CompilerContext, resolveDependenciesFrom: InjectorModule[], ofName: string, argPosition: number, notFoundFunction: string): string; | ||
protected optionsFromProperty(property: PropertySchema): { | ||
token: any; | ||
name: string | number; | ||
optional: boolean; | ||
}; | ||
} | ||
declare class BuildProviderIndex { | ||
protected offset: number; | ||
reserve(): number; | ||
} | ||
export declare class BuildContext { | ||
static ids: number; | ||
id: number; | ||
tagRegistry: TagRegistry; | ||
protected injectors: (Injector | undefined)[]; | ||
readonly scopeCaches: ScopedContextScopeCaches; | ||
protected cache: ScopedContextCache; | ||
constructor(contextManager?: ContextRegistry, scope?: string, configuredProviderRegistry?: ConfiguredProviderRegistry, parent?: InjectorContext | undefined, additionalInjectorParent?: Injector | undefined, modules?: { | ||
[name: string]: InjectorModule; | ||
}, scopeCaches?: ScopedContextScopeCaches, tagRegistry?: TagRegistry); | ||
getModule(name: string): InjectorModule; | ||
registerModule(module: InjectorModule, config?: ConfigDefinition<any>): void; | ||
providerIndex: BuildProviderIndex; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* In the process of preparing providers, each module redirects their | ||
* global setup calls in this registry. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
getModuleNames(): string[]; | ||
globalSetupProviderRegistry: SetupProviderRegistry; | ||
} | ||
/** | ||
* A InjectorContext is responsible for taking a root InjectorModule and build all Injectors. | ||
* | ||
* It also can create scopes aka a sub InjectorContext with providers from a particular scope. | ||
*/ | ||
export declare class InjectorContext { | ||
rootModule: InjectorModule; | ||
readonly scope?: Scope | undefined; | ||
protected buildContext: BuildContext; | ||
constructor(rootModule: InjectorModule, scope?: Scope | undefined, buildContext?: BuildContext); | ||
get<T>(token: T | Token, module?: InjectorModule): ResolveToken<T>; | ||
set<T>(token: T, value: any, module?: InjectorModule): void; | ||
static forProviders(providers: ProviderWithScope[]): InjectorContext; | ||
getInjectorForModule(module: InjectorModule): Injector; | ||
getInjector(contextId: number): Injector; | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R; | ||
createChildScope(scope: string, additionalInjectorParent?: Injector): InjectorContext; | ||
/** | ||
* Returns the unscoped injector. Use `.get(T, Scope)` for resolving scoped token. | ||
*/ | ||
getInjector(module: InjectorModule): Injector; | ||
getRootInjector(): Injector; | ||
createChildScope(scope: string): InjectorContext; | ||
} | ||
export {}; |
"use strict"; | ||
/* | ||
* Deepkit Framework | ||
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the MIT License. | ||
* | ||
* You should have received a copy of the MIT License along with this program. | ||
*/ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InjectorContext = exports.setupProvider = exports.ConfiguredProviderRegistry = exports.Context = exports.ScopedContextCache = exports.ScopedContextScopeCaches = exports.ContextRegistry = exports.MemoryInjector = exports.Injector = exports.tokenLabel = exports.DependenciesUnmetError = exports.TokenNotFoundError = exports.CircularDependencyError = exports.injectable = exports.InjectToken = exports.inject = exports.isInjectDecorator = exports.createConfig = exports.injectorReference = exports.InjectorReference = exports.ConfigDefinition = exports.ConfigSlice = exports.ConfigToken = void 0; | ||
const type_1 = require("@deepkit/type"); | ||
exports.InjectorContext = exports.BuildContext = exports.Injector = exports.resolveToken = exports.SetupProviderRegistry = exports.tokenLabel = exports.DependenciesUnmetError = exports.TokenNotFoundError = exports.CircularDependencyError = void 0; | ||
const provider_1 = require("./provider"); | ||
const core_1 = require("@deepkit/core"); | ||
const type_1 = require("@deepkit/type"); | ||
const decorator_1 = require("./decorator"); | ||
const config_1 = require("./config"); | ||
const module_1 = require("./module"); | ||
class ConfigToken { | ||
constructor(config, name) { | ||
this.config = config; | ||
this.name = name; | ||
} | ||
} | ||
exports.ConfigToken = ConfigToken; | ||
class ConfigSlice { | ||
constructor(config, names) { | ||
//we want that ConfigSlice acts as a regular plain object, which can be serialized at wish. | ||
Object.defineProperties(this, { | ||
config: { enumerable: false, value: config }, | ||
names: { enumerable: false, value: names }, | ||
bag: { enumerable: false, writable: true }, | ||
}); | ||
for (const name of names) { | ||
Object.defineProperty(this, name, { | ||
enumerable: true, | ||
get: () => { | ||
return this.bag ? this.bag[name] : undefined; | ||
} | ||
}); | ||
} | ||
} | ||
valueOf() { | ||
return { ...this }; | ||
} | ||
} | ||
exports.ConfigSlice = ConfigSlice; | ||
class ConfigDefinition { | ||
constructor(schema) { | ||
this.schema = schema; | ||
} | ||
setModule(module) { | ||
this.module = module; | ||
} | ||
hasModule() { | ||
return this.module !== undefined; | ||
} | ||
getModule() { | ||
if (!this.module) | ||
throw new Error('ConfigDefinition module not set. Make sure your config is assigned to a single module. See createModule({config: x}).'); | ||
return this.module; | ||
} | ||
getConfigOrDefaults() { | ||
if (this.module) | ||
return this.module.getConfig(); | ||
return type_1.jsonSerializer.for(this.schema).validatedDeserialize({}); | ||
} | ||
all() { | ||
const self = this; | ||
return class extends ConfigSlice { | ||
constructor() { | ||
super(self, [...self.schema.getProperties()].map(v => v.name)); | ||
} | ||
}; | ||
} | ||
slice(names) { | ||
const self = this; | ||
return class extends ConfigSlice { | ||
constructor() { | ||
super(self, names); | ||
} | ||
}; | ||
} | ||
token(name) { | ||
return new ConfigToken(this, name); | ||
} | ||
} | ||
exports.ConfigDefinition = ConfigDefinition; | ||
class InjectorReference { | ||
constructor(to) { | ||
this.to = to; | ||
} | ||
} | ||
exports.InjectorReference = InjectorReference; | ||
function injectorReference(classTypeOrToken) { | ||
return new InjectorReference(classTypeOrToken); | ||
} | ||
exports.injectorReference = injectorReference; | ||
function createConfig(config) { | ||
return new ConfigDefinition(type_1.t.schema(config)); | ||
} | ||
exports.createConfig = createConfig; | ||
const injectSymbol = Symbol('inject'); | ||
function isInjectDecorator(v) { | ||
return core_1.isFunction(v) && v.hasOwnProperty(injectSymbol); | ||
} | ||
exports.isInjectDecorator = isInjectDecorator; | ||
function inject(token) { | ||
const injectOptions = { | ||
optional: false, | ||
root: false, | ||
token: token, | ||
}; | ||
const fn = (target, propertyOrMethodName, parameterIndexOrDescriptor) => { | ||
type_1.FieldDecoratorWrapper((target, property, returnType) => { | ||
property.data['deepkit/inject'] = injectOptions; | ||
property.setFromJSType(returnType); | ||
})(target, propertyOrMethodName, parameterIndexOrDescriptor); | ||
}; | ||
Object.defineProperty(fn, injectSymbol, { value: true, enumerable: false }); | ||
Object.defineProperty(fn, 'optional', { | ||
get() { | ||
injectOptions.optional = true; | ||
return fn; | ||
} | ||
}); | ||
Object.defineProperty(fn, 'options', { | ||
get() { | ||
return injectOptions; | ||
} | ||
}); | ||
Object.defineProperty(fn, 'root', { | ||
get() { | ||
injectOptions.optional = true; | ||
return fn; | ||
} | ||
}); | ||
return fn; | ||
} | ||
exports.inject = inject; | ||
class InjectToken { | ||
constructor(name) { | ||
this.name = name; | ||
} | ||
toString() { | ||
return 'InjectToken=' + this.name; | ||
} | ||
} | ||
exports.InjectToken = InjectToken; | ||
function injectable() { | ||
return (target) => { | ||
//don't do anything. This is just used to generate type metadata. | ||
}; | ||
} | ||
exports.injectable = injectable; | ||
class CircularDependencyError extends core_1.CustomError { | ||
@@ -177,125 +33,282 @@ } | ||
exports.tokenLabel = tokenLabel; | ||
function constructorParameterNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
throw new DependenciesUnmetError(`Unknown constructor argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function tokenNotfoundError(token, moduleName) { | ||
throw new TokenNotFoundError(`Token '${tokenLabel(token)}' in ${moduleName} not found. Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function factoryDependencyNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function propertyParameterNotFound(ofName, name, position, token) { | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
let CircularDetector = []; | ||
let CircularDetectorResets = []; | ||
class Injector { | ||
constructor(providers = [], parents = [], injectorContext = new InjectorContext, configuredProviderRegistry = undefined, tagRegistry = new provider_1.TagRegistry(), contextResolver) { | ||
this.providers = providers; | ||
this.parents = parents; | ||
this.injectorContext = injectorContext; | ||
this.configuredProviderRegistry = configuredProviderRegistry; | ||
this.tagRegistry = tagRegistry; | ||
this.contextResolver = contextResolver; | ||
this.circularCheck = true; | ||
this.resolved = []; | ||
if (!this.configuredProviderRegistry) | ||
this.configuredProviderRegistry = injectorContext.configuredProviderRegistry; | ||
if (this.providers.length) | ||
this.retriever = this.buildRetriever(); | ||
function throwCircularDependency() { | ||
const path = CircularDetector.map(tokenLabel).join(' -> '); | ||
CircularDetector.length = 0; | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new CircularDependencyError(`Circular dependency found ${path}`); | ||
} | ||
class SetupProviderRegistry { | ||
constructor() { | ||
this.calls = new Map(); | ||
} | ||
retriever(injector, token, frontInjector) { | ||
for (const parent of injector.parents) { | ||
const v = 'retriever' in parent ? parent.retriever(parent, token, frontInjector) : parent.get(token, frontInjector); | ||
if (v !== undefined) | ||
return v; | ||
add(token, ...newCalls) { | ||
this.get(token).push(...newCalls); | ||
} | ||
mergeInto(registry) { | ||
for (const [token, calls] of this.calls) { | ||
registry.add(token, ...calls); | ||
} | ||
return undefined; | ||
} | ||
getInjectorForModule(module) { | ||
return this.contextResolver ? this.contextResolver.getInjectorForModule(module) : this; | ||
get(token) { | ||
let calls = this.calls.get(token); | ||
if (!calls) { | ||
calls = []; | ||
this.calls.set(token, calls); | ||
} | ||
return calls; | ||
} | ||
/** | ||
* Creates a clone of this instance, maintains the provider structure, but drops provider instances. | ||
* Note: addProviders() in the new fork changes the origin, since providers array is not cloned. | ||
*/ | ||
fork(parents, injectorContext) { | ||
const injector = new Injector(undefined, parents || this.parents, injectorContext, this.configuredProviderRegistry, this.tagRegistry, this.contextResolver); | ||
injector.providers = this.providers; | ||
injector.retriever = this.retriever; | ||
return injector; | ||
} | ||
exports.SetupProviderRegistry = SetupProviderRegistry; | ||
function resolveToken(provider) { | ||
if (core_1.isClass(provider)) | ||
return provider; | ||
if (provider instanceof provider_1.TagProvider) | ||
return resolveToken(provider.provider); | ||
return provider.provide; | ||
} | ||
exports.resolveToken = resolveToken; | ||
/** | ||
* This is the actual dependency injection container. | ||
* Every module has its own injector. | ||
*/ | ||
class Injector { | ||
constructor(module, buildContext) { | ||
this.module = module; | ||
this.buildContext = buildContext; | ||
/** | ||
* All unscoped provider instances. Scoped instances are attached to `Scope`. | ||
*/ | ||
this.instances = {}; | ||
module.injector = this; | ||
this.build(buildContext); | ||
} | ||
/** | ||
* Changes the provider structure of this injector. | ||
* | ||
* Note: This is very performance sensitive. Every time you call this function a new dependency injector function | ||
* is generated, which si pretty slow. So, it's recommended to create a Injector with providers in the constructor | ||
* and not change it. | ||
*/ | ||
addProviders(...providers) { | ||
this.providers.push(...providers); | ||
this.retriever = this.buildRetriever(); | ||
static from(providers, parent) { | ||
return new Injector(new module_1.InjectorModule(providers, parent === null || parent === void 0 ? void 0 : parent.module), new BuildContext); | ||
} | ||
isRoot() { | ||
return this.parents.length === 0; | ||
static fromModule(module, parent) { | ||
return new Injector(module, new BuildContext); | ||
} | ||
createFactoryProperty(options, compiler, ofName, argPosition, notFoundFunction) { | ||
const token = options.token; | ||
if (token instanceof ConfigDefinition) { | ||
if (token.hasModule()) { | ||
const module = this.injectorContext.getModule(token.getModule().getName()); | ||
return compiler.reserveVariable('fullConfig', module.getConfig()); | ||
get(token, scope) { | ||
if (!this.resolver) | ||
throw new Error('Injector was not built'); | ||
return this.resolver(token, scope); | ||
} | ||
set(token, value, scope) { | ||
if (!this.setter) | ||
throw new Error('Injector was not built'); | ||
this.setter(token, value, scope); | ||
} | ||
clear() { | ||
this.instances = {}; | ||
} | ||
build(buildContext) { | ||
const resolverCompiler = new core_1.CompilerContext(); | ||
resolverCompiler.context.set('CircularDetector', CircularDetector); | ||
resolverCompiler.context.set('CircularDetectorResets', CircularDetectorResets); | ||
resolverCompiler.context.set('throwCircularDependency', throwCircularDependency); | ||
resolverCompiler.context.set('tokenNotfoundError', tokenNotfoundError); | ||
resolverCompiler.context.set('injector', this); | ||
const lines = []; | ||
const resets = []; | ||
const creating = []; | ||
const setterCompiler = new core_1.CompilerContext(); | ||
setterCompiler.context.set('injector', this); | ||
const setterLines = []; | ||
for (const [token, prepared] of this.module.getPreparedProviders(buildContext).entries()) { | ||
//scopes will be created first, so they are returned instead of the unscoped instance | ||
prepared.providers.sort((a, b) => { | ||
if (a.scope && !b.scope) | ||
return -1; | ||
if (!a.scope && b.scope) | ||
return +1; | ||
return 0; | ||
}); | ||
for (const provider of prepared.providers) { | ||
const scope = module_1.getScope(provider); | ||
const name = 'i' + this.buildContext.providerIndex.reserve(); | ||
creating.push(`let creating_${name} = false;`); | ||
resets.push(`creating_${name} = false;`); | ||
const accessor = scope ? 'scope.instances.' + name : 'injector.instances.' + name; | ||
setterLines.push(`case ${setterCompiler.reserveVariable('token', token)}: { | ||
${accessor} = value; | ||
break; | ||
}`); | ||
if (prepared.resolveFrom) { | ||
//its a redirect | ||
lines.push(` | ||
case token === ${resolverCompiler.reserveConst(token)}: { | ||
return ${resolverCompiler.reserveConst(prepared.resolveFrom)}.injector.resolver(${resolverCompiler.reserveConst(token)}, scope); | ||
} | ||
`); | ||
} | ||
else { | ||
//we own and instantiate the service | ||
lines.push(this.buildProvider(buildContext, resolverCompiler, name, accessor, scope, provider, prepared.modules)); | ||
} | ||
} | ||
else { | ||
return compiler.reserveVariable('fullConfig', token.getConfigOrDefaults()); | ||
} | ||
} | ||
else if (token instanceof ConfigToken) { | ||
if (token.config.hasModule()) { | ||
const module = this.injectorContext.getModule(token.config.getModule().getName()); | ||
const config = module.getConfig(); | ||
return compiler.reserveVariable(token.name, config[token.name]); | ||
const setter = setterCompiler.build(` | ||
switch (token) { | ||
${setterLines.join('\n')} | ||
} | ||
else { | ||
const config = token.config.getConfigOrDefaults(); | ||
return compiler.reserveVariable(token.name, config[token.name]); | ||
`, 'token', 'value', 'scope'); | ||
const resolver = resolverCompiler.raw(` | ||
${creating.join('\n')}; | ||
CircularDetectorResets.push(() => { | ||
${resets.join('\n')}; | ||
}); | ||
return function(token, scope) { | ||
switch (true) { | ||
${lines.join('\n')} | ||
} | ||
tokenNotfoundError(token, '${core_1.getClassName(this.module)}'); | ||
} | ||
`); | ||
this.setter = setter; | ||
this.resolver = resolver; | ||
} | ||
buildProvider(buildContext, compiler, name, accessor, scope, provider, resolveDependenciesFrom) { | ||
var _a; | ||
let transient = false; | ||
const token = provider.provide; | ||
let factory = { code: '', dependencies: 0 }; | ||
const tokenVar = compiler.reserveConst(token); | ||
if (provider_1.isValueProvider(provider)) { | ||
transient = provider.transient === true; | ||
const valueVar = compiler.reserveVariable('useValue', provider.useValue); | ||
factory.code = `${accessor} = ${valueVar};`; | ||
} | ||
else if (core_1.isClass(token) && (Object.getPrototypeOf(Object.getPrototypeOf(token)) === ConfigSlice || Object.getPrototypeOf(token) === ConfigSlice)) { | ||
const value = new token; | ||
if (!value.bag) { | ||
if (value.config.hasModule()) { | ||
const module = this.injectorContext.getModule(value.config.getModule().getName()); | ||
value.bag = module.getConfig(); | ||
else if (provider_1.isClassProvider(provider)) { | ||
transient = provider.transient === true; | ||
let useClass = provider.useClass; | ||
if (!useClass) { | ||
if (!core_1.isClass(provider.provide)) { | ||
throw new Error(`UseClassProvider needs to set either 'useClass' or 'provide' as a ClassType. Got ${provider.provide}`); | ||
} | ||
else { | ||
value.bag = value.config.getConfigOrDefaults(); | ||
} | ||
return compiler.reserveVariable('configSlice', value); | ||
useClass = provider.provide; | ||
} | ||
factory = this.createFactory(provider, accessor, compiler, useClass, resolveDependenciesFrom); | ||
} | ||
else if (token === provider_1.TagRegistry) { | ||
return compiler.reserveVariable('tagRegistry', this.tagRegistry); | ||
else if (provider_1.isExistingProvider(provider)) { | ||
transient = provider.transient === true; | ||
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(provider.useExisting)}, scope)`; | ||
} | ||
else if (core_1.isPrototypeOfBase(token, provider_1.Tag)) { | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const providers = compiler.reserveVariable('tagRegistry', this.tagRegistry.resolve(token)); | ||
return `new ${tokenVar}(${providers}.map(v => (frontInjector.retriever ? frontInjector.retriever(frontInjector, v, frontInjector) : frontInjector.get(v, frontInjector))))`; | ||
else if (provider_1.isFactoryProvider(provider)) { | ||
transient = provider.transient === true; | ||
const args = []; | ||
let i = 0; | ||
for (const dep of provider.deps || []) { | ||
let optional = false; | ||
let token = dep; | ||
if (decorator_1.isInjectDecorator(dep)) { | ||
optional = dep.options.optional; | ||
token = dep.options.token; | ||
} | ||
if (type_1.isFieldDecorator(dep)) { | ||
const propertySchema = dep.buildPropertySchema(); | ||
optional = propertySchema.isOptional; | ||
if (propertySchema.type === 'literal' || propertySchema.type === 'class') { | ||
token = propertySchema.literalValue !== undefined ? propertySchema.literalValue : propertySchema.getResolvedClassType(); | ||
} | ||
} | ||
if (!token) { | ||
throw new Error(`No token defined for dependency ${i} in 'deps' of useFactory for ${tokenLabel(provider.provide)}`); | ||
} | ||
factory.dependencies++; | ||
args.push(this.createFactoryProperty({ | ||
name: i++, | ||
token, | ||
optional, | ||
}, provider, compiler, resolveDependenciesFrom, 'useFactory', args.length, 'factoryDependencyNotFound')); | ||
} | ||
factory.code = `${accessor} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; | ||
} | ||
else { | ||
if (token === undefined) { | ||
let of = `${ofName}.${options.name}`; | ||
if (argPosition >= 0) { | ||
const argsCheck = []; | ||
for (let i = 0; i < argPosition; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
of = `${ofName}(${argsCheck.join(', ')})`; | ||
throw new Error('Invalid provider'); | ||
} | ||
const configureProvider = []; | ||
const configuredProviderCalls = (_a = resolveDependenciesFrom[0].setupProviderRegistry) === null || _a === void 0 ? void 0 : _a.get(token); | ||
configuredProviderCalls.push(...buildContext.globalSetupProviderRegistry.get(token)); | ||
if (configuredProviderCalls) { | ||
configuredProviderCalls.sort((a, b) => { | ||
return a.order - b.order; | ||
}); | ||
for (const call of configuredProviderCalls) { | ||
if (call.type === 'stop') | ||
break; | ||
if (call.type === 'call') { | ||
const args = []; | ||
const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; | ||
for (const arg of call.args) { | ||
if (arg instanceof decorator_1.InjectorReference) { | ||
const injector = arg.module ? compiler.reserveConst(arg.module) + '.injector' : 'injector'; | ||
args.push(`${injector}.resolver(${compiler.reserveConst(arg.to)}, scope)`); | ||
} | ||
else { | ||
args.push(`${compiler.reserveVariable('arg', arg)}`); | ||
} | ||
} | ||
configureProvider.push(`${accessor}.${methodName}(${args.join(', ')});`); | ||
} | ||
throw new DependenciesUnmetError(`Undefined dependency '${options.name}: undefined' of ${of}. Dependency '${options.name}' has no type. Imported reflect-metadata correctly? ` + | ||
`Use '@inject(PROVIDER) ${options.name}: T' if T is an interface. For circular references use @inject(() => T) ${options.name}: T.`); | ||
if (call.type === 'property') { | ||
const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; | ||
const value = call.value instanceof decorator_1.InjectorReference ? `frontInjector.get(${compiler.reserveVariable('forward', call.value.to)})` : compiler.reserveVariable('value', call.value); | ||
configureProvider.push(`${accessor}.${property} = ${value};`); | ||
} | ||
} | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; | ||
return `(frontInjector.retriever ? frontInjector.retriever(frontInjector, ${tokenVar}, frontInjector) : frontInjector.get(${tokenVar}, frontInjector)) ${orThrow}`; | ||
} | ||
return 'undefined'; | ||
} | ||
optionsFromProperty(property) { | ||
const options = property.data['deepkit/inject']; | ||
let token = property.resolveClassType; | ||
if (options && options.token) { | ||
token = core_1.isFunction(options.token) ? options.token() : options.token; | ||
else { | ||
configureProvider.push('//no custom provider setup'); | ||
} | ||
return { token, name: property.name, optional: !!options && options.optional }; | ||
const scopeCheck = scope ? ` && scope && scope.name === ${JSON.stringify(scope)}` : ''; | ||
//circular dependencies can happen, when for example a service with InjectorContext injected manually instantiates a service. | ||
//if that service references back to the first one, it will be a circular loop. So we track that with `creating` state. | ||
const creatingVar = `creating_${name}`; | ||
const circularDependencyCheckStart = factory.dependencies ? `if (${creatingVar}) throwCircularDependency();${creatingVar} = true;` : ''; | ||
const circularDependencyCheckEnd = factory.dependencies ? `${creatingVar} = false;` : ''; | ||
return ` | ||
//${tokenLabel(token)} | ||
case token === ${tokenVar}${scopeCheck}: { | ||
${!transient ? `if (${accessor} !== undefined) return ${accessor};` : ''} | ||
CircularDetector.push(${tokenVar}); | ||
${circularDependencyCheckStart} | ||
${factory.code} | ||
${circularDependencyCheckEnd} | ||
CircularDetector.pop(); | ||
${configureProvider.join('\n')} | ||
return ${accessor}; | ||
} | ||
`; | ||
} | ||
createFactory(compiler, classType) { | ||
createFactory(provider, resolvedName, compiler, classType, resolveDependenciesFrom) { | ||
if (!classType) | ||
@@ -307,4 +320,9 @@ throw new Error('Can not create factory for undefined ClassType'); | ||
const classTypeVar = compiler.reserveVariable('classType', classType); | ||
let dependencies = 0; | ||
for (const property of schema.getMethodProperties('constructor')) { | ||
args.push(this.createFactoryProperty(this.optionsFromProperty(property), compiler, core_1.getClassName(classType), args.length, 'constructorParameterNotFound')); | ||
if (!property) { | ||
throw new Error(`Constructor arguments hole in ${core_1.getClassName(classType)}`); | ||
} | ||
dependencies++; | ||
args.push(this.createFactoryProperty(this.optionsFromProperty(property), provider, compiler, resolveDependenciesFrom, core_1.getClassName(classType), args.length, 'constructorParameterNotFound')); | ||
} | ||
@@ -316,421 +334,177 @@ for (const property of schema.getProperties()) { | ||
continue; | ||
propertyAssignment.push(`v.${property.name} = ${this.createFactoryProperty(this.optionsFromProperty(property), compiler, core_1.getClassName(classType), -1, 'propertyParameterNotFound')};`); | ||
dependencies++; | ||
try { | ||
const resolveProperty = this.createFactoryProperty(this.optionsFromProperty(property), provider, compiler, resolveDependenciesFrom, core_1.getClassName(classType), -1, 'propertyParameterNotFound'); | ||
propertyAssignment.push(`${resolvedName}.${property.name} = ${resolveProperty};`); | ||
} | ||
catch (error) { | ||
throw new Error(`Could not resolve property injection token ${core_1.getClassName(classType)}.${property.name}: ${error.message}`); | ||
} | ||
} | ||
return `v = new ${classTypeVar}(${args.join(',')});\n${propertyAssignment.join('\n')}`; | ||
return { | ||
code: `${resolvedName} = new ${classTypeVar}(${args.join(',')});\n${propertyAssignment.join('\n')}`, | ||
dependencies | ||
}; | ||
} | ||
buildRetriever() { | ||
var _a; | ||
const compiler = new core_1.CompilerContext(); | ||
const lines = []; | ||
const resets = []; | ||
this.resolved = []; | ||
lines.push(` | ||
case ${compiler.reserveVariable('injectorContextClassType', InjectorContext)}: return injector.injectorContext; | ||
case ${compiler.reserveVariable('injectorClassType', Injector)}: return injector; | ||
`); | ||
let resolvedIds = 0; | ||
const normalizedProviders = new Map(); | ||
//make sure that providers that declare the same provider token will be filtered out so that the last will be used. | ||
for (const provider of this.providers) { | ||
if (provider instanceof provider_1.TagProvider) { | ||
normalizedProviders.set(provider, provider); | ||
createFactoryProperty(options, fromProvider, compiler, resolveDependenciesFrom, ofName, argPosition, notFoundFunction) { | ||
const token = options.token; | ||
//regarding configuration values: the attached module is not necessarily in resolveDependenciesFrom[0] | ||
//if the parent module overwrites its, then the parent module is at 0th position. | ||
if (token instanceof config_1.ConfigDefinition) { | ||
const module = module_1.findModuleForConfig(token, resolveDependenciesFrom); | ||
return compiler.reserveVariable('fullConfig', module.getConfig()); | ||
} | ||
else if (token instanceof config_1.ConfigToken) { | ||
const module = module_1.findModuleForConfig(token.config, resolveDependenciesFrom); | ||
const config = module.getConfig(); | ||
return compiler.reserveVariable(token.name, config[token.name]); | ||
} | ||
else if (core_1.isClass(token) && (Object.getPrototypeOf(Object.getPrototypeOf(token)) === config_1.ConfigSlice || Object.getPrototypeOf(token) === config_1.ConfigSlice)) { | ||
const value = new token; | ||
const module = module_1.findModuleForConfig(value.config, resolveDependenciesFrom); | ||
value.bag = module.getConfig(); | ||
return compiler.reserveVariable('configSlice', value); | ||
} | ||
else if (token === provider_1.TagRegistry) { | ||
return compiler.reserveVariable('tagRegistry', this.buildContext.tagRegistry); | ||
} | ||
else if (core_1.isPrototypeOfBase(token, provider_1.Tag)) { | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const resolvedVar = compiler.reserveVariable('tagResolved'); | ||
const entries = this.buildContext.tagRegistry.resolve(token); | ||
const args = []; | ||
for (const entry of entries) { | ||
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(entry.tagProvider.provider.provide)}, scope)`); | ||
} | ||
else if (provider_1.isValueProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (provider_1.isClassProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (provider_1.isExistingProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (provider_1.isFactoryProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (core_1.isClass(provider)) { | ||
normalizedProviders.set(provider, provider); | ||
} | ||
return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`; | ||
} | ||
for (let provider of normalizedProviders.values()) { | ||
const resolvedId = resolvedIds++; | ||
this.resolved.push(undefined); | ||
let transient = false; | ||
let factory = ''; | ||
let token; | ||
const tagToken = provider instanceof provider_1.TagProvider ? provider : undefined; | ||
if (provider instanceof provider_1.TagProvider) { | ||
provider = provider.provider; | ||
} | ||
if (provider_1.isValueProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
const valueVar = compiler.reserveVariable('useValue', provider.useValue); | ||
factory = `v = ${valueVar};`; | ||
} | ||
else if (provider_1.isClassProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
factory = this.createFactory(compiler, provider.useClass || provider.provide); | ||
} | ||
else if (provider_1.isExistingProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
factory = this.createFactory(compiler, provider.useExisting); | ||
} | ||
else if (provider_1.isFactoryProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
const args = []; | ||
let i = 0; | ||
for (const dep of provider.deps || []) { | ||
let optional = false; | ||
let token = dep; | ||
if (isInjectDecorator(dep)) { | ||
optional = dep.options.optional; | ||
token = dep.options.token; | ||
} | ||
if (!token) { | ||
throw new Error(`No token defined for dependency ${i} in 'deps' of useFactory for ${tokenLabel(provider.provide)}`); | ||
} | ||
args.push(this.createFactoryProperty({ | ||
name: i++, | ||
token, | ||
optional, | ||
}, compiler, 'useFactory', args.length, 'factoryDependencyNotFound')); | ||
else { | ||
let of = `${ofName}.${options.name}`; | ||
if (token === undefined) { | ||
if (argPosition >= 0) { | ||
const argsCheck = []; | ||
for (let i = 0; i < argPosition; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
of = `${ofName}(${argsCheck.join(', ')})`; | ||
} | ||
factory = `v = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; | ||
throw new DependenciesUnmetError(`Undefined dependency '${options.name}: undefined' of ${of}. Dependency '${options.name}' has no type. Imported reflect-metadata correctly? ` + | ||
`Use '@inject(PROVIDER) ${options.name}: T' if T is an interface. For circular references use @inject(() => T) ${options.name}: T.`); | ||
} | ||
else if (core_1.isClass(provider)) { | ||
token = provider; | ||
factory = this.createFactory(compiler, provider); | ||
} | ||
else { | ||
throw new Error('Invalid provider'); | ||
} | ||
if (tagToken) | ||
token = tagToken; | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const creatingVar = compiler.reserveVariable('creating', false); | ||
const configuredProviderCalls = (_a = this.configuredProviderRegistry) === null || _a === void 0 ? void 0 : _a.get(token); | ||
const configureProvider = []; | ||
if (configuredProviderCalls) { | ||
configuredProviderCalls.sort((a, b) => { | ||
return a.order - b.order; | ||
}); | ||
for (const call of configuredProviderCalls) { | ||
if (call.type === 'stop') | ||
break; | ||
if (call.type === 'call') { | ||
const args = []; | ||
const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; | ||
for (const arg of call.args) { | ||
if (arg instanceof InjectorReference) { | ||
args.push(`frontInjector.get(${compiler.reserveVariable('forward', arg.to)})`); | ||
} | ||
else { | ||
args.push(`${compiler.reserveVariable('arg', arg)}`); | ||
} | ||
let foundPreparedProvider = undefined; | ||
for (const module of resolveDependenciesFrom) { | ||
foundPreparedProvider = module.getPreparedProvider(token); | ||
if (foundPreparedProvider) { | ||
if (foundPreparedProvider) { | ||
//check if the found provider was actually exported to this current module. | ||
//if not it means that provider is encapsulated living only in its module and can not be accessed from other modules. | ||
const moduleHasAccessToThisProvider = foundPreparedProvider.modules.some(m => m === module); | ||
if (!moduleHasAccessToThisProvider) { | ||
foundPreparedProvider = undefined; | ||
} | ||
configureProvider.push(`v.${methodName}(${args.join(', ')});`); | ||
} | ||
if (call.type === 'property') { | ||
const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; | ||
const value = call.value instanceof InjectorReference ? `frontInjector.get(${compiler.reserveVariable('forward', call.value.to)})` : compiler.reserveVariable('value', call.value); | ||
configureProvider.push(`v.${property} = ${value};`); | ||
} | ||
} | ||
} | ||
else { | ||
configureProvider.push('//no custom provider setup'); | ||
if (!foundPreparedProvider) { | ||
//try if parents have anything | ||
const foundInModule = this.module.resolveToken(token); | ||
if (foundInModule) { | ||
foundPreparedProvider = foundInModule.getPreparedProvider(token); | ||
} | ||
} | ||
resets.push(`${creatingVar} = false;`); | ||
lines.push(` | ||
//${tokenLabel(token)} | ||
case ${tokenVar}: { | ||
${transient ? 'let v;' : `let v = injector.resolved[${resolvedId}]; if (v !== undefined) return v;`} | ||
CircularDetector.push(${tokenVar}); | ||
if (${creatingVar}) { | ||
throwCircularDependency(); | ||
} | ||
${creatingVar} = true; | ||
${factory} | ||
${transient ? '' : `injector.resolved[${resolvedId}] = v;`} | ||
${creatingVar} = false; | ||
${configureProvider.join('\n')} | ||
CircularDetector.pop(); | ||
return v; | ||
} | ||
`); | ||
if (!foundPreparedProvider && options.optional) | ||
return 'undefined'; | ||
if (!foundPreparedProvider) { | ||
throw new DependenciesUnmetError(`Unknown dependency '${options.name}: ${tokenLabel(token)}' of ${of}.`); | ||
} | ||
const allPossibleScopes = foundPreparedProvider.providers.map(module_1.getScope); | ||
const fromScope = module_1.getScope(fromProvider); | ||
const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1; | ||
if (!unscoped && !allPossibleScopes.includes(fromScope)) { | ||
throw new DependenciesUnmetError(`Dependency '${options.name}: ${tokenLabel(token)}' of ${of} can not be injected into ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` + | ||
`since ${tokenLabel(token)} only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.`); | ||
} | ||
//when the dependency is FactoryProvider it might return undefined. | ||
//in this case, if the dependency is not optional, we throw an error. | ||
const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; | ||
const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; | ||
if (resolveFromModule === this.module) { | ||
return `injector.resolver(${tokenVar}, scope)`; | ||
} | ||
return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope) ${orThrow}`; | ||
} | ||
const parents = []; | ||
for (let i = 0; i < this.parents.length; i++) { | ||
let retriever = 'retriever' in this.parents[i] ? `injector.parents[${i}].retriever(injector.parents[${i}], ` : `injector.parents[${i}].get(`; | ||
parents.push(` | ||
{ | ||
const v = ${retriever}token, frontInjector); | ||
if (v !== undefined) return v; | ||
} | ||
`); | ||
} | ||
optionsFromProperty(property) { | ||
const options = property.data['deepkit/inject']; | ||
let token = property.resolveClassType; | ||
if (options && options.token) { | ||
token = core_1.isFunction(options.token) ? options.token() : options.token; | ||
} | ||
compiler.context.set('CircularDetector', CircularDetector); | ||
compiler.context.set('throwCircularDependency', throwCircularDependency); | ||
compiler.context.set('CircularDetectorResets', CircularDetectorResets); | ||
compiler.context.set('constructorParameterNotFound', constructorParameterNotFound); | ||
compiler.context.set('factoryDependencyNotFound', factoryDependencyNotFound); | ||
compiler.context.set('propertyParameterNotFound', propertyParameterNotFound); | ||
compiler.preCode = ` | ||
CircularDetectorResets.push(() => { | ||
${resets.join('\n')}; | ||
}); | ||
`; | ||
return compiler.build(` | ||
frontInjector = frontInjector || injector; | ||
switch (token) { | ||
${lines.join('\n')} | ||
else if (property.type === 'class') { | ||
token = property.getResolvedClassType(); | ||
} | ||
${parents.join('\n')} | ||
return undefined; | ||
`, 'injector', 'token', 'frontInjector'); | ||
else if (property.type === 'literal') { | ||
token = property.literalValue; | ||
} | ||
return { token, name: property.name, optional: property.isOptional ? true : (!!options && options.optional) }; | ||
} | ||
get(token, frontInjector) { | ||
const v = this.retriever(this, token, frontInjector || this); | ||
if (v !== undefined) | ||
return v; | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new TokenNotFoundError(`Could not resolve injector token ${tokenLabel(token)}`); | ||
} | ||
} | ||
exports.Injector = Injector; | ||
function constructorParameterNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown constructor argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function factoryDependencyNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function propertyParameterNotFound(ofName, name, position, token) { | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function throwCircularDependency() { | ||
const path = CircularDetector.map(tokenLabel).join(' -> '); | ||
CircularDetector.length = 0; | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new CircularDependencyError(`Circular dependency found ${path}`); | ||
} | ||
class MemoryInjector extends Injector { | ||
constructor(providers) { | ||
super(); | ||
this.providers = providers; | ||
} | ||
fork(parents) { | ||
return this; | ||
} | ||
retriever(injector, token) { | ||
for (const p of this.providers) { | ||
if (p.provide === token) | ||
return 'useFactory' in p ? p.useFactory() : p.useValue; | ||
} | ||
} | ||
get(token, frontInjector) { | ||
const result = this.retriever(this, token); | ||
if (result === undefined) | ||
throw new TokenNotFoundError(`Could not resolve injector token ${tokenLabel(token)}`); | ||
return result; | ||
} | ||
} | ||
exports.MemoryInjector = MemoryInjector; | ||
class ContextRegistry { | ||
class BuildProviderIndex { | ||
constructor() { | ||
this.contexts = []; | ||
this.offset = 0; | ||
} | ||
get size() { | ||
return this.contexts.length; | ||
reserve() { | ||
return this.offset++; | ||
} | ||
get(id) { | ||
return this.contexts[id]; | ||
} | ||
set(id, value) { | ||
this.contexts[id] = value; | ||
} | ||
} | ||
exports.ContextRegistry = ContextRegistry; | ||
class ScopedContextScopeCaches { | ||
constructor(size) { | ||
this.size = size; | ||
this.caches = {}; | ||
} | ||
getCache(scope) { | ||
let cache = this.caches[scope]; | ||
if (!cache) { | ||
cache = new ScopedContextCache(this.size); | ||
this.caches[scope] = cache; | ||
} | ||
return cache; | ||
} | ||
} | ||
exports.ScopedContextScopeCaches = ScopedContextScopeCaches; | ||
class ScopedContextCache { | ||
constructor(size) { | ||
this.size = size; | ||
this.injectors = new Array(this.size); | ||
} | ||
get(contextId) { | ||
return this.injectors[contextId]; | ||
} | ||
set(contextId, injector) { | ||
this.injectors[contextId] = injector; | ||
} | ||
} | ||
exports.ScopedContextCache = ScopedContextCache; | ||
class Context { | ||
constructor(module, id, parent) { | ||
this.module = module; | ||
this.id = id; | ||
this.parent = parent; | ||
this.providers = []; | ||
} | ||
} | ||
exports.Context = Context; | ||
class ConfiguredProviderRegistry { | ||
class BuildContext { | ||
constructor() { | ||
this.calls = new Map(); | ||
this.id = BuildContext.ids++; | ||
this.tagRegistry = new provider_1.TagRegistry; | ||
this.providerIndex = new BuildProviderIndex; | ||
/** | ||
* In the process of preparing providers, each module redirects their | ||
* global setup calls in this registry. | ||
*/ | ||
this.globalSetupProviderRegistry = new SetupProviderRegistry; | ||
} | ||
add(token, ...newCalls) { | ||
this.get(token).push(...newCalls); | ||
} | ||
get(token) { | ||
let calls = this.calls.get(token); | ||
if (!calls) { | ||
calls = []; | ||
this.calls.set(token, calls); | ||
} | ||
return calls; | ||
} | ||
clone() { | ||
const c = new ConfiguredProviderRegistry; | ||
for (const [token, calls] of this.calls.entries()) { | ||
c.calls.set(token, calls.slice()); | ||
} | ||
return c; | ||
} | ||
} | ||
exports.ConfiguredProviderRegistry = ConfiguredProviderRegistry; | ||
exports.BuildContext = BuildContext; | ||
BuildContext.ids = 0; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* A InjectorContext is responsible for taking a root InjectorModule and build all Injectors. | ||
* | ||
* It also can create scopes aka a sub InjectorContext with providers from a particular scope. | ||
*/ | ||
function setupProvider(classTypeOrToken, registry, order) { | ||
const proxy = new Proxy({}, { | ||
get(target, prop) { | ||
return (...args) => { | ||
registry.add(classTypeOrToken, { type: 'call', methodName: prop, args: args, order }); | ||
return proxy; | ||
}; | ||
}, | ||
set(target, prop, value) { | ||
registry.add(classTypeOrToken, { type: 'property', property: prop, value: value, order }); | ||
return true; | ||
} | ||
}); | ||
return proxy; | ||
} | ||
exports.setupProvider = setupProvider; | ||
class InjectorContext { | ||
constructor(contextManager = new ContextRegistry, scope = 'module', configuredProviderRegistry = new ConfiguredProviderRegistry, parent = undefined, additionalInjectorParent = undefined, modules = {}, scopeCaches, tagRegistry = new provider_1.TagRegistry()) { | ||
this.contextManager = contextManager; | ||
constructor(rootModule, scope, buildContext = new BuildContext) { | ||
this.rootModule = rootModule; | ||
this.scope = scope; | ||
this.configuredProviderRegistry = configuredProviderRegistry; | ||
this.parent = parent; | ||
this.additionalInjectorParent = additionalInjectorParent; | ||
this.modules = modules; | ||
this.tagRegistry = tagRegistry; | ||
this.injectors = new Array(this.contextManager.contexts.length); | ||
this.scopeCaches = scopeCaches || new ScopedContextScopeCaches(this.contextManager.size); | ||
this.cache = this.scopeCaches.getCache(this.scope); | ||
this.buildContext = buildContext; | ||
} | ||
getModule(name) { | ||
if (!this.modules[name]) | ||
throw new Error(`No Module with name ${name} registered`); | ||
return this.modules[name]; | ||
get(token, module) { | ||
return this.getInjector(module || this.rootModule).get(token, this.scope); | ||
} | ||
registerModule(module, config) { | ||
if (this.modules[module.getName()]) | ||
throw new Error(`Module ${module.getName()} already registered`); | ||
if (config) | ||
config.setModule(module); | ||
this.modules[module.getName()] = module; | ||
for (const [provider, calls] of module.getConfiguredProviderRegistry().calls) { | ||
this.configuredProviderRegistry.add(provider, ...calls); | ||
} | ||
set(token, value, module) { | ||
return this.getInjector(module || this.rootModule).set(token, value, this.scope); | ||
} | ||
static forProviders(providers) { | ||
return new InjectorContext(new module_1.InjectorModule(providers)); | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Returns the unscoped injector. Use `.get(T, Scope)` for resolving scoped token. | ||
*/ | ||
setupProvider(classTypeOrToken, order = 0) { | ||
return setupProvider(classTypeOrToken, this.configuredProviderRegistry, order); | ||
getInjector(module) { | ||
return module.getOrCreateInjector(this.buildContext); | ||
} | ||
getModuleNames() { | ||
return Object.keys(this.modules); | ||
getRootInjector() { | ||
return this.getInjector(this.rootModule); | ||
} | ||
static forProviders(providers) { | ||
const registry = new ContextRegistry(); | ||
const context = new Context(new module_1.InjectorModule('', {}), 0); | ||
registry.set(0, context); | ||
context.providers.push(...providers); | ||
return new InjectorContext(registry); | ||
createChildScope(scope) { | ||
return new InjectorContext(this.rootModule, { name: scope, instances: {} }, this.buildContext); | ||
} | ||
getInjectorForModule(module) { | ||
return this.getInjector(module.contextId); | ||
} | ||
getInjector(contextId) { | ||
let injector = this.injectors[contextId]; | ||
if (injector) | ||
return injector; | ||
const parents = []; | ||
parents.push(this.parent ? this.parent.getInjector(contextId) : new Injector()); | ||
if (this.additionalInjectorParent) | ||
parents.push(this.additionalInjectorParent.fork(undefined, this)); | ||
const context = this.contextManager.get(contextId); | ||
if (context.parent) | ||
parents.push(this.getInjector(context.parent.id)); | ||
injector = this.cache.get(contextId); | ||
if (injector) { | ||
//we have one from cache. Clear it, and return | ||
injector = injector.fork(parents, this); | ||
return this.injectors[contextId] = injector; | ||
} | ||
const providers = provider_1.getProviders(context.providers, this.scope); | ||
injector = new Injector(providers, parents, this, this.configuredProviderRegistry, this.tagRegistry); | ||
this.injectors[contextId] = injector; | ||
this.cache.set(contextId, injector); | ||
return injector; | ||
} | ||
get(token, frontInjector) { | ||
const injector = this.getInjector(0); | ||
return injector.get(token, frontInjector); | ||
} | ||
createChildScope(scope, additionalInjectorParent) { | ||
return new InjectorContext(this.contextManager, scope, this.configuredProviderRegistry, this, additionalInjectorParent, this.modules, this.scopeCaches, this.tagRegistry); | ||
} | ||
} | ||
exports.InjectorContext = InjectorContext; | ||
//# sourceMappingURL=injector.js.map |
@@ -0,20 +1,137 @@ | ||
import { ConfigDefinition } from './config'; | ||
import { NormalizedProvider, ProviderWithScope, Token } from './provider'; | ||
import { ClassType } from '@deepkit/core'; | ||
import { ConfiguredProviderRegistry, ConfigureProvider } from './injector'; | ||
export declare class InjectorModule<N extends string = string, C extends { | ||
import { InjectorToken } from './decorator'; | ||
import { BuildContext, Injector, SetupProviderRegistry } from './injector'; | ||
export declare type ConfigureProvider<T> = { | ||
[name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name]; | ||
}; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
*/ | ||
export declare function setupProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, registry: SetupProviderRegistry, order: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
export interface PreparedProvider { | ||
/** | ||
* The modules from which dependencies can be resolved. The first item is always the module from which this provider was declared. | ||
* | ||
* This is per default the module in which the provider was declared, | ||
* but if the provider was moved (by exporting), then if | ||
* a) the parent had this provider already, then this array has additionally the one from which the provider was exported. | ||
* b) the parent had no provider of that token, then this array is just the module from which the provider was exported. | ||
* | ||
* This is important otherwise exported provider won't have access in their dependencies to their original (encapsulated) injector. | ||
*/ | ||
modules: InjectorModule[]; | ||
/** | ||
* A token can have multiple providers, for each scope its own entry. | ||
* Each scoped provider can only exist once. | ||
*/ | ||
providers: NormalizedProvider[]; | ||
/** | ||
* When this provider was exported to another module and thus is actually instantiated in another module, then this is set. | ||
* This is necessary to tell the module who declared this provider to not instantiate it, but redirects resolve requests | ||
* to `resolveFrom` instead. | ||
*/ | ||
resolveFrom?: InjectorModule; | ||
} | ||
export declare function findModuleForConfig(config: ConfigDefinition<any>, modules: InjectorModule[]): InjectorModule; | ||
export declare type ExportType = ClassType | InjectorToken<any> | string | InjectorModule; | ||
export declare function isProvided(providers: ProviderWithScope[], token: any): boolean; | ||
export declare function getScope(provider: ProviderWithScope): string; | ||
export declare class InjectorModule<C extends { | ||
[name: string]: any; | ||
} = any> { | ||
name: N; | ||
} = any, IMPORT = InjectorModule<any, any>> { | ||
providers: ProviderWithScope[]; | ||
parent?: InjectorModule<any, InjectorModule<any, any>> | undefined; | ||
config: C; | ||
contextId: number; | ||
protected setupProviderRegistry: ConfiguredProviderRegistry; | ||
constructor(name: N, config: C); | ||
getName(): N; | ||
exports: ExportType[]; | ||
id: number; | ||
/** | ||
* Whether this module is for the root module. All its providers are automatically exported and moved to the root level. | ||
*/ | ||
root: boolean; | ||
/** | ||
* The built injector. This is set once a Injector for this module has been created. | ||
*/ | ||
injector?: Injector; | ||
setupProviderRegistry: SetupProviderRegistry; | ||
globalSetupProviderRegistry: SetupProviderRegistry; | ||
imports: InjectorModule[]; | ||
/** | ||
* The first stage of building the injector is to resolve all providers and exports. | ||
* Then the actual injector functions can be built. | ||
*/ | ||
protected processed: boolean; | ||
protected exportsDisabled: boolean; | ||
configDefinition?: ConfigDefinition<any>; | ||
constructor(providers?: ProviderWithScope[], parent?: InjectorModule<any, InjectorModule<any, any>> | undefined, config?: C, exports?: ExportType[]); | ||
registerAsChildren(child: InjectorModule): void; | ||
/** | ||
* When the module exports providers the importer don't want to have then `disableExports` disable all exports. | ||
*/ | ||
disableExports(): this; | ||
/** | ||
* Makes all the providers, controllers, etc available at the root module, basically exporting everything. | ||
*/ | ||
forRoot(): this; | ||
/** | ||
* Reverts the root default setting to false. | ||
*/ | ||
notForRoot(): this; | ||
unregisterAsChildren(child: InjectorModule): void; | ||
getChildren(): InjectorModule[]; | ||
setConfigDefinition(config: ConfigDefinition<any>): this; | ||
setParent(parent: InjectorModule): this; | ||
getParent(): InjectorModule | undefined; | ||
protected assertInjectorNotBuilt(): void; | ||
addExport(...controller: ClassType[]): this; | ||
isProvided(classType: ClassType): boolean; | ||
addProvider(...provider: ProviderWithScope[]): this; | ||
getProviders(): ProviderWithScope[]; | ||
getConfig(): C; | ||
setConfig(config: C): void; | ||
getConfiguredProviderRegistry(): ConfiguredProviderRegistry; | ||
configure(config: Partial<C>): this; | ||
getImports(): InjectorModule[]; | ||
getImportedModulesByClass<T extends InjectorModule>(classType: ClassType<T>): T[]; | ||
getImportedModuleByClass<T extends InjectorModule>(classType: ClassType<T>): T; | ||
getImportedModule<T extends InjectorModule>(module: T): T; | ||
getExports(): ExportType[]; | ||
hasImport<T extends InjectorModule>(moduleClass: ClassType<T>): boolean; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Modifies this module and adds a new import, returning the same module. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
addImport(...modules: InjectorModule<any>[]): this; | ||
/** | ||
* Allows to register additional setup calls for a provider in this module. | ||
* The injector token needs to be available in the local module providers. | ||
* Use setupGlobalProvider to register globally setup calls (not limited to this module only). | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
/** | ||
* Allows to register additional setup calls for a provider in the whole module tree. | ||
* The injector token needs to be available in the local module providers. | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupGlobalProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
getOrCreateInjector(buildContext: BuildContext): Injector; | ||
protected preparedProviders?: Map<any, PreparedProvider>; | ||
getPreparedProvider(token: any): PreparedProvider | undefined; | ||
resolveToken(token: any): InjectorModule | undefined; | ||
/** | ||
* Prepared the module for a injector tree build. | ||
* | ||
* - Index providers by token so that last known provider is picked (so they can be overwritten). | ||
* - Register TagProvider in TagRegistry | ||
* - Put TagProvider in providers if not already made. | ||
* - Put exports to parent's module with the reference to this, so the dependencies are fetched from the correct module. | ||
*/ | ||
getPreparedProviders(buildContext: BuildContext): Map<any, PreparedProvider>; | ||
protected exported: boolean; | ||
protected handleExports(buildContext: BuildContext): void; | ||
findRoot(): InjectorModule; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InjectorModule = void 0; | ||
exports.InjectorModule = exports.getScope = exports.isProvided = exports.findModuleForConfig = exports.setupProvider = void 0; | ||
const provider_1 = require("./provider"); | ||
const core_1 = require("@deepkit/core"); | ||
const injector_1 = require("./injector"); | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
*/ | ||
function setupProvider(classTypeOrToken, registry, order) { | ||
const proxy = new Proxy({}, { | ||
get(target, prop) { | ||
return (...args) => { | ||
registry.add(classTypeOrToken, { type: 'call', methodName: prop, args: args, order }); | ||
return proxy; | ||
}; | ||
}, | ||
set(target, prop, value) { | ||
registry.add(classTypeOrToken, { type: 'property', property: prop, value: value, order }); | ||
return true; | ||
} | ||
}); | ||
return proxy; | ||
} | ||
exports.setupProvider = setupProvider; | ||
let moduleIds = 0; | ||
function registerPreparedProvider(map, modules, providers) { | ||
const token = providers[0].provide; | ||
const preparedProvider = map.get(token); | ||
if (preparedProvider) { | ||
for (const provider of providers) { | ||
const scope = getScope(provider); | ||
//check if given provider has a unknown scope, if so set it. | ||
//if the scope is known, overwrite it (we want always the last known provider to be valid) | ||
const knownProvider = preparedProvider.providers.findIndex(v => getScope(v) === scope); | ||
if (knownProvider === -1) { | ||
//scope not known, add it | ||
preparedProvider.providers.push(provider); | ||
} | ||
else { | ||
//scope already known, replace it | ||
preparedProvider.providers.splice(knownProvider, 1, provider); | ||
} | ||
} | ||
} | ||
else { | ||
//just add it | ||
map.set(token, { modules, providers: providers.slice(0) }); | ||
} | ||
} | ||
function findModuleForConfig(config, modules) { | ||
for (const m of modules) { | ||
if (m.configDefinition === config) | ||
return m; | ||
} | ||
throw new Error(`No module found for configuration ${config.schema.toString()}. Did you attach it to a module?`); | ||
} | ||
exports.findModuleForConfig = findModuleForConfig; | ||
function isProvided(providers, token) { | ||
return providers.find(v => !(v instanceof provider_1.TagProvider) ? token === (core_1.isClass(v) ? v : v.provide) : false) !== undefined; | ||
} | ||
exports.isProvided = isProvided; | ||
function getScope(provider) { | ||
return (core_1.isClass(provider) ? '' : provider instanceof provider_1.TagProvider ? provider.provider.scope : provider.scope) || ''; | ||
} | ||
exports.getScope = getScope; | ||
class InjectorModule { | ||
constructor(name, config) { | ||
this.name = name; | ||
constructor(providers = [], parent, config = {}, exports = []) { | ||
this.providers = providers; | ||
this.parent = parent; | ||
this.config = config; | ||
this.contextId = 0; | ||
this.setupProviderRegistry = new injector_1.ConfiguredProviderRegistry; | ||
this.exports = exports; | ||
this.id = moduleIds++; | ||
/** | ||
* Whether this module is for the root module. All its providers are automatically exported and moved to the root level. | ||
*/ | ||
this.root = false; | ||
this.setupProviderRegistry = new injector_1.SetupProviderRegistry; | ||
this.globalSetupProviderRegistry = new injector_1.SetupProviderRegistry; | ||
this.imports = []; | ||
/** | ||
* The first stage of building the injector is to resolve all providers and exports. | ||
* Then the actual injector functions can be built. | ||
*/ | ||
this.processed = false; | ||
this.exportsDisabled = false; | ||
this.exported = false; | ||
if (this.parent) | ||
this.parent.registerAsChildren(this); | ||
} | ||
getName() { | ||
return this.name; | ||
registerAsChildren(child) { | ||
if (this.imports.includes(child)) | ||
return; | ||
this.imports.push(child); | ||
} | ||
/** | ||
* When the module exports providers the importer don't want to have then `disableExports` disable all exports. | ||
*/ | ||
disableExports() { | ||
this.exportsDisabled = true; | ||
return this; | ||
} | ||
/** | ||
* Makes all the providers, controllers, etc available at the root module, basically exporting everything. | ||
*/ | ||
forRoot() { | ||
this.root = true; | ||
return this; | ||
} | ||
/** | ||
* Reverts the root default setting to false. | ||
*/ | ||
notForRoot() { | ||
this.root = false; | ||
return this; | ||
} | ||
unregisterAsChildren(child) { | ||
if (!this.imports.includes(child)) | ||
return; | ||
child.parent = undefined; | ||
core_1.arrayRemoveItem(this.imports, child); | ||
} | ||
getChildren() { | ||
return this.imports; | ||
} | ||
setConfigDefinition(config) { | ||
this.configDefinition = config; | ||
return this; | ||
} | ||
setParent(parent) { | ||
if (this.parent === parent) | ||
return this; | ||
this.assertInjectorNotBuilt(); | ||
if (this.parent) | ||
this.parent.unregisterAsChildren(this); | ||
this.parent = parent; | ||
this.parent.registerAsChildren(this); | ||
return this; | ||
} | ||
getParent() { | ||
return this.parent; | ||
} | ||
assertInjectorNotBuilt() { | ||
if (!this.injector) | ||
return; | ||
throw new Error(`Injector already built for ${core_1.getClassName(this)}. Can not modify its provider or tree structure.`); | ||
} | ||
addExport(...controller) { | ||
this.assertInjectorNotBuilt(); | ||
this.exports.push(...controller); | ||
return this; | ||
} | ||
isProvided(classType) { | ||
return isProvided(this.getProviders(), classType); | ||
} | ||
addProvider(...provider) { | ||
this.assertInjectorNotBuilt(); | ||
this.providers.push(...provider); | ||
return this; | ||
} | ||
getProviders() { | ||
return this.providers; | ||
} | ||
getConfig() { | ||
return this.config; | ||
} | ||
setConfig(config) { | ||
configure(config) { | ||
Object.assign(this.config, config); | ||
return this; | ||
} | ||
getConfiguredProviderRegistry() { | ||
return this.setupProviderRegistry; | ||
getImports() { | ||
return this.imports; | ||
} | ||
getImportedModulesByClass(classType) { | ||
return this.getImports().filter(v => v instanceof classType); | ||
} | ||
getImportedModuleByClass(classType) { | ||
const v = this.getImports().find(v => v instanceof classType); | ||
if (!v) { | ||
throw new Error(`No module ${core_1.getClassName(classType)} in ${core_1.getClassName(this)}#${this.id} imported.`); | ||
} | ||
return v; | ||
} | ||
getImportedModule(module) { | ||
const v = this.getImports().find(v => v.id === module.id); | ||
if (!v) { | ||
throw new Error(`No module ${core_1.getClassName(module)}#${module.id} in ${core_1.getClassName(this)}#${this.id} imported.`); | ||
} | ||
return v; | ||
} | ||
getExports() { | ||
return this.exports; | ||
} | ||
hasImport(moduleClass) { | ||
for (const importModule of this.getImports()) { | ||
if (importModule instanceof moduleClass) | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Modifies this module and adds a new import, returning the same module. | ||
*/ | ||
addImport(...modules) { | ||
this.assertInjectorNotBuilt(); | ||
for (const module of modules) { | ||
module.setParent(this); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Allows to register additional setup calls for a provider in this module. | ||
* The injector token needs to be available in the local module providers. | ||
* Use setupGlobalProvider to register globally setup calls (not limited to this module only). | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupProvider(classTypeOrToken, order = 0) { | ||
return injector_1.setupProvider(classTypeOrToken, this.setupProviderRegistry, order); | ||
return setupProvider(classTypeOrToken, this.setupProviderRegistry, order); | ||
} | ||
/** | ||
* Allows to register additional setup calls for a provider in the whole module tree. | ||
* The injector token needs to be available in the local module providers. | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupGlobalProvider(classTypeOrToken, order = 0) { | ||
return setupProvider(classTypeOrToken, this.globalSetupProviderRegistry, order); | ||
} | ||
getOrCreateInjector(buildContext) { | ||
if (this.injector) | ||
return this.injector; | ||
//notify everyone we know to prepare providers | ||
if (this.parent) | ||
this.parent.getPreparedProviders(buildContext); | ||
this.getPreparedProviders(buildContext); | ||
//handle exports, from bottom to up | ||
if (this.parent) | ||
this.parent.handleExports(buildContext); | ||
this.handleExports(buildContext); | ||
//build the injector context | ||
if (this.parent) | ||
this.parent.getOrCreateInjector(buildContext); | ||
this.injector = new injector_1.Injector(this, buildContext); | ||
for (const child of this.imports) | ||
child.getOrCreateInjector(buildContext); | ||
return this.injector; | ||
} | ||
getPreparedProvider(token) { | ||
if (!this.preparedProviders) | ||
return; | ||
return this.preparedProviders.get(token); | ||
} | ||
resolveToken(token) { | ||
if (!this.preparedProviders) | ||
return; | ||
if (this.preparedProviders.has(token)) | ||
return this; | ||
if (this.parent) | ||
return this.parent.resolveToken(token); | ||
return; | ||
} | ||
/** | ||
* Prepared the module for a injector tree build. | ||
* | ||
* - Index providers by token so that last known provider is picked (so they can be overwritten). | ||
* - Register TagProvider in TagRegistry | ||
* - Put TagProvider in providers if not already made. | ||
* - Put exports to parent's module with the reference to this, so the dependencies are fetched from the correct module. | ||
*/ | ||
getPreparedProviders(buildContext) { | ||
if (this.preparedProviders) | ||
return this.preparedProviders; | ||
for (const m of this.imports) { | ||
m.getPreparedProviders(buildContext); | ||
} | ||
this.preparedProviders = new Map(); | ||
this.globalSetupProviderRegistry.mergeInto(buildContext.globalSetupProviderRegistry); | ||
//make sure that providers that declare the same provider token will be filtered out so that the last will be used. | ||
for (const provider of this.providers) { | ||
if (provider instanceof provider_1.TagProvider) { | ||
buildContext.tagRegistry.register(provider, this); | ||
if (!this.preparedProviders.has(provider.provider.provide)) { | ||
//we dont want to overwrite that provider with a tag | ||
registerPreparedProvider(this.preparedProviders, [this], [provider.provider]); | ||
} | ||
} | ||
else if (core_1.isClass(provider)) { | ||
registerPreparedProvider(this.preparedProviders, [this], [{ provide: provider }]); | ||
} | ||
else { | ||
registerPreparedProvider(this.preparedProviders, [this], [provider]); | ||
} | ||
} | ||
return this.preparedProviders; | ||
} | ||
handleExports(buildContext) { | ||
if (this.exported) | ||
return; | ||
this.exported = true; | ||
//the import order is important. the last entry is the most important and should be able to overwrite | ||
//previous modules. In order to make that work, we call handleExports in reversed order. | ||
//this lets providers from the last import register their provider first, and make them available first | ||
//in the injector (which equals to be resolved first). | ||
for (let i = this.imports.length - 1; i >= 0; i--) { | ||
this.imports[i].setParent(this); | ||
this.imports[i].handleExports(buildContext); | ||
} | ||
// for (const m of this.imports) { | ||
// m.setParent(this); | ||
// m.handleExports(buildContext); | ||
// } | ||
if (!this.preparedProviders) | ||
return; | ||
if (!this.parent) | ||
return; | ||
if (this.exportsDisabled) | ||
return; | ||
const exportToken = (token, to) => { | ||
if (!this.preparedProviders) | ||
return; | ||
const preparedProvider = this.preparedProviders.get(token); | ||
//if it was not in provider, we continue | ||
if (!preparedProvider) | ||
return; | ||
//mark this provider as redirect to `exportTo` | ||
preparedProvider.resolveFrom = to; | ||
const parentProviders = to.getPreparedProviders(buildContext); | ||
const parentProvider = parentProviders.get(token); | ||
//if the parent has this token already defined, we just switch its module to ours, | ||
//so its able to inject our encapsulated services. | ||
if (parentProvider) { | ||
//we add our module as additional source for potential dependencies | ||
parentProvider.modules.push(this); | ||
} | ||
else { | ||
parentProviders.set(token, { modules: [this], providers: preparedProvider.providers.slice() }); | ||
} | ||
}; | ||
if (this.root) { | ||
const root = this.findRoot(); | ||
if (root !== this) { | ||
for (const token of this.preparedProviders.keys()) { | ||
exportToken(token, root); | ||
} | ||
} | ||
} | ||
else { | ||
for (const entry of this.exports) { | ||
if ((core_1.isClass(entry) && core_1.isPrototypeOfBase(entry, InjectorModule)) || entry instanceof InjectorModule) { | ||
const moduleInstance = core_1.isClass(entry) ? this.imports.find(v => v instanceof entry) : entry; | ||
if (!moduleInstance) { | ||
throw new Error(`Unknown module ${core_1.getClassName(entry)} exported from ${core_1.getClassName(this)}. The module was never imported.`); | ||
} | ||
//export everything to the parent that we received from that `entry` module | ||
for (const [token, preparedProvider] of this.preparedProviders.entries()) { | ||
if (preparedProvider.modules.includes(moduleInstance)) { | ||
//this provider was received from `entry` | ||
//mark this provider as redirect to `exportTo` | ||
preparedProvider.resolveFrom = this.parent; | ||
const parentProviders = this.parent.getPreparedProviders(buildContext); | ||
const parentProvider = parentProviders.get(token); | ||
//if the parent has this token already defined, we just switch its module to ours, | ||
//so its able to inject our encapsulated services. | ||
if (parentProvider) { | ||
//we add our module as additional source for potential dependencies | ||
parentProvider.modules.push(this); | ||
} | ||
else { | ||
parentProviders.set(token, { modules: [this, ...preparedProvider.modules], providers: preparedProvider.providers.slice() }); | ||
} | ||
} | ||
} | ||
} | ||
else { | ||
//export single token | ||
exportToken(entry, this.parent); | ||
} | ||
} | ||
} | ||
} | ||
findRoot() { | ||
if (this.parent) | ||
return this.parent.findRoot(); | ||
return this; | ||
} | ||
} | ||
exports.InjectorModule = InjectorModule; | ||
//# sourceMappingURL=module.js.map |
@@ -1,2 +0,4 @@ | ||
import { ClassType } from '@deepkit/core'; | ||
import { AbstractClassType, ClassType } from '@deepkit/core'; | ||
import { InjectorToken } from './decorator'; | ||
import { InjectorModule } from './module'; | ||
export interface ProviderBase { | ||
@@ -9,7 +11,8 @@ /** | ||
} | ||
export declare type Token<T = any> = symbol | string | InjectorToken<T> | AbstractClassType<T>; | ||
export interface ValueProvider<T> extends ProviderBase { | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -22,5 +25,5 @@ * The value to inject. | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -33,5 +36,5 @@ * Class to instantiate for the `token`. | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -44,5 +47,5 @@ * Existing `token` to return. (equivalent to `injector.get(useExisting)`) | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -61,6 +64,11 @@ * A function to invoke to create a value for this `token`. The function is invoked with | ||
export declare type ProviderProvide<T = any> = ValueProvider<T> | ClassProvider<T> | ExistingProvider<T> | FactoryProvider<T>; | ||
interface TagRegistryEntry<T> { | ||
tagProvider: TagProvider<T>; | ||
module: InjectorModule; | ||
} | ||
export declare class TagRegistry { | ||
tags: TagProvider<any>[]; | ||
constructor(tags?: TagProvider<any>[]); | ||
resolve<T extends ClassType<Tag<any>>>(tag: T): TagProvider<InstanceType<T>>[]; | ||
tags: TagRegistryEntry<any>[]; | ||
constructor(tags?: TagRegistryEntry<any>[]); | ||
register(tagProvider: TagProvider<any>, module: InjectorModule): number; | ||
resolve<T extends ClassType<Tag<any>>>(tag: T): TagRegistryEntry<InstanceType<T>>[]; | ||
} | ||
@@ -91,2 +99,4 @@ export declare class TagProvider<T> { | ||
export declare function isInjectionProvider(obj: any): obj is Provider<any>; | ||
export declare function isTransient(provider: ProviderWithScope): boolean; | ||
export declare function getProviders(providers: ProviderWithScope[], requestScope: 'module' | 'session' | 'request' | string): Provider<any>[]; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getProviders = exports.isInjectionProvider = exports.isFactoryProvider = exports.isExistingProvider = exports.isClassProvider = exports.isValueProvider = exports.isScopedProvider = exports.Tag = exports.TagProvider = exports.TagRegistry = void 0; | ||
exports.getProviders = exports.isTransient = exports.isInjectionProvider = exports.isFactoryProvider = exports.isExistingProvider = exports.isClassProvider = exports.isValueProvider = exports.isScopedProvider = exports.Tag = exports.TagProvider = exports.TagRegistry = void 0; | ||
/* | ||
@@ -18,4 +18,7 @@ * Deepkit Framework | ||
} | ||
register(tagProvider, module) { | ||
return this.tags.push({ tagProvider, module }); | ||
} | ||
resolve(tag) { | ||
return this.tags.filter(v => v.tag instanceof tag); | ||
return this.tags.filter(v => v.tagProvider.tag instanceof tag); | ||
} | ||
@@ -71,2 +74,10 @@ } | ||
exports.isInjectionProvider = isInjectionProvider; | ||
function isTransient(provider) { | ||
if (core_1.isClass(provider)) | ||
return false; | ||
if (provider instanceof TagProvider) | ||
return false; | ||
return provider.transient === true; | ||
} | ||
exports.isTransient = isTransient; | ||
function getProviders(providers, requestScope) { | ||
@@ -73,0 +84,0 @@ const result = []; |
@@ -15,2 +15,3 @@ "use strict"; | ||
const injector_1 = require("../src/injector"); | ||
const injector_context_1 = require("../src/injector-context"); | ||
globals_1.test('context fork', () => { | ||
@@ -34,3 +35,3 @@ let databaseConstructed = 0; | ||
], Connection); | ||
const context = injector_1.InjectorContext.forProviders([ | ||
const context = injector_context_1.InjectorContext.forProviders([ | ||
Database, | ||
@@ -70,3 +71,3 @@ { provide: Connection, scope: 'http' }, | ||
} | ||
const context = injector_1.InjectorContext.forProviders([MyService, { provide: MySubService, scope: 'rpc' }]); | ||
const context = injector_context_1.InjectorContext.forProviders([MyService, { provide: MySubService, scope: 'rpc' }]); | ||
context.configuredProviderRegistry.add(MyService, { type: 'call', methodName: 'set', args: ['foo'], order: 0 }); | ||
@@ -73,0 +74,0 @@ context.configuredProviderRegistry.add(MySubService, { type: 'call', methodName: 'set', args: ['foo'], order: 0 }); |
@@ -16,6 +16,8 @@ "use strict"; | ||
exports.a = void 0; | ||
require("reflect-metadata"); | ||
const globals_1 = require("@jest/globals"); | ||
const type_1 = require("@deepkit/type"); | ||
require("reflect-metadata"); | ||
const injector_1 = require("../src/injector"); | ||
const decorator_1 = require("../src/decorator"); | ||
const config_1 = require("../src/config"); | ||
const module_1 = require("../src/module"); | ||
@@ -33,6 +35,6 @@ exports.a = 'asd'; | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
const injector = new injector_1.Injector([MyServer, Connection]); | ||
const injector = injector_1.Injector.from([MyServer, Connection]); | ||
globals_1.expect(injector.get(Connection)).toBeInstanceOf(Connection); | ||
@@ -54,7 +56,6 @@ globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection, Missing]) | ||
], MyServer); | ||
const injector = new injector_1.Injector([MyServer, Connection]); | ||
globals_1.expect(() => injector.get(MyServer)).toThrow(`Unknown constructor argument 'missing: Missing' of MyServer(✓, ?). Make sure 'Missing' is provided.`); | ||
globals_1.expect(() => injector_1.Injector.from([MyServer, Connection])).toThrow(`Unknown dependency 'missing: Missing' of MyServer.missing`); | ||
}); | ||
@@ -72,6 +73,6 @@ globals_1.test('wrong dep 1', () => { | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection, Object]) | ||
], MyServer); | ||
globals_1.expect(() => new injector_1.Injector([MyServer, Connection])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(✓, ?).`); | ||
globals_1.expect(() => injector_1.Injector.from([MyServer, Connection])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(✓, ?).`); | ||
}); | ||
@@ -85,6 +86,6 @@ globals_1.test('wrong dep 2', () => { | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Object]) | ||
], MyServer); | ||
globals_1.expect(() => new injector_1.Injector([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(?).`); | ||
globals_1.expect(() => injector_1.Injector.from([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(?).`); | ||
}); | ||
@@ -95,9 +96,9 @@ globals_1.test('wrong dep 3', () => { | ||
__decorate([ | ||
injector_1.inject(), | ||
decorator_1.inject(), | ||
__metadata("design:type", Object) | ||
], MyServer.prototype, "missing", void 0); | ||
MyServer = __decorate([ | ||
injector_1.injectable() | ||
decorator_1.injectable | ||
], MyServer); | ||
globals_1.expect(() => new injector_1.Injector([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer.missing.`); | ||
globals_1.expect(() => injector_1.Injector.from([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer.missing.`); | ||
}); | ||
@@ -112,7 +113,7 @@ globals_1.test('injector key', () => { | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject('foo')), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject('foo')), | ||
__metadata("design:paramtypes", [String]) | ||
], MyServer); | ||
const injector = new injector_1.Injector([MyServer, { provide: 'foo', useValue: 'bar' }]); | ||
const injector = injector_1.Injector.from([MyServer, { provide: 'foo', useValue: 'bar' }]); | ||
globals_1.expect(injector.get('foo')).toBe('bar'); | ||
@@ -131,6 +132,6 @@ globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
const injector = new injector_1.Injector([MyServer, { provide: Connection, transient: true }]); | ||
const injector = injector_1.Injector.from([MyServer, { provide: Connection, transient: true }]); | ||
const c1 = injector.get(Connection); | ||
@@ -161,11 +162,11 @@ const c2 = injector.get(Connection); | ||
__decorate([ | ||
injector_1.inject(), | ||
decorator_1.inject(), | ||
__metadata("design:type", Connection) | ||
], MyServer.prototype, "connection", void 0); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject('name')), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject('name')), | ||
__metadata("design:paramtypes", [String]) | ||
], MyServer); | ||
const injector = new injector_1.Injector([MyServer, Connection, { provide: 'name', useValue: 'peter' }]); | ||
const injector = injector_1.Injector.from([MyServer, Connection, { provide: 'name', useValue: 'peter' }]); | ||
const s = injector.get(MyServer); | ||
@@ -187,8 +188,8 @@ globals_1.expect(s.connection).toBeInstanceOf(Connection); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject(Connection2)), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject(Connection2)), | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new injector_1.Injector([MyServer, Connection, Connection2]); | ||
const injector = injector_1.Injector.from([MyServer, Connection, Connection2]); | ||
globals_1.expect(injector.get(Connection)).toBeInstanceOf(Connection); | ||
@@ -208,11 +209,28 @@ globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new injector_1.Injector([MyServer]); | ||
globals_1.expect(() => injector.get(Connection)).toThrow('Could not resolve injector token Connection'); | ||
globals_1.expect(() => injector.get(MyServer)).toThrow(`Unknown constructor argument 'connection: Connection' of MyServer(?).`); | ||
globals_1.expect(() => injector_1.Injector.from([MyServer])).toThrow(`Unknown dependency 'connection: Connection' of MyServer.connection`); | ||
} | ||
}); | ||
globals_1.test('injector optional unmet dependency', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
globals_1.expect(connection).toBeUndefined(); | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
decorator_1.injectable, | ||
__param(0, type_1.t.optional), | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = injector_1.Injector.from([MyServer]); | ||
globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
globals_1.test('injector optional dependency', () => { | ||
@@ -228,12 +246,77 @@ class Connection { | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject().optional), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject().optional), | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new injector_1.Injector([MyServer]); | ||
globals_1.expect(() => injector.get(Connection)).toThrow('Could not resolve injector token Connection'); | ||
const injector = injector_1.Injector.from([MyServer]); | ||
globals_1.expect(() => injector.get(Connection)).toThrow(`Token 'Connection' in InjectorModule not found`); | ||
globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
globals_1.test('injector via t.optional', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
globals_1.expect(connection).toBeUndefined(); | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
decorator_1.injectable, | ||
__param(0, type_1.t.optional), | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = injector_1.Injector.from([MyServer]); | ||
globals_1.expect(() => injector.get(Connection)).toThrow(`Token 'Connection' in InjectorModule not found`); | ||
globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
globals_1.test('injector via t.type', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
decorator_1.injectable, | ||
__param(0, type_1.t.type(Connection)), | ||
__metadata("design:paramtypes", [Object]) | ||
], MyServer); | ||
{ | ||
const injector = injector_1.Injector.from([MyServer, Connection]); | ||
globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
globals_1.expect(injector.get(MyServer).connection).toBeInstanceOf(Connection); | ||
} | ||
}); | ||
globals_1.test('injector via t.type string', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
decorator_1.injectable, | ||
__param(0, type_1.t.type('connection')), | ||
__metadata("design:paramtypes", [Object]) | ||
], MyServer); | ||
{ | ||
const injector = injector_1.Injector.from([MyServer, { provide: 'connection', useClass: Connection }]); | ||
globals_1.expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
globals_1.expect(injector.get(MyServer).connection).toBeInstanceOf(Connection); | ||
} | ||
}); | ||
globals_1.test('injector token', () => { | ||
const Connection = new decorator_1.InjectorToken('connection'); | ||
{ | ||
const injector = injector_1.Injector.from([{ provide: Connection, useValue: { doIt() { return 'hi'; } } }]); | ||
globals_1.expect(injector.get(Connection).doIt()).toBe('hi'); | ||
} | ||
}); | ||
globals_1.test('injector overwrite provider', () => { | ||
@@ -251,7 +334,7 @@ class Connection { | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new injector_1.Injector([MyServer, { | ||
const injector = injector_1.Injector.from([MyServer, { | ||
provide: Connection, useClass: Connection2 | ||
@@ -270,7 +353,7 @@ }]); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [MyServer]) | ||
], MyServer); | ||
{ | ||
const injector = new injector_1.Injector([MyServer]); | ||
const injector = injector_1.Injector.from([MyServer]); | ||
globals_1.expect(() => injector.get(MyServer)).toThrow(injector_1.CircularDependencyError); | ||
@@ -287,4 +370,4 @@ } | ||
Connection = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject(() => MyServer)), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject(() => MyServer)), | ||
__metadata("design:paramtypes", [Object]) | ||
@@ -299,7 +382,7 @@ ], Connection); | ||
MyServer = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new injector_1.Injector([MyServer, Connection]); | ||
const injector = injector_1.Injector.from([MyServer, Connection]); | ||
globals_1.expect(() => injector.get(MyServer)).toThrow(injector_1.CircularDependencyError); | ||
@@ -313,3 +396,3 @@ globals_1.expect(() => injector.get(MyServer)).toThrow('Circular dependency found MyServer -> Connection -> MyServer'); | ||
{ | ||
const injector = new injector_1.Injector([{ provide: Service, useFactory: () => new Service() }]); | ||
const injector = injector_1.Injector.from([{ provide: Service, useFactory: () => new Service() }]); | ||
const s1 = injector.get(Service); | ||
@@ -322,31 +405,7 @@ globals_1.expect(s1).toBeInstanceOf(Service); | ||
}); | ||
globals_1.test('injector stack parent', () => { | ||
const i1 = new injector_1.Injector([ | ||
{ provide: 'level', deps: ['deep1'], useFactory: (d) => d }, | ||
{ provide: 'level2', deps: ['deep2'], useFactory: (d) => d }, | ||
]); | ||
const i2 = new injector_1.Injector([{ provide: 'deep1', useValue: 2 }], [i1]); | ||
const i3 = new injector_1.Injector([{ provide: 'deep2', useValue: 3 }], [i2]); | ||
globals_1.expect(i2.get('level')).toBe(2); | ||
globals_1.expect(i3.get('level')).toBe(2); | ||
globals_1.expect(() => i2.get('level2')).toThrow(`Unknown factory dependency argument 'deep2' of useFactory`); | ||
globals_1.expect(i3.get('level2')).toBe(3); | ||
}); | ||
globals_1.test('injector stack parent fork', () => { | ||
const i1 = new injector_1.Injector([ | ||
{ provide: 'level', deps: ['deep1'], useFactory: (d) => d }, | ||
{ provide: 'level2', deps: ['deep2'], useFactory: (d) => d }, | ||
]); | ||
const i2 = new injector_1.Injector([{ provide: 'deep1', useValue: 2 }], [i1]).fork(); | ||
const i3 = new injector_1.Injector([{ provide: 'deep2', useValue: 3 }], [i2]).fork(); | ||
globals_1.expect(i2.get('level')).toBe(2); | ||
globals_1.expect(i3.get('level')).toBe(2); | ||
globals_1.expect(() => i2.get('level2')).toThrow(`Unknown factory dependency argument 'deep2' of useFactory`); | ||
globals_1.expect(i3.get('level2')).toBe(3); | ||
}); | ||
globals_1.test('injector config', () => { | ||
const FullConfig = injector_1.createConfig({ | ||
const moduleConfig = config_1.createConfig({ | ||
debug: type_1.t.boolean.default(false) | ||
}); | ||
class ServiceConfig extends FullConfig.slice(['debug']) { | ||
class ServiceConfig extends moduleConfig.slice('debug') { | ||
} | ||
@@ -359,3 +418,3 @@ let MyService = class MyService { | ||
MyService = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [ServiceConfig]) | ||
@@ -369,4 +428,4 @@ ], MyService); | ||
MyService2 = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject(FullConfig)), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject(moduleConfig)), | ||
__metadata("design:paramtypes", [Object]) | ||
@@ -380,7 +439,7 @@ ], MyService2); | ||
MyService3 = __decorate([ | ||
injector_1.injectable(), | ||
__param(0, injector_1.inject(FullConfig.all())), | ||
decorator_1.injectable, | ||
__param(0, decorator_1.inject(moduleConfig.all())), | ||
__metadata("design:paramtypes", [Object]) | ||
], MyService3); | ||
class Slice extends FullConfig.slice(['debug']) { | ||
class Slice extends moduleConfig.slice('debug') { | ||
} | ||
@@ -393,7 +452,8 @@ let MyService4 = class MyService4 { | ||
MyService4 = __decorate([ | ||
injector_1.injectable(), | ||
decorator_1.injectable, | ||
__metadata("design:paramtypes", [Slice]) | ||
], MyService4); | ||
{ | ||
const i1 = new injector_1.Injector([MyService, MyService2, MyService3, MyService4], []); | ||
const i1 = injector_1.Injector.fromModule(new module_1.InjectorModule([MyService, MyService2, MyService3, MyService4]).setConfigDefinition(moduleConfig)); | ||
i1.module.configure({ debug: false }); | ||
globals_1.expect(i1.get(MyService).config.debug).toBe(false); | ||
@@ -405,6 +465,4 @@ globals_1.expect(i1.get(MyService2).config.debug).toBe(false); | ||
{ | ||
const myModule = new module_1.InjectorModule('asd', { debug: true }); | ||
const injectorContext = new injector_1.InjectorContext(); | ||
injectorContext.registerModule(myModule, FullConfig); | ||
const i1 = new injector_1.Injector([MyService, MyService2, MyService3, MyService4], [], injectorContext); | ||
const i1 = injector_1.Injector.fromModule(new module_1.InjectorModule([MyService, MyService2, MyService3, MyService4]).setConfigDefinition(moduleConfig)); | ||
i1.module.configure({ debug: true }); | ||
globals_1.expect(i1.get(MyService).config.debug).toBe(true); | ||
@@ -426,34 +484,22 @@ globals_1.expect(i1.get(MyService2).config.debug).toBe(true); | ||
{ | ||
const injectorContext = new injector_1.InjectorContext(); | ||
const i1 = new injector_1.Injector([MyService], [], injectorContext); | ||
const i1 = injector_1.Injector.from([MyService]); | ||
globals_1.expect(i1.get(MyService).transporter).toEqual([]); | ||
} | ||
{ | ||
const injectorContext = new injector_1.InjectorContext(); | ||
injectorContext.setupProvider(MyService).addTransporter('a'); | ||
injectorContext.setupProvider(MyService).addTransporter('b'); | ||
globals_1.expect(injectorContext.configuredProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = new injector_1.Injector([MyService], [], injectorContext); | ||
const module = new module_1.InjectorModule([MyService]); | ||
module.setupProvider(MyService).addTransporter('a'); | ||
module.setupProvider(MyService).addTransporter('b'); | ||
globals_1.expect(module.setupProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = injector_1.Injector.fromModule(module); | ||
globals_1.expect(i1.get(MyService).transporter).toEqual(['a', 'b']); | ||
} | ||
{ | ||
const injectorContext = new injector_1.InjectorContext(); | ||
injectorContext.setupProvider(MyService).transporter = ['a']; | ||
injectorContext.setupProvider(MyService).transporter = ['a', 'b', 'c']; | ||
globals_1.expect(injectorContext.configuredProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = new injector_1.Injector([MyService], [], injectorContext); | ||
const module = new module_1.InjectorModule([MyService]); | ||
module.setupProvider(MyService).transporter = ['a']; | ||
module.setupProvider(MyService).transporter = ['a', 'b', 'c']; | ||
globals_1.expect(module.setupProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = injector_1.Injector.fromModule(module); | ||
globals_1.expect(i1.get(MyService).transporter).toEqual(['a', 'b', 'c']); | ||
} | ||
}); | ||
globals_1.test('injector fork', () => { | ||
class MyService { | ||
} | ||
const i1 = new injector_1.Injector([MyService]); | ||
const s1 = i1.get(MyService); | ||
globals_1.expect(s1).toBeInstanceOf(MyService); | ||
const i2 = i1.fork(); | ||
const s2 = i2.get(MyService); | ||
globals_1.expect(s2).toBeInstanceOf(MyService); | ||
globals_1.expect(s2).not.toBe(s1); | ||
}); | ||
globals_1.test('constructor one with @inject', () => { | ||
@@ -474,4 +520,4 @@ class HttpKernel { | ||
MyService = __decorate([ | ||
injector_1.injectable(), | ||
__param(2, injector_1.inject().optional), | ||
decorator_1.injectable, | ||
__param(2, decorator_1.inject().optional), | ||
__metadata("design:paramtypes", [HttpKernel, | ||
@@ -492,3 +538,21 @@ Logger, | ||
} | ||
let Service = class Service { | ||
constructor(stopwatch, logger) { | ||
this.stopwatch = stopwatch; | ||
this.logger = logger; | ||
} | ||
}; | ||
Service = __decorate([ | ||
decorator_1.injectable, | ||
__param(1, decorator_1.inject(Logger)), | ||
__metadata("design:paramtypes", [Stopwatch, Object]) | ||
], Service); | ||
{ | ||
const schema = type_1.getClassSchema(Service); | ||
const methods = schema.getMethodProperties('constructor'); | ||
globals_1.expect(methods.length).toBe(2); | ||
globals_1.expect(methods[0].name).toBe('stopwatch'); | ||
globals_1.expect(methods[1].name).toBe('logger'); | ||
} | ||
}); | ||
//# sourceMappingURL=injector.spec.js.map |
@@ -0,3 +1,5 @@ | ||
export * from './src/config'; | ||
export * from './src/decorator'; | ||
export * from './src/injector'; | ||
export * from './src/provider'; | ||
export * from './src/module'; | ||
export * from './src/provider'; |
@@ -0,4 +1,6 @@ | ||
export * from './src/config'; | ||
export * from './src/decorator'; | ||
export * from './src/injector'; | ||
export * from './src/provider'; | ||
export * from './src/module'; | ||
export * from './src/provider'; | ||
//# sourceMappingURL=index.js.map |
@@ -1,68 +0,6 @@ | ||
import { ClassSchema, ExtractClassDefinition, PlainSchemaProps, PropertySchema } from '@deepkit/type'; | ||
import { Provider, ProviderWithScope, TagRegistry } from './provider'; | ||
import { NormalizedProvider, ProviderWithScope, TagRegistry, Token } from './provider'; | ||
import { ClassType, CompilerContext, CustomError } from '@deepkit/core'; | ||
import { PropertySchema } from '@deepkit/type'; | ||
import { InjectorToken } from './decorator'; | ||
import { InjectorModule } from './module'; | ||
export declare class ConfigToken<T extends {}> { | ||
config: ConfigDefinition<T>; | ||
name: keyof T & string; | ||
constructor(config: ConfigDefinition<T>, name: keyof T & string); | ||
} | ||
export declare class ConfigSlice<T extends {}> { | ||
bag?: { | ||
[name: string]: any; | ||
}; | ||
config: ConfigDefinition<T>; | ||
names: (keyof T & string)[]; | ||
constructor(config: ConfigDefinition<T>, names: (keyof T & string)[]); | ||
valueOf(): this; | ||
} | ||
export declare class ConfigDefinition<T extends {}> { | ||
readonly schema: ClassSchema<T>; | ||
protected module?: InjectorModule; | ||
type: T; | ||
constructor(schema: ClassSchema<T>); | ||
setModule(module: InjectorModule): void; | ||
hasModule(): boolean; | ||
getModule(): InjectorModule; | ||
getConfigOrDefaults(): any; | ||
all(): ClassType<T>; | ||
slice<N extends (keyof T & string)[]>(names: N): ClassType<Pick<T, N[number]>>; | ||
token<N extends (keyof T & string)>(name: N): ConfigToken<T>; | ||
} | ||
export declare class InjectorReference { | ||
readonly to: any; | ||
constructor(to: any); | ||
} | ||
export declare function injectorReference<T>(classTypeOrToken: T): any; | ||
export declare function createConfig<T extends PlainSchemaProps>(config: T): ConfigDefinition<ExtractClassDefinition<T>>; | ||
export interface InjectDecorator { | ||
(target: object, property?: string, parameterIndexOrDescriptor?: any): any; | ||
/** | ||
* Mark as optional. | ||
*/ | ||
readonly optional: this; | ||
/** | ||
* Resolves the dependency token from the root injector. | ||
*/ | ||
readonly root: this; | ||
readonly options: { | ||
token: any; | ||
optional: boolean; | ||
root: boolean; | ||
}; | ||
} | ||
export declare type InjectOptions = { | ||
token: any | ForwardRef<any>; | ||
optional: boolean; | ||
root: boolean; | ||
}; | ||
declare type ForwardRef<T> = () => T; | ||
export declare function isInjectDecorator(v: any): v is InjectDecorator; | ||
export declare function inject(token?: any | ForwardRef<any>): InjectDecorator; | ||
export declare class InjectToken { | ||
readonly name: string; | ||
constructor(name: string); | ||
toString(): string; | ||
} | ||
export declare function injectable(): (target: object) => void; | ||
export declare class CircularDependencyError extends CustomError { | ||
@@ -75,101 +13,3 @@ } | ||
export declare function tokenLabel(token: any): string; | ||
export interface ConfigContainer { | ||
get(path: string): any; | ||
} | ||
export interface BasicInjector { | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: BasicInjector): R; | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | ||
export declare class Injector implements BasicInjector { | ||
protected providers: Provider[]; | ||
protected parents: (BasicInjector | Injector)[]; | ||
protected injectorContext: InjectorContext; | ||
protected configuredProviderRegistry: ConfiguredProviderRegistry | undefined; | ||
protected tagRegistry: TagRegistry; | ||
protected contextResolver?: { | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | undefined; | ||
circularCheck: boolean; | ||
protected resolved: any[]; | ||
protected retriever(injector: Injector, token: any, frontInjector?: Injector): any; | ||
constructor(providers?: Provider[], parents?: (BasicInjector | Injector)[], injectorContext?: InjectorContext, configuredProviderRegistry?: ConfiguredProviderRegistry | undefined, tagRegistry?: TagRegistry, contextResolver?: { | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | undefined); | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
/** | ||
* Creates a clone of this instance, maintains the provider structure, but drops provider instances. | ||
* Note: addProviders() in the new fork changes the origin, since providers array is not cloned. | ||
*/ | ||
fork(parents?: Injector[], injectorContext?: InjectorContext): Injector; | ||
/** | ||
* Changes the provider structure of this injector. | ||
* | ||
* Note: This is very performance sensitive. Every time you call this function a new dependency injector function | ||
* is generated, which si pretty slow. So, it's recommended to create a Injector with providers in the constructor | ||
* and not change it. | ||
*/ | ||
addProviders(...providers: Provider[]): void; | ||
isRoot(): boolean; | ||
protected createFactoryProperty(options: { | ||
name: string | number; | ||
token: any; | ||
optional: boolean; | ||
}, compiler: CompilerContext, ofName: string, argPosition: number, notFoundFunction: string): string; | ||
protected optionsFromProperty(property: PropertySchema): { | ||
token: any; | ||
name: string | number; | ||
optional: boolean; | ||
}; | ||
protected createFactory(compiler: CompilerContext, classType: ClassType): string; | ||
protected buildRetriever(): (injector: Injector, token: any, frontInjector?: Injector) => any; | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R; | ||
} | ||
export declare class MemoryInjector extends Injector { | ||
protected providers: ({ | ||
provide: any; | ||
useValue: any; | ||
} | { | ||
provide: any; | ||
useFactory: () => any; | ||
})[]; | ||
constructor(providers: ({ | ||
provide: any; | ||
useValue: any; | ||
} | { | ||
provide: any; | ||
useFactory: () => any; | ||
})[]); | ||
fork(parents?: Injector[]): Injector; | ||
protected retriever(injector: Injector, token: any): any; | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R; | ||
} | ||
export declare class ContextRegistry { | ||
contexts: Context[]; | ||
get size(): number; | ||
get(id: number): Context; | ||
set(id: number, value: Context): void; | ||
} | ||
export declare class ScopedContextScopeCaches { | ||
protected size: number; | ||
protected caches: { | ||
[name: string]: ScopedContextCache; | ||
}; | ||
constructor(size: number); | ||
getCache(scope: string): ScopedContextCache; | ||
} | ||
export declare class ScopedContextCache { | ||
protected size: number; | ||
protected injectors: (Injector | undefined)[]; | ||
constructor(size: number); | ||
get(contextId: number): Injector | undefined; | ||
set(contextId: number, injector: Injector): void; | ||
} | ||
export declare class Context { | ||
readonly module: InjectorModule; | ||
readonly id: number; | ||
readonly parent?: Context | undefined; | ||
providers: ProviderWithScope[]; | ||
constructor(module: InjectorModule, id: number, parent?: Context | undefined); | ||
} | ||
export declare type ConfiguredProviderCalls = { | ||
export declare type SetupProviderCalls = { | ||
type: 'call'; | ||
@@ -188,46 +28,90 @@ methodName: string | symbol | number; | ||
}; | ||
export declare class ConfiguredProviderRegistry { | ||
calls: Map<any, ConfiguredProviderCalls[]>; | ||
add(token: any, ...newCalls: ConfiguredProviderCalls[]): void; | ||
get(token: any): ConfiguredProviderCalls[]; | ||
clone(): ConfiguredProviderRegistry; | ||
export declare class SetupProviderRegistry { | ||
calls: Map<Token<any>, SetupProviderCalls[]>; | ||
add(token: any, ...newCalls: SetupProviderCalls[]): void; | ||
mergeInto(registry: SetupProviderRegistry): void; | ||
get(token: Token): SetupProviderCalls[]; | ||
} | ||
export declare type ConfigureProvider<T> = { | ||
[name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name]; | ||
}; | ||
interface Scope { | ||
name: string; | ||
instances: { | ||
[name: string]: any; | ||
}; | ||
} | ||
export declare type ResolveToken<T> = T extends ClassType<infer R> ? R : T extends InjectorToken<infer R> ? R : T; | ||
export declare function resolveToken(provider: ProviderWithScope): Token; | ||
export interface InjectorInterface { | ||
get<T>(token: T, scope?: Scope): ResolveToken<T>; | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* This is the actual dependency injection container. | ||
* Every module has its own injector. | ||
*/ | ||
export declare function setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, registry: ConfiguredProviderRegistry, order: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
export declare class InjectorContext implements BasicInjector { | ||
readonly contextManager: ContextRegistry; | ||
readonly scope: string; | ||
readonly configuredProviderRegistry: ConfiguredProviderRegistry; | ||
readonly parent: InjectorContext | undefined; | ||
readonly additionalInjectorParent: Injector | undefined; | ||
readonly modules: { | ||
[name: string]: InjectorModule; | ||
export declare class Injector implements InjectorInterface { | ||
readonly module: InjectorModule; | ||
private buildContext; | ||
private resolver?; | ||
private setter?; | ||
/** | ||
* All unscoped provider instances. Scoped instances are attached to `Scope`. | ||
*/ | ||
private instances; | ||
constructor(module: InjectorModule, buildContext: BuildContext); | ||
static from(providers: ProviderWithScope[], parent?: Injector): Injector; | ||
static fromModule(module: InjectorModule, parent?: Injector): Injector; | ||
get<T>(token: T, scope?: Scope): ResolveToken<T>; | ||
set<T>(token: T, value: any, scope?: Scope): void; | ||
clear(): void; | ||
protected build(buildContext: BuildContext): void; | ||
protected buildProvider(buildContext: BuildContext, compiler: CompilerContext, name: string, accessor: string, scope: string, provider: NormalizedProvider, resolveDependenciesFrom: InjectorModule[]): string; | ||
protected createFactory(provider: NormalizedProvider, resolvedName: string, compiler: CompilerContext, classType: ClassType, resolveDependenciesFrom: InjectorModule[]): { | ||
code: string; | ||
dependencies: number; | ||
}; | ||
protected createFactoryProperty(options: { | ||
name: string | number; | ||
token: any; | ||
optional: boolean; | ||
}, fromProvider: NormalizedProvider, compiler: CompilerContext, resolveDependenciesFrom: InjectorModule[], ofName: string, argPosition: number, notFoundFunction: string): string; | ||
protected optionsFromProperty(property: PropertySchema): { | ||
token: any; | ||
name: string | number; | ||
optional: boolean; | ||
}; | ||
} | ||
declare class BuildProviderIndex { | ||
protected offset: number; | ||
reserve(): number; | ||
} | ||
export declare class BuildContext { | ||
static ids: number; | ||
id: number; | ||
tagRegistry: TagRegistry; | ||
protected injectors: (Injector | undefined)[]; | ||
readonly scopeCaches: ScopedContextScopeCaches; | ||
protected cache: ScopedContextCache; | ||
constructor(contextManager?: ContextRegistry, scope?: string, configuredProviderRegistry?: ConfiguredProviderRegistry, parent?: InjectorContext | undefined, additionalInjectorParent?: Injector | undefined, modules?: { | ||
[name: string]: InjectorModule; | ||
}, scopeCaches?: ScopedContextScopeCaches, tagRegistry?: TagRegistry); | ||
getModule(name: string): InjectorModule; | ||
registerModule(module: InjectorModule, config?: ConfigDefinition<any>): void; | ||
providerIndex: BuildProviderIndex; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* In the process of preparing providers, each module redirects their | ||
* global setup calls in this registry. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
getModuleNames(): string[]; | ||
globalSetupProviderRegistry: SetupProviderRegistry; | ||
} | ||
/** | ||
* A InjectorContext is responsible for taking a root InjectorModule and build all Injectors. | ||
* | ||
* It also can create scopes aka a sub InjectorContext with providers from a particular scope. | ||
*/ | ||
export declare class InjectorContext { | ||
rootModule: InjectorModule; | ||
readonly scope?: Scope | undefined; | ||
protected buildContext: BuildContext; | ||
constructor(rootModule: InjectorModule, scope?: Scope | undefined, buildContext?: BuildContext); | ||
get<T>(token: T | Token, module?: InjectorModule): ResolveToken<T>; | ||
set<T>(token: T, value: any, module?: InjectorModule): void; | ||
static forProviders(providers: ProviderWithScope[]): InjectorContext; | ||
getInjectorForModule(module: InjectorModule): Injector; | ||
getInjector(contextId: number): Injector; | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R; | ||
createChildScope(scope: string, additionalInjectorParent?: Injector): InjectorContext; | ||
/** | ||
* Returns the unscoped injector. Use `.get(T, Scope)` for resolving scoped token. | ||
*/ | ||
getInjector(module: InjectorModule): Injector; | ||
getRootInjector(): Injector; | ||
createChildScope(scope: string): InjectorContext; | ||
} | ||
export {}; |
@@ -1,141 +0,7 @@ | ||
/* | ||
* Deepkit Framework | ||
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the MIT License. | ||
* | ||
* You should have received a copy of the MIT License along with this program. | ||
*/ | ||
import { FieldDecoratorWrapper, getClassSchema, jsonSerializer, t } from '@deepkit/type'; | ||
import { getProviders, isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, Tag, TagProvider, TagRegistry } from './provider'; | ||
import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, Tag, TagProvider, TagRegistry } from './provider'; | ||
import { CompilerContext, CustomError, getClassName, isClass, isFunction, isPrototypeOfBase } from '@deepkit/core'; | ||
import { InjectorModule } from './module'; | ||
export class ConfigToken { | ||
constructor(config, name) { | ||
this.config = config; | ||
this.name = name; | ||
} | ||
} | ||
export class ConfigSlice { | ||
constructor(config, names) { | ||
//we want that ConfigSlice acts as a regular plain object, which can be serialized at wish. | ||
Object.defineProperties(this, { | ||
config: { enumerable: false, value: config }, | ||
names: { enumerable: false, value: names }, | ||
bag: { enumerable: false, writable: true }, | ||
}); | ||
for (const name of names) { | ||
Object.defineProperty(this, name, { | ||
enumerable: true, | ||
get: () => { | ||
return this.bag ? this.bag[name] : undefined; | ||
} | ||
}); | ||
} | ||
} | ||
valueOf() { | ||
return { ...this }; | ||
} | ||
} | ||
export class ConfigDefinition { | ||
constructor(schema) { | ||
this.schema = schema; | ||
} | ||
setModule(module) { | ||
this.module = module; | ||
} | ||
hasModule() { | ||
return this.module !== undefined; | ||
} | ||
getModule() { | ||
if (!this.module) | ||
throw new Error('ConfigDefinition module not set. Make sure your config is assigned to a single module. See createModule({config: x}).'); | ||
return this.module; | ||
} | ||
getConfigOrDefaults() { | ||
if (this.module) | ||
return this.module.getConfig(); | ||
return jsonSerializer.for(this.schema).validatedDeserialize({}); | ||
} | ||
all() { | ||
const self = this; | ||
return class extends ConfigSlice { | ||
constructor() { | ||
super(self, [...self.schema.getProperties()].map(v => v.name)); | ||
} | ||
}; | ||
} | ||
slice(names) { | ||
const self = this; | ||
return class extends ConfigSlice { | ||
constructor() { | ||
super(self, names); | ||
} | ||
}; | ||
} | ||
token(name) { | ||
return new ConfigToken(this, name); | ||
} | ||
} | ||
export class InjectorReference { | ||
constructor(to) { | ||
this.to = to; | ||
} | ||
} | ||
export function injectorReference(classTypeOrToken) { | ||
return new InjectorReference(classTypeOrToken); | ||
} | ||
export function createConfig(config) { | ||
return new ConfigDefinition(t.schema(config)); | ||
} | ||
const injectSymbol = Symbol('inject'); | ||
export function isInjectDecorator(v) { | ||
return isFunction(v) && v.hasOwnProperty(injectSymbol); | ||
} | ||
export function inject(token) { | ||
const injectOptions = { | ||
optional: false, | ||
root: false, | ||
token: token, | ||
}; | ||
const fn = (target, propertyOrMethodName, parameterIndexOrDescriptor) => { | ||
FieldDecoratorWrapper((target, property, returnType) => { | ||
property.data['deepkit/inject'] = injectOptions; | ||
property.setFromJSType(returnType); | ||
})(target, propertyOrMethodName, parameterIndexOrDescriptor); | ||
}; | ||
Object.defineProperty(fn, injectSymbol, { value: true, enumerable: false }); | ||
Object.defineProperty(fn, 'optional', { | ||
get() { | ||
injectOptions.optional = true; | ||
return fn; | ||
} | ||
}); | ||
Object.defineProperty(fn, 'options', { | ||
get() { | ||
return injectOptions; | ||
} | ||
}); | ||
Object.defineProperty(fn, 'root', { | ||
get() { | ||
injectOptions.optional = true; | ||
return fn; | ||
} | ||
}); | ||
return fn; | ||
} | ||
export class InjectToken { | ||
constructor(name) { | ||
this.name = name; | ||
} | ||
toString() { | ||
return 'InjectToken=' + this.name; | ||
} | ||
} | ||
export function injectable() { | ||
return (target) => { | ||
//don't do anything. This is just used to generate type metadata. | ||
}; | ||
} | ||
import { getClassSchema, isFieldDecorator } from '@deepkit/type'; | ||
import { InjectorReference, isInjectDecorator } from './decorator'; | ||
import { ConfigDefinition, ConfigSlice, ConfigToken } from './config'; | ||
import { findModuleForConfig, getScope, InjectorModule } from './module'; | ||
export class CircularDependencyError extends CustomError { | ||
@@ -160,125 +26,280 @@ } | ||
} | ||
function constructorParameterNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
throw new DependenciesUnmetError(`Unknown constructor argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function tokenNotfoundError(token, moduleName) { | ||
throw new TokenNotFoundError(`Token '${tokenLabel(token)}' in ${moduleName} not found. Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function factoryDependencyNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function propertyParameterNotFound(ofName, name, position, token) { | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
let CircularDetector = []; | ||
let CircularDetectorResets = []; | ||
export class Injector { | ||
constructor(providers = [], parents = [], injectorContext = new InjectorContext, configuredProviderRegistry = undefined, tagRegistry = new TagRegistry(), contextResolver) { | ||
this.providers = providers; | ||
this.parents = parents; | ||
this.injectorContext = injectorContext; | ||
this.configuredProviderRegistry = configuredProviderRegistry; | ||
this.tagRegistry = tagRegistry; | ||
this.contextResolver = contextResolver; | ||
this.circularCheck = true; | ||
this.resolved = []; | ||
if (!this.configuredProviderRegistry) | ||
this.configuredProviderRegistry = injectorContext.configuredProviderRegistry; | ||
if (this.providers.length) | ||
this.retriever = this.buildRetriever(); | ||
function throwCircularDependency() { | ||
const path = CircularDetector.map(tokenLabel).join(' -> '); | ||
CircularDetector.length = 0; | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new CircularDependencyError(`Circular dependency found ${path}`); | ||
} | ||
export class SetupProviderRegistry { | ||
constructor() { | ||
this.calls = new Map(); | ||
} | ||
retriever(injector, token, frontInjector) { | ||
for (const parent of injector.parents) { | ||
const v = 'retriever' in parent ? parent.retriever(parent, token, frontInjector) : parent.get(token, frontInjector); | ||
if (v !== undefined) | ||
return v; | ||
add(token, ...newCalls) { | ||
this.get(token).push(...newCalls); | ||
} | ||
mergeInto(registry) { | ||
for (const [token, calls] of this.calls) { | ||
registry.add(token, ...calls); | ||
} | ||
return undefined; | ||
} | ||
getInjectorForModule(module) { | ||
return this.contextResolver ? this.contextResolver.getInjectorForModule(module) : this; | ||
get(token) { | ||
let calls = this.calls.get(token); | ||
if (!calls) { | ||
calls = []; | ||
this.calls.set(token, calls); | ||
} | ||
return calls; | ||
} | ||
/** | ||
* Creates a clone of this instance, maintains the provider structure, but drops provider instances. | ||
* Note: addProviders() in the new fork changes the origin, since providers array is not cloned. | ||
*/ | ||
fork(parents, injectorContext) { | ||
const injector = new Injector(undefined, parents || this.parents, injectorContext, this.configuredProviderRegistry, this.tagRegistry, this.contextResolver); | ||
injector.providers = this.providers; | ||
injector.retriever = this.retriever; | ||
return injector; | ||
} | ||
export function resolveToken(provider) { | ||
if (isClass(provider)) | ||
return provider; | ||
if (provider instanceof TagProvider) | ||
return resolveToken(provider.provider); | ||
return provider.provide; | ||
} | ||
/** | ||
* This is the actual dependency injection container. | ||
* Every module has its own injector. | ||
*/ | ||
export class Injector { | ||
constructor(module, buildContext) { | ||
this.module = module; | ||
this.buildContext = buildContext; | ||
/** | ||
* All unscoped provider instances. Scoped instances are attached to `Scope`. | ||
*/ | ||
this.instances = {}; | ||
module.injector = this; | ||
this.build(buildContext); | ||
} | ||
/** | ||
* Changes the provider structure of this injector. | ||
* | ||
* Note: This is very performance sensitive. Every time you call this function a new dependency injector function | ||
* is generated, which si pretty slow. So, it's recommended to create a Injector with providers in the constructor | ||
* and not change it. | ||
*/ | ||
addProviders(...providers) { | ||
this.providers.push(...providers); | ||
this.retriever = this.buildRetriever(); | ||
static from(providers, parent) { | ||
return new Injector(new InjectorModule(providers, parent === null || parent === void 0 ? void 0 : parent.module), new BuildContext); | ||
} | ||
isRoot() { | ||
return this.parents.length === 0; | ||
static fromModule(module, parent) { | ||
return new Injector(module, new BuildContext); | ||
} | ||
createFactoryProperty(options, compiler, ofName, argPosition, notFoundFunction) { | ||
const token = options.token; | ||
if (token instanceof ConfigDefinition) { | ||
if (token.hasModule()) { | ||
const module = this.injectorContext.getModule(token.getModule().getName()); | ||
return compiler.reserveVariable('fullConfig', module.getConfig()); | ||
get(token, scope) { | ||
if (!this.resolver) | ||
throw new Error('Injector was not built'); | ||
return this.resolver(token, scope); | ||
} | ||
set(token, value, scope) { | ||
if (!this.setter) | ||
throw new Error('Injector was not built'); | ||
this.setter(token, value, scope); | ||
} | ||
clear() { | ||
this.instances = {}; | ||
} | ||
build(buildContext) { | ||
const resolverCompiler = new CompilerContext(); | ||
resolverCompiler.context.set('CircularDetector', CircularDetector); | ||
resolverCompiler.context.set('CircularDetectorResets', CircularDetectorResets); | ||
resolverCompiler.context.set('throwCircularDependency', throwCircularDependency); | ||
resolverCompiler.context.set('tokenNotfoundError', tokenNotfoundError); | ||
resolverCompiler.context.set('injector', this); | ||
const lines = []; | ||
const resets = []; | ||
const creating = []; | ||
const setterCompiler = new CompilerContext(); | ||
setterCompiler.context.set('injector', this); | ||
const setterLines = []; | ||
for (const [token, prepared] of this.module.getPreparedProviders(buildContext).entries()) { | ||
//scopes will be created first, so they are returned instead of the unscoped instance | ||
prepared.providers.sort((a, b) => { | ||
if (a.scope && !b.scope) | ||
return -1; | ||
if (!a.scope && b.scope) | ||
return +1; | ||
return 0; | ||
}); | ||
for (const provider of prepared.providers) { | ||
const scope = getScope(provider); | ||
const name = 'i' + this.buildContext.providerIndex.reserve(); | ||
creating.push(`let creating_${name} = false;`); | ||
resets.push(`creating_${name} = false;`); | ||
const accessor = scope ? 'scope.instances.' + name : 'injector.instances.' + name; | ||
setterLines.push(`case ${setterCompiler.reserveVariable('token', token)}: { | ||
${accessor} = value; | ||
break; | ||
}`); | ||
if (prepared.resolveFrom) { | ||
//its a redirect | ||
lines.push(` | ||
case token === ${resolverCompiler.reserveConst(token)}: { | ||
return ${resolverCompiler.reserveConst(prepared.resolveFrom)}.injector.resolver(${resolverCompiler.reserveConst(token)}, scope); | ||
} | ||
`); | ||
} | ||
else { | ||
//we own and instantiate the service | ||
lines.push(this.buildProvider(buildContext, resolverCompiler, name, accessor, scope, provider, prepared.modules)); | ||
} | ||
} | ||
else { | ||
return compiler.reserveVariable('fullConfig', token.getConfigOrDefaults()); | ||
} | ||
} | ||
else if (token instanceof ConfigToken) { | ||
if (token.config.hasModule()) { | ||
const module = this.injectorContext.getModule(token.config.getModule().getName()); | ||
const config = module.getConfig(); | ||
return compiler.reserveVariable(token.name, config[token.name]); | ||
const setter = setterCompiler.build(` | ||
switch (token) { | ||
${setterLines.join('\n')} | ||
} | ||
else { | ||
const config = token.config.getConfigOrDefaults(); | ||
return compiler.reserveVariable(token.name, config[token.name]); | ||
`, 'token', 'value', 'scope'); | ||
const resolver = resolverCompiler.raw(` | ||
${creating.join('\n')}; | ||
CircularDetectorResets.push(() => { | ||
${resets.join('\n')}; | ||
}); | ||
return function(token, scope) { | ||
switch (true) { | ||
${lines.join('\n')} | ||
} | ||
tokenNotfoundError(token, '${getClassName(this.module)}'); | ||
} | ||
`); | ||
this.setter = setter; | ||
this.resolver = resolver; | ||
} | ||
buildProvider(buildContext, compiler, name, accessor, scope, provider, resolveDependenciesFrom) { | ||
var _a; | ||
let transient = false; | ||
const token = provider.provide; | ||
let factory = { code: '', dependencies: 0 }; | ||
const tokenVar = compiler.reserveConst(token); | ||
if (isValueProvider(provider)) { | ||
transient = provider.transient === true; | ||
const valueVar = compiler.reserveVariable('useValue', provider.useValue); | ||
factory.code = `${accessor} = ${valueVar};`; | ||
} | ||
else if (isClass(token) && (Object.getPrototypeOf(Object.getPrototypeOf(token)) === ConfigSlice || Object.getPrototypeOf(token) === ConfigSlice)) { | ||
const value = new token; | ||
if (!value.bag) { | ||
if (value.config.hasModule()) { | ||
const module = this.injectorContext.getModule(value.config.getModule().getName()); | ||
value.bag = module.getConfig(); | ||
else if (isClassProvider(provider)) { | ||
transient = provider.transient === true; | ||
let useClass = provider.useClass; | ||
if (!useClass) { | ||
if (!isClass(provider.provide)) { | ||
throw new Error(`UseClassProvider needs to set either 'useClass' or 'provide' as a ClassType. Got ${provider.provide}`); | ||
} | ||
else { | ||
value.bag = value.config.getConfigOrDefaults(); | ||
} | ||
return compiler.reserveVariable('configSlice', value); | ||
useClass = provider.provide; | ||
} | ||
factory = this.createFactory(provider, accessor, compiler, useClass, resolveDependenciesFrom); | ||
} | ||
else if (token === TagRegistry) { | ||
return compiler.reserveVariable('tagRegistry', this.tagRegistry); | ||
else if (isExistingProvider(provider)) { | ||
transient = provider.transient === true; | ||
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(provider.useExisting)}, scope)`; | ||
} | ||
else if (isPrototypeOfBase(token, Tag)) { | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const providers = compiler.reserveVariable('tagRegistry', this.tagRegistry.resolve(token)); | ||
return `new ${tokenVar}(${providers}.map(v => (frontInjector.retriever ? frontInjector.retriever(frontInjector, v, frontInjector) : frontInjector.get(v, frontInjector))))`; | ||
else if (isFactoryProvider(provider)) { | ||
transient = provider.transient === true; | ||
const args = []; | ||
let i = 0; | ||
for (const dep of provider.deps || []) { | ||
let optional = false; | ||
let token = dep; | ||
if (isInjectDecorator(dep)) { | ||
optional = dep.options.optional; | ||
token = dep.options.token; | ||
} | ||
if (isFieldDecorator(dep)) { | ||
const propertySchema = dep.buildPropertySchema(); | ||
optional = propertySchema.isOptional; | ||
if (propertySchema.type === 'literal' || propertySchema.type === 'class') { | ||
token = propertySchema.literalValue !== undefined ? propertySchema.literalValue : propertySchema.getResolvedClassType(); | ||
} | ||
} | ||
if (!token) { | ||
throw new Error(`No token defined for dependency ${i} in 'deps' of useFactory for ${tokenLabel(provider.provide)}`); | ||
} | ||
factory.dependencies++; | ||
args.push(this.createFactoryProperty({ | ||
name: i++, | ||
token, | ||
optional, | ||
}, provider, compiler, resolveDependenciesFrom, 'useFactory', args.length, 'factoryDependencyNotFound')); | ||
} | ||
factory.code = `${accessor} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; | ||
} | ||
else { | ||
if (token === undefined) { | ||
let of = `${ofName}.${options.name}`; | ||
if (argPosition >= 0) { | ||
const argsCheck = []; | ||
for (let i = 0; i < argPosition; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
of = `${ofName}(${argsCheck.join(', ')})`; | ||
throw new Error('Invalid provider'); | ||
} | ||
const configureProvider = []; | ||
const configuredProviderCalls = (_a = resolveDependenciesFrom[0].setupProviderRegistry) === null || _a === void 0 ? void 0 : _a.get(token); | ||
configuredProviderCalls.push(...buildContext.globalSetupProviderRegistry.get(token)); | ||
if (configuredProviderCalls) { | ||
configuredProviderCalls.sort((a, b) => { | ||
return a.order - b.order; | ||
}); | ||
for (const call of configuredProviderCalls) { | ||
if (call.type === 'stop') | ||
break; | ||
if (call.type === 'call') { | ||
const args = []; | ||
const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; | ||
for (const arg of call.args) { | ||
if (arg instanceof InjectorReference) { | ||
const injector = arg.module ? compiler.reserveConst(arg.module) + '.injector' : 'injector'; | ||
args.push(`${injector}.resolver(${compiler.reserveConst(arg.to)}, scope)`); | ||
} | ||
else { | ||
args.push(`${compiler.reserveVariable('arg', arg)}`); | ||
} | ||
} | ||
configureProvider.push(`${accessor}.${methodName}(${args.join(', ')});`); | ||
} | ||
throw new DependenciesUnmetError(`Undefined dependency '${options.name}: undefined' of ${of}. Dependency '${options.name}' has no type. Imported reflect-metadata correctly? ` + | ||
`Use '@inject(PROVIDER) ${options.name}: T' if T is an interface. For circular references use @inject(() => T) ${options.name}: T.`); | ||
if (call.type === 'property') { | ||
const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; | ||
const value = call.value instanceof InjectorReference ? `frontInjector.get(${compiler.reserveVariable('forward', call.value.to)})` : compiler.reserveVariable('value', call.value); | ||
configureProvider.push(`${accessor}.${property} = ${value};`); | ||
} | ||
} | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; | ||
return `(frontInjector.retriever ? frontInjector.retriever(frontInjector, ${tokenVar}, frontInjector) : frontInjector.get(${tokenVar}, frontInjector)) ${orThrow}`; | ||
} | ||
return 'undefined'; | ||
} | ||
optionsFromProperty(property) { | ||
const options = property.data['deepkit/inject']; | ||
let token = property.resolveClassType; | ||
if (options && options.token) { | ||
token = isFunction(options.token) ? options.token() : options.token; | ||
else { | ||
configureProvider.push('//no custom provider setup'); | ||
} | ||
return { token, name: property.name, optional: !!options && options.optional }; | ||
const scopeCheck = scope ? ` && scope && scope.name === ${JSON.stringify(scope)}` : ''; | ||
//circular dependencies can happen, when for example a service with InjectorContext injected manually instantiates a service. | ||
//if that service references back to the first one, it will be a circular loop. So we track that with `creating` state. | ||
const creatingVar = `creating_${name}`; | ||
const circularDependencyCheckStart = factory.dependencies ? `if (${creatingVar}) throwCircularDependency();${creatingVar} = true;` : ''; | ||
const circularDependencyCheckEnd = factory.dependencies ? `${creatingVar} = false;` : ''; | ||
return ` | ||
//${tokenLabel(token)} | ||
case token === ${tokenVar}${scopeCheck}: { | ||
${!transient ? `if (${accessor} !== undefined) return ${accessor};` : ''} | ||
CircularDetector.push(${tokenVar}); | ||
${circularDependencyCheckStart} | ||
${factory.code} | ||
${circularDependencyCheckEnd} | ||
CircularDetector.pop(); | ||
${configureProvider.join('\n')} | ||
return ${accessor}; | ||
} | ||
`; | ||
} | ||
createFactory(compiler, classType) { | ||
createFactory(provider, resolvedName, compiler, classType, resolveDependenciesFrom) { | ||
if (!classType) | ||
@@ -290,4 +311,9 @@ throw new Error('Can not create factory for undefined ClassType'); | ||
const classTypeVar = compiler.reserveVariable('classType', classType); | ||
let dependencies = 0; | ||
for (const property of schema.getMethodProperties('constructor')) { | ||
args.push(this.createFactoryProperty(this.optionsFromProperty(property), compiler, getClassName(classType), args.length, 'constructorParameterNotFound')); | ||
if (!property) { | ||
throw new Error(`Constructor arguments hole in ${getClassName(classType)}`); | ||
} | ||
dependencies++; | ||
args.push(this.createFactoryProperty(this.optionsFromProperty(property), provider, compiler, resolveDependenciesFrom, getClassName(classType), args.length, 'constructorParameterNotFound')); | ||
} | ||
@@ -299,412 +325,174 @@ for (const property of schema.getProperties()) { | ||
continue; | ||
propertyAssignment.push(`v.${property.name} = ${this.createFactoryProperty(this.optionsFromProperty(property), compiler, getClassName(classType), -1, 'propertyParameterNotFound')};`); | ||
dependencies++; | ||
try { | ||
const resolveProperty = this.createFactoryProperty(this.optionsFromProperty(property), provider, compiler, resolveDependenciesFrom, getClassName(classType), -1, 'propertyParameterNotFound'); | ||
propertyAssignment.push(`${resolvedName}.${property.name} = ${resolveProperty};`); | ||
} | ||
catch (error) { | ||
throw new Error(`Could not resolve property injection token ${getClassName(classType)}.${property.name}: ${error.message}`); | ||
} | ||
} | ||
return `v = new ${classTypeVar}(${args.join(',')});\n${propertyAssignment.join('\n')}`; | ||
return { | ||
code: `${resolvedName} = new ${classTypeVar}(${args.join(',')});\n${propertyAssignment.join('\n')}`, | ||
dependencies | ||
}; | ||
} | ||
buildRetriever() { | ||
var _a; | ||
const compiler = new CompilerContext(); | ||
const lines = []; | ||
const resets = []; | ||
this.resolved = []; | ||
lines.push(` | ||
case ${compiler.reserveVariable('injectorContextClassType', InjectorContext)}: return injector.injectorContext; | ||
case ${compiler.reserveVariable('injectorClassType', Injector)}: return injector; | ||
`); | ||
let resolvedIds = 0; | ||
const normalizedProviders = new Map(); | ||
//make sure that providers that declare the same provider token will be filtered out so that the last will be used. | ||
for (const provider of this.providers) { | ||
if (provider instanceof TagProvider) { | ||
normalizedProviders.set(provider, provider); | ||
createFactoryProperty(options, fromProvider, compiler, resolveDependenciesFrom, ofName, argPosition, notFoundFunction) { | ||
const token = options.token; | ||
//regarding configuration values: the attached module is not necessarily in resolveDependenciesFrom[0] | ||
//if the parent module overwrites its, then the parent module is at 0th position. | ||
if (token instanceof ConfigDefinition) { | ||
const module = findModuleForConfig(token, resolveDependenciesFrom); | ||
return compiler.reserveVariable('fullConfig', module.getConfig()); | ||
} | ||
else if (token instanceof ConfigToken) { | ||
const module = findModuleForConfig(token.config, resolveDependenciesFrom); | ||
const config = module.getConfig(); | ||
return compiler.reserveVariable(token.name, config[token.name]); | ||
} | ||
else if (isClass(token) && (Object.getPrototypeOf(Object.getPrototypeOf(token)) === ConfigSlice || Object.getPrototypeOf(token) === ConfigSlice)) { | ||
const value = new token; | ||
const module = findModuleForConfig(value.config, resolveDependenciesFrom); | ||
value.bag = module.getConfig(); | ||
return compiler.reserveVariable('configSlice', value); | ||
} | ||
else if (token === TagRegistry) { | ||
return compiler.reserveVariable('tagRegistry', this.buildContext.tagRegistry); | ||
} | ||
else if (isPrototypeOfBase(token, Tag)) { | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const resolvedVar = compiler.reserveVariable('tagResolved'); | ||
const entries = this.buildContext.tagRegistry.resolve(token); | ||
const args = []; | ||
for (const entry of entries) { | ||
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(entry.tagProvider.provider.provide)}, scope)`); | ||
} | ||
else if (isValueProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (isClassProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (isExistingProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (isFactoryProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} | ||
else if (isClass(provider)) { | ||
normalizedProviders.set(provider, provider); | ||
} | ||
return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`; | ||
} | ||
for (let provider of normalizedProviders.values()) { | ||
const resolvedId = resolvedIds++; | ||
this.resolved.push(undefined); | ||
let transient = false; | ||
let factory = ''; | ||
let token; | ||
const tagToken = provider instanceof TagProvider ? provider : undefined; | ||
if (provider instanceof TagProvider) { | ||
provider = provider.provider; | ||
} | ||
if (isValueProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
const valueVar = compiler.reserveVariable('useValue', provider.useValue); | ||
factory = `v = ${valueVar};`; | ||
} | ||
else if (isClassProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
factory = this.createFactory(compiler, provider.useClass || provider.provide); | ||
} | ||
else if (isExistingProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
factory = this.createFactory(compiler, provider.useExisting); | ||
} | ||
else if (isFactoryProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
const args = []; | ||
let i = 0; | ||
for (const dep of provider.deps || []) { | ||
let optional = false; | ||
let token = dep; | ||
if (isInjectDecorator(dep)) { | ||
optional = dep.options.optional; | ||
token = dep.options.token; | ||
} | ||
if (!token) { | ||
throw new Error(`No token defined for dependency ${i} in 'deps' of useFactory for ${tokenLabel(provider.provide)}`); | ||
} | ||
args.push(this.createFactoryProperty({ | ||
name: i++, | ||
token, | ||
optional, | ||
}, compiler, 'useFactory', args.length, 'factoryDependencyNotFound')); | ||
else { | ||
let of = `${ofName}.${options.name}`; | ||
if (token === undefined) { | ||
if (argPosition >= 0) { | ||
const argsCheck = []; | ||
for (let i = 0; i < argPosition; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
of = `${ofName}(${argsCheck.join(', ')})`; | ||
} | ||
factory = `v = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; | ||
throw new DependenciesUnmetError(`Undefined dependency '${options.name}: undefined' of ${of}. Dependency '${options.name}' has no type. Imported reflect-metadata correctly? ` + | ||
`Use '@inject(PROVIDER) ${options.name}: T' if T is an interface. For circular references use @inject(() => T) ${options.name}: T.`); | ||
} | ||
else if (isClass(provider)) { | ||
token = provider; | ||
factory = this.createFactory(compiler, provider); | ||
} | ||
else { | ||
throw new Error('Invalid provider'); | ||
} | ||
if (tagToken) | ||
token = tagToken; | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const creatingVar = compiler.reserveVariable('creating', false); | ||
const configuredProviderCalls = (_a = this.configuredProviderRegistry) === null || _a === void 0 ? void 0 : _a.get(token); | ||
const configureProvider = []; | ||
if (configuredProviderCalls) { | ||
configuredProviderCalls.sort((a, b) => { | ||
return a.order - b.order; | ||
}); | ||
for (const call of configuredProviderCalls) { | ||
if (call.type === 'stop') | ||
break; | ||
if (call.type === 'call') { | ||
const args = []; | ||
const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; | ||
for (const arg of call.args) { | ||
if (arg instanceof InjectorReference) { | ||
args.push(`frontInjector.get(${compiler.reserveVariable('forward', arg.to)})`); | ||
} | ||
else { | ||
args.push(`${compiler.reserveVariable('arg', arg)}`); | ||
} | ||
let foundPreparedProvider = undefined; | ||
for (const module of resolveDependenciesFrom) { | ||
foundPreparedProvider = module.getPreparedProvider(token); | ||
if (foundPreparedProvider) { | ||
if (foundPreparedProvider) { | ||
//check if the found provider was actually exported to this current module. | ||
//if not it means that provider is encapsulated living only in its module and can not be accessed from other modules. | ||
const moduleHasAccessToThisProvider = foundPreparedProvider.modules.some(m => m === module); | ||
if (!moduleHasAccessToThisProvider) { | ||
foundPreparedProvider = undefined; | ||
} | ||
configureProvider.push(`v.${methodName}(${args.join(', ')});`); | ||
} | ||
if (call.type === 'property') { | ||
const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; | ||
const value = call.value instanceof InjectorReference ? `frontInjector.get(${compiler.reserveVariable('forward', call.value.to)})` : compiler.reserveVariable('value', call.value); | ||
configureProvider.push(`v.${property} = ${value};`); | ||
} | ||
} | ||
} | ||
else { | ||
configureProvider.push('//no custom provider setup'); | ||
if (!foundPreparedProvider) { | ||
//try if parents have anything | ||
const foundInModule = this.module.resolveToken(token); | ||
if (foundInModule) { | ||
foundPreparedProvider = foundInModule.getPreparedProvider(token); | ||
} | ||
} | ||
resets.push(`${creatingVar} = false;`); | ||
lines.push(` | ||
//${tokenLabel(token)} | ||
case ${tokenVar}: { | ||
${transient ? 'let v;' : `let v = injector.resolved[${resolvedId}]; if (v !== undefined) return v;`} | ||
CircularDetector.push(${tokenVar}); | ||
if (${creatingVar}) { | ||
throwCircularDependency(); | ||
} | ||
${creatingVar} = true; | ||
${factory} | ||
${transient ? '' : `injector.resolved[${resolvedId}] = v;`} | ||
${creatingVar} = false; | ||
${configureProvider.join('\n')} | ||
CircularDetector.pop(); | ||
return v; | ||
} | ||
`); | ||
if (!foundPreparedProvider && options.optional) | ||
return 'undefined'; | ||
if (!foundPreparedProvider) { | ||
throw new DependenciesUnmetError(`Unknown dependency '${options.name}: ${tokenLabel(token)}' of ${of}.`); | ||
} | ||
const allPossibleScopes = foundPreparedProvider.providers.map(getScope); | ||
const fromScope = getScope(fromProvider); | ||
const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1; | ||
if (!unscoped && !allPossibleScopes.includes(fromScope)) { | ||
throw new DependenciesUnmetError(`Dependency '${options.name}: ${tokenLabel(token)}' of ${of} can not be injected into ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` + | ||
`since ${tokenLabel(token)} only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.`); | ||
} | ||
//when the dependency is FactoryProvider it might return undefined. | ||
//in this case, if the dependency is not optional, we throw an error. | ||
const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; | ||
const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; | ||
if (resolveFromModule === this.module) { | ||
return `injector.resolver(${tokenVar}, scope)`; | ||
} | ||
return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope) ${orThrow}`; | ||
} | ||
const parents = []; | ||
for (let i = 0; i < this.parents.length; i++) { | ||
let retriever = 'retriever' in this.parents[i] ? `injector.parents[${i}].retriever(injector.parents[${i}], ` : `injector.parents[${i}].get(`; | ||
parents.push(` | ||
{ | ||
const v = ${retriever}token, frontInjector); | ||
if (v !== undefined) return v; | ||
} | ||
`); | ||
} | ||
optionsFromProperty(property) { | ||
const options = property.data['deepkit/inject']; | ||
let token = property.resolveClassType; | ||
if (options && options.token) { | ||
token = isFunction(options.token) ? options.token() : options.token; | ||
} | ||
compiler.context.set('CircularDetector', CircularDetector); | ||
compiler.context.set('throwCircularDependency', throwCircularDependency); | ||
compiler.context.set('CircularDetectorResets', CircularDetectorResets); | ||
compiler.context.set('constructorParameterNotFound', constructorParameterNotFound); | ||
compiler.context.set('factoryDependencyNotFound', factoryDependencyNotFound); | ||
compiler.context.set('propertyParameterNotFound', propertyParameterNotFound); | ||
compiler.preCode = ` | ||
CircularDetectorResets.push(() => { | ||
${resets.join('\n')}; | ||
}); | ||
`; | ||
return compiler.build(` | ||
frontInjector = frontInjector || injector; | ||
switch (token) { | ||
${lines.join('\n')} | ||
else if (property.type === 'class') { | ||
token = property.getResolvedClassType(); | ||
} | ||
${parents.join('\n')} | ||
return undefined; | ||
`, 'injector', 'token', 'frontInjector'); | ||
} | ||
get(token, frontInjector) { | ||
const v = this.retriever(this, token, frontInjector || this); | ||
if (v !== undefined) | ||
return v; | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new TokenNotFoundError(`Could not resolve injector token ${tokenLabel(token)}`); | ||
} | ||
} | ||
function constructorParameterNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown constructor argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function factoryDependencyNotFound(ofName, name, position, token) { | ||
const argsCheck = []; | ||
for (let i = 0; i < position; i++) | ||
argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function propertyParameterNotFound(ofName, name, position, token) { | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new DependenciesUnmetError(`Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.`); | ||
} | ||
function throwCircularDependency() { | ||
const path = CircularDetector.map(tokenLabel).join(' -> '); | ||
CircularDetector.length = 0; | ||
for (const reset of CircularDetectorResets) | ||
reset(); | ||
throw new CircularDependencyError(`Circular dependency found ${path}`); | ||
} | ||
export class MemoryInjector extends Injector { | ||
constructor(providers) { | ||
super(); | ||
this.providers = providers; | ||
} | ||
fork(parents) { | ||
return this; | ||
} | ||
retriever(injector, token) { | ||
for (const p of this.providers) { | ||
if (p.provide === token) | ||
return 'useFactory' in p ? p.useFactory() : p.useValue; | ||
else if (property.type === 'literal') { | ||
token = property.literalValue; | ||
} | ||
return { token, name: property.name, optional: property.isOptional ? true : (!!options && options.optional) }; | ||
} | ||
get(token, frontInjector) { | ||
const result = this.retriever(this, token); | ||
if (result === undefined) | ||
throw new TokenNotFoundError(`Could not resolve injector token ${tokenLabel(token)}`); | ||
return result; | ||
} | ||
} | ||
export class ContextRegistry { | ||
class BuildProviderIndex { | ||
constructor() { | ||
this.contexts = []; | ||
this.offset = 0; | ||
} | ||
get size() { | ||
return this.contexts.length; | ||
reserve() { | ||
return this.offset++; | ||
} | ||
get(id) { | ||
return this.contexts[id]; | ||
} | ||
set(id, value) { | ||
this.contexts[id] = value; | ||
} | ||
} | ||
export class ScopedContextScopeCaches { | ||
constructor(size) { | ||
this.size = size; | ||
this.caches = {}; | ||
} | ||
getCache(scope) { | ||
let cache = this.caches[scope]; | ||
if (!cache) { | ||
cache = new ScopedContextCache(this.size); | ||
this.caches[scope] = cache; | ||
} | ||
return cache; | ||
} | ||
} | ||
export class ScopedContextCache { | ||
constructor(size) { | ||
this.size = size; | ||
this.injectors = new Array(this.size); | ||
} | ||
get(contextId) { | ||
return this.injectors[contextId]; | ||
} | ||
set(contextId, injector) { | ||
this.injectors[contextId] = injector; | ||
} | ||
} | ||
export class Context { | ||
constructor(module, id, parent) { | ||
this.module = module; | ||
this.id = id; | ||
this.parent = parent; | ||
this.providers = []; | ||
} | ||
} | ||
export class ConfiguredProviderRegistry { | ||
export class BuildContext { | ||
constructor() { | ||
this.calls = new Map(); | ||
this.id = BuildContext.ids++; | ||
this.tagRegistry = new TagRegistry; | ||
this.providerIndex = new BuildProviderIndex; | ||
/** | ||
* In the process of preparing providers, each module redirects their | ||
* global setup calls in this registry. | ||
*/ | ||
this.globalSetupProviderRegistry = new SetupProviderRegistry; | ||
} | ||
add(token, ...newCalls) { | ||
this.get(token).push(...newCalls); | ||
} | ||
get(token) { | ||
let calls = this.calls.get(token); | ||
if (!calls) { | ||
calls = []; | ||
this.calls.set(token, calls); | ||
} | ||
return calls; | ||
} | ||
clone() { | ||
const c = new ConfiguredProviderRegistry; | ||
for (const [token, calls] of this.calls.entries()) { | ||
c.calls.set(token, calls.slice()); | ||
} | ||
return c; | ||
} | ||
} | ||
BuildContext.ids = 0; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* A InjectorContext is responsible for taking a root InjectorModule and build all Injectors. | ||
* | ||
* It also can create scopes aka a sub InjectorContext with providers from a particular scope. | ||
*/ | ||
export function setupProvider(classTypeOrToken, registry, order) { | ||
const proxy = new Proxy({}, { | ||
get(target, prop) { | ||
return (...args) => { | ||
registry.add(classTypeOrToken, { type: 'call', methodName: prop, args: args, order }); | ||
return proxy; | ||
}; | ||
}, | ||
set(target, prop, value) { | ||
registry.add(classTypeOrToken, { type: 'property', property: prop, value: value, order }); | ||
return true; | ||
} | ||
}); | ||
return proxy; | ||
} | ||
export class InjectorContext { | ||
constructor(contextManager = new ContextRegistry, scope = 'module', configuredProviderRegistry = new ConfiguredProviderRegistry, parent = undefined, additionalInjectorParent = undefined, modules = {}, scopeCaches, tagRegistry = new TagRegistry()) { | ||
this.contextManager = contextManager; | ||
constructor(rootModule, scope, buildContext = new BuildContext) { | ||
this.rootModule = rootModule; | ||
this.scope = scope; | ||
this.configuredProviderRegistry = configuredProviderRegistry; | ||
this.parent = parent; | ||
this.additionalInjectorParent = additionalInjectorParent; | ||
this.modules = modules; | ||
this.tagRegistry = tagRegistry; | ||
this.injectors = new Array(this.contextManager.contexts.length); | ||
this.scopeCaches = scopeCaches || new ScopedContextScopeCaches(this.contextManager.size); | ||
this.cache = this.scopeCaches.getCache(this.scope); | ||
this.buildContext = buildContext; | ||
} | ||
getModule(name) { | ||
if (!this.modules[name]) | ||
throw new Error(`No Module with name ${name} registered`); | ||
return this.modules[name]; | ||
get(token, module) { | ||
return this.getInjector(module || this.rootModule).get(token, this.scope); | ||
} | ||
registerModule(module, config) { | ||
if (this.modules[module.getName()]) | ||
throw new Error(`Module ${module.getName()} already registered`); | ||
if (config) | ||
config.setModule(module); | ||
this.modules[module.getName()] = module; | ||
for (const [provider, calls] of module.getConfiguredProviderRegistry().calls) { | ||
this.configuredProviderRegistry.add(provider, ...calls); | ||
} | ||
set(token, value, module) { | ||
return this.getInjector(module || this.rootModule).set(token, value, this.scope); | ||
} | ||
static forProviders(providers) { | ||
return new InjectorContext(new InjectorModule(providers)); | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Returns the unscoped injector. Use `.get(T, Scope)` for resolving scoped token. | ||
*/ | ||
setupProvider(classTypeOrToken, order = 0) { | ||
return setupProvider(classTypeOrToken, this.configuredProviderRegistry, order); | ||
getInjector(module) { | ||
return module.getOrCreateInjector(this.buildContext); | ||
} | ||
getModuleNames() { | ||
return Object.keys(this.modules); | ||
getRootInjector() { | ||
return this.getInjector(this.rootModule); | ||
} | ||
static forProviders(providers) { | ||
const registry = new ContextRegistry(); | ||
const context = new Context(new InjectorModule('', {}), 0); | ||
registry.set(0, context); | ||
context.providers.push(...providers); | ||
return new InjectorContext(registry); | ||
createChildScope(scope) { | ||
return new InjectorContext(this.rootModule, { name: scope, instances: {} }, this.buildContext); | ||
} | ||
getInjectorForModule(module) { | ||
return this.getInjector(module.contextId); | ||
} | ||
getInjector(contextId) { | ||
let injector = this.injectors[contextId]; | ||
if (injector) | ||
return injector; | ||
const parents = []; | ||
parents.push(this.parent ? this.parent.getInjector(contextId) : new Injector()); | ||
if (this.additionalInjectorParent) | ||
parents.push(this.additionalInjectorParent.fork(undefined, this)); | ||
const context = this.contextManager.get(contextId); | ||
if (context.parent) | ||
parents.push(this.getInjector(context.parent.id)); | ||
injector = this.cache.get(contextId); | ||
if (injector) { | ||
//we have one from cache. Clear it, and return | ||
injector = injector.fork(parents, this); | ||
return this.injectors[contextId] = injector; | ||
} | ||
const providers = getProviders(context.providers, this.scope); | ||
injector = new Injector(providers, parents, this, this.configuredProviderRegistry, this.tagRegistry); | ||
this.injectors[contextId] = injector; | ||
this.cache.set(contextId, injector); | ||
return injector; | ||
} | ||
get(token, frontInjector) { | ||
const injector = this.getInjector(0); | ||
return injector.get(token, frontInjector); | ||
} | ||
createChildScope(scope, additionalInjectorParent) { | ||
return new InjectorContext(this.contextManager, scope, this.configuredProviderRegistry, this, additionalInjectorParent, this.modules, this.scopeCaches, this.tagRegistry); | ||
} | ||
} | ||
//# sourceMappingURL=injector.js.map |
@@ -0,20 +1,137 @@ | ||
import { ConfigDefinition } from './config'; | ||
import { NormalizedProvider, ProviderWithScope, Token } from './provider'; | ||
import { ClassType } from '@deepkit/core'; | ||
import { ConfiguredProviderRegistry, ConfigureProvider } from './injector'; | ||
export declare class InjectorModule<N extends string = string, C extends { | ||
import { InjectorToken } from './decorator'; | ||
import { BuildContext, Injector, SetupProviderRegistry } from './injector'; | ||
export declare type ConfigureProvider<T> = { | ||
[name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name]; | ||
}; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
*/ | ||
export declare function setupProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, registry: SetupProviderRegistry, order: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
export interface PreparedProvider { | ||
/** | ||
* The modules from which dependencies can be resolved. The first item is always the module from which this provider was declared. | ||
* | ||
* This is per default the module in which the provider was declared, | ||
* but if the provider was moved (by exporting), then if | ||
* a) the parent had this provider already, then this array has additionally the one from which the provider was exported. | ||
* b) the parent had no provider of that token, then this array is just the module from which the provider was exported. | ||
* | ||
* This is important otherwise exported provider won't have access in their dependencies to their original (encapsulated) injector. | ||
*/ | ||
modules: InjectorModule[]; | ||
/** | ||
* A token can have multiple providers, for each scope its own entry. | ||
* Each scoped provider can only exist once. | ||
*/ | ||
providers: NormalizedProvider[]; | ||
/** | ||
* When this provider was exported to another module and thus is actually instantiated in another module, then this is set. | ||
* This is necessary to tell the module who declared this provider to not instantiate it, but redirects resolve requests | ||
* to `resolveFrom` instead. | ||
*/ | ||
resolveFrom?: InjectorModule; | ||
} | ||
export declare function findModuleForConfig(config: ConfigDefinition<any>, modules: InjectorModule[]): InjectorModule; | ||
export declare type ExportType = ClassType | InjectorToken<any> | string | InjectorModule; | ||
export declare function isProvided(providers: ProviderWithScope[], token: any): boolean; | ||
export declare function getScope(provider: ProviderWithScope): string; | ||
export declare class InjectorModule<C extends { | ||
[name: string]: any; | ||
} = any> { | ||
name: N; | ||
} = any, IMPORT = InjectorModule<any, any>> { | ||
providers: ProviderWithScope[]; | ||
parent?: InjectorModule<any, InjectorModule<any, any>> | undefined; | ||
config: C; | ||
contextId: number; | ||
protected setupProviderRegistry: ConfiguredProviderRegistry; | ||
constructor(name: N, config: C); | ||
getName(): N; | ||
exports: ExportType[]; | ||
id: number; | ||
/** | ||
* Whether this module is for the root module. All its providers are automatically exported and moved to the root level. | ||
*/ | ||
root: boolean; | ||
/** | ||
* The built injector. This is set once a Injector for this module has been created. | ||
*/ | ||
injector?: Injector; | ||
setupProviderRegistry: SetupProviderRegistry; | ||
globalSetupProviderRegistry: SetupProviderRegistry; | ||
imports: InjectorModule[]; | ||
/** | ||
* The first stage of building the injector is to resolve all providers and exports. | ||
* Then the actual injector functions can be built. | ||
*/ | ||
protected processed: boolean; | ||
protected exportsDisabled: boolean; | ||
configDefinition?: ConfigDefinition<any>; | ||
constructor(providers?: ProviderWithScope[], parent?: InjectorModule<any, InjectorModule<any, any>> | undefined, config?: C, exports?: ExportType[]); | ||
registerAsChildren(child: InjectorModule): void; | ||
/** | ||
* When the module exports providers the importer don't want to have then `disableExports` disable all exports. | ||
*/ | ||
disableExports(): this; | ||
/** | ||
* Makes all the providers, controllers, etc available at the root module, basically exporting everything. | ||
*/ | ||
forRoot(): this; | ||
/** | ||
* Reverts the root default setting to false. | ||
*/ | ||
notForRoot(): this; | ||
unregisterAsChildren(child: InjectorModule): void; | ||
getChildren(): InjectorModule[]; | ||
setConfigDefinition(config: ConfigDefinition<any>): this; | ||
setParent(parent: InjectorModule): this; | ||
getParent(): InjectorModule | undefined; | ||
protected assertInjectorNotBuilt(): void; | ||
addExport(...controller: ClassType[]): this; | ||
isProvided(classType: ClassType): boolean; | ||
addProvider(...provider: ProviderWithScope[]): this; | ||
getProviders(): ProviderWithScope[]; | ||
getConfig(): C; | ||
setConfig(config: C): void; | ||
getConfiguredProviderRegistry(): ConfiguredProviderRegistry; | ||
configure(config: Partial<C>): this; | ||
getImports(): InjectorModule[]; | ||
getImportedModulesByClass<T extends InjectorModule>(classType: ClassType<T>): T[]; | ||
getImportedModuleByClass<T extends InjectorModule>(classType: ClassType<T>): T; | ||
getImportedModule<T extends InjectorModule>(module: T): T; | ||
getExports(): ExportType[]; | ||
hasImport<T extends InjectorModule>(moduleClass: ClassType<T>): boolean; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Modifies this module and adds a new import, returning the same module. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
addImport(...modules: InjectorModule<any>[]): this; | ||
/** | ||
* Allows to register additional setup calls for a provider in this module. | ||
* The injector token needs to be available in the local module providers. | ||
* Use setupGlobalProvider to register globally setup calls (not limited to this module only). | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
/** | ||
* Allows to register additional setup calls for a provider in the whole module tree. | ||
* The injector token needs to be available in the local module providers. | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupGlobalProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, order?: number): ConfigureProvider<T extends ClassType<infer C> ? C : T>; | ||
getOrCreateInjector(buildContext: BuildContext): Injector; | ||
protected preparedProviders?: Map<any, PreparedProvider>; | ||
getPreparedProvider(token: any): PreparedProvider | undefined; | ||
resolveToken(token: any): InjectorModule | undefined; | ||
/** | ||
* Prepared the module for a injector tree build. | ||
* | ||
* - Index providers by token so that last known provider is picked (so they can be overwritten). | ||
* - Register TagProvider in TagRegistry | ||
* - Put TagProvider in providers if not already made. | ||
* - Put exports to parent's module with the reference to this, so the dependencies are fetched from the correct module. | ||
*/ | ||
getPreparedProviders(buildContext: BuildContext): Map<any, PreparedProvider>; | ||
protected exported: boolean; | ||
protected handleExports(buildContext: BuildContext): void; | ||
findRoot(): InjectorModule; | ||
} |
@@ -1,29 +0,384 @@ | ||
import { ConfiguredProviderRegistry, setupProvider } from './injector'; | ||
import { TagProvider } from './provider'; | ||
import { arrayRemoveItem, getClassName, isClass, isPrototypeOfBase } from '@deepkit/core'; | ||
import { Injector, SetupProviderRegistry } from './injector'; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
*/ | ||
export function setupProvider(classTypeOrToken, registry, order) { | ||
const proxy = new Proxy({}, { | ||
get(target, prop) { | ||
return (...args) => { | ||
registry.add(classTypeOrToken, { type: 'call', methodName: prop, args: args, order }); | ||
return proxy; | ||
}; | ||
}, | ||
set(target, prop, value) { | ||
registry.add(classTypeOrToken, { type: 'property', property: prop, value: value, order }); | ||
return true; | ||
} | ||
}); | ||
return proxy; | ||
} | ||
let moduleIds = 0; | ||
function registerPreparedProvider(map, modules, providers) { | ||
const token = providers[0].provide; | ||
const preparedProvider = map.get(token); | ||
if (preparedProvider) { | ||
for (const provider of providers) { | ||
const scope = getScope(provider); | ||
//check if given provider has a unknown scope, if so set it. | ||
//if the scope is known, overwrite it (we want always the last known provider to be valid) | ||
const knownProvider = preparedProvider.providers.findIndex(v => getScope(v) === scope); | ||
if (knownProvider === -1) { | ||
//scope not known, add it | ||
preparedProvider.providers.push(provider); | ||
} | ||
else { | ||
//scope already known, replace it | ||
preparedProvider.providers.splice(knownProvider, 1, provider); | ||
} | ||
} | ||
} | ||
else { | ||
//just add it | ||
map.set(token, { modules, providers: providers.slice(0) }); | ||
} | ||
} | ||
export function findModuleForConfig(config, modules) { | ||
for (const m of modules) { | ||
if (m.configDefinition === config) | ||
return m; | ||
} | ||
throw new Error(`No module found for configuration ${config.schema.toString()}. Did you attach it to a module?`); | ||
} | ||
export function isProvided(providers, token) { | ||
return providers.find(v => !(v instanceof TagProvider) ? token === (isClass(v) ? v : v.provide) : false) !== undefined; | ||
} | ||
export function getScope(provider) { | ||
return (isClass(provider) ? '' : provider instanceof TagProvider ? provider.provider.scope : provider.scope) || ''; | ||
} | ||
export class InjectorModule { | ||
constructor(name, config) { | ||
this.name = name; | ||
constructor(providers = [], parent, config = {}, exports = []) { | ||
this.providers = providers; | ||
this.parent = parent; | ||
this.config = config; | ||
this.contextId = 0; | ||
this.setupProviderRegistry = new ConfiguredProviderRegistry; | ||
this.exports = exports; | ||
this.id = moduleIds++; | ||
/** | ||
* Whether this module is for the root module. All its providers are automatically exported and moved to the root level. | ||
*/ | ||
this.root = false; | ||
this.setupProviderRegistry = new SetupProviderRegistry; | ||
this.globalSetupProviderRegistry = new SetupProviderRegistry; | ||
this.imports = []; | ||
/** | ||
* The first stage of building the injector is to resolve all providers and exports. | ||
* Then the actual injector functions can be built. | ||
*/ | ||
this.processed = false; | ||
this.exportsDisabled = false; | ||
this.exported = false; | ||
if (this.parent) | ||
this.parent.registerAsChildren(this); | ||
} | ||
getName() { | ||
return this.name; | ||
registerAsChildren(child) { | ||
if (this.imports.includes(child)) | ||
return; | ||
this.imports.push(child); | ||
} | ||
/** | ||
* When the module exports providers the importer don't want to have then `disableExports` disable all exports. | ||
*/ | ||
disableExports() { | ||
this.exportsDisabled = true; | ||
return this; | ||
} | ||
/** | ||
* Makes all the providers, controllers, etc available at the root module, basically exporting everything. | ||
*/ | ||
forRoot() { | ||
this.root = true; | ||
return this; | ||
} | ||
/** | ||
* Reverts the root default setting to false. | ||
*/ | ||
notForRoot() { | ||
this.root = false; | ||
return this; | ||
} | ||
unregisterAsChildren(child) { | ||
if (!this.imports.includes(child)) | ||
return; | ||
child.parent = undefined; | ||
arrayRemoveItem(this.imports, child); | ||
} | ||
getChildren() { | ||
return this.imports; | ||
} | ||
setConfigDefinition(config) { | ||
this.configDefinition = config; | ||
return this; | ||
} | ||
setParent(parent) { | ||
if (this.parent === parent) | ||
return this; | ||
this.assertInjectorNotBuilt(); | ||
if (this.parent) | ||
this.parent.unregisterAsChildren(this); | ||
this.parent = parent; | ||
this.parent.registerAsChildren(this); | ||
return this; | ||
} | ||
getParent() { | ||
return this.parent; | ||
} | ||
assertInjectorNotBuilt() { | ||
if (!this.injector) | ||
return; | ||
throw new Error(`Injector already built for ${getClassName(this)}. Can not modify its provider or tree structure.`); | ||
} | ||
addExport(...controller) { | ||
this.assertInjectorNotBuilt(); | ||
this.exports.push(...controller); | ||
return this; | ||
} | ||
isProvided(classType) { | ||
return isProvided(this.getProviders(), classType); | ||
} | ||
addProvider(...provider) { | ||
this.assertInjectorNotBuilt(); | ||
this.providers.push(...provider); | ||
return this; | ||
} | ||
getProviders() { | ||
return this.providers; | ||
} | ||
getConfig() { | ||
return this.config; | ||
} | ||
setConfig(config) { | ||
configure(config) { | ||
Object.assign(this.config, config); | ||
return this; | ||
} | ||
getConfiguredProviderRegistry() { | ||
return this.setupProviderRegistry; | ||
getImports() { | ||
return this.imports; | ||
} | ||
getImportedModulesByClass(classType) { | ||
return this.getImports().filter(v => v instanceof classType); | ||
} | ||
getImportedModuleByClass(classType) { | ||
const v = this.getImports().find(v => v instanceof classType); | ||
if (!v) { | ||
throw new Error(`No module ${getClassName(classType)} in ${getClassName(this)}#${this.id} imported.`); | ||
} | ||
return v; | ||
} | ||
getImportedModule(module) { | ||
const v = this.getImports().find(v => v.id === module.id); | ||
if (!v) { | ||
throw new Error(`No module ${getClassName(module)}#${module.id} in ${getClassName(this)}#${this.id} imported.`); | ||
} | ||
return v; | ||
} | ||
getExports() { | ||
return this.exports; | ||
} | ||
hasImport(moduleClass) { | ||
for (const importModule of this.getImports()) { | ||
if (importModule instanceof moduleClass) | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Modifies this module and adds a new import, returning the same module. | ||
*/ | ||
addImport(...modules) { | ||
this.assertInjectorNotBuilt(); | ||
for (const module of modules) { | ||
module.setParent(this); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Allows to register additional setup calls for a provider in this module. | ||
* The injector token needs to be available in the local module providers. | ||
* Use setupGlobalProvider to register globally setup calls (not limited to this module only). | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupProvider(classTypeOrToken, order = 0) { | ||
return setupProvider(classTypeOrToken, this.setupProviderRegistry, order); | ||
} | ||
/** | ||
* Allows to register additional setup calls for a provider in the whole module tree. | ||
* The injector token needs to be available in the local module providers. | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupGlobalProvider(classTypeOrToken, order = 0) { | ||
return setupProvider(classTypeOrToken, this.globalSetupProviderRegistry, order); | ||
} | ||
getOrCreateInjector(buildContext) { | ||
if (this.injector) | ||
return this.injector; | ||
//notify everyone we know to prepare providers | ||
if (this.parent) | ||
this.parent.getPreparedProviders(buildContext); | ||
this.getPreparedProviders(buildContext); | ||
//handle exports, from bottom to up | ||
if (this.parent) | ||
this.parent.handleExports(buildContext); | ||
this.handleExports(buildContext); | ||
//build the injector context | ||
if (this.parent) | ||
this.parent.getOrCreateInjector(buildContext); | ||
this.injector = new Injector(this, buildContext); | ||
for (const child of this.imports) | ||
child.getOrCreateInjector(buildContext); | ||
return this.injector; | ||
} | ||
getPreparedProvider(token) { | ||
if (!this.preparedProviders) | ||
return; | ||
return this.preparedProviders.get(token); | ||
} | ||
resolveToken(token) { | ||
if (!this.preparedProviders) | ||
return; | ||
if (this.preparedProviders.has(token)) | ||
return this; | ||
if (this.parent) | ||
return this.parent.resolveToken(token); | ||
return; | ||
} | ||
/** | ||
* Prepared the module for a injector tree build. | ||
* | ||
* - Index providers by token so that last known provider is picked (so they can be overwritten). | ||
* - Register TagProvider in TagRegistry | ||
* - Put TagProvider in providers if not already made. | ||
* - Put exports to parent's module with the reference to this, so the dependencies are fetched from the correct module. | ||
*/ | ||
getPreparedProviders(buildContext) { | ||
if (this.preparedProviders) | ||
return this.preparedProviders; | ||
for (const m of this.imports) { | ||
m.getPreparedProviders(buildContext); | ||
} | ||
this.preparedProviders = new Map(); | ||
this.globalSetupProviderRegistry.mergeInto(buildContext.globalSetupProviderRegistry); | ||
//make sure that providers that declare the same provider token will be filtered out so that the last will be used. | ||
for (const provider of this.providers) { | ||
if (provider instanceof TagProvider) { | ||
buildContext.tagRegistry.register(provider, this); | ||
if (!this.preparedProviders.has(provider.provider.provide)) { | ||
//we dont want to overwrite that provider with a tag | ||
registerPreparedProvider(this.preparedProviders, [this], [provider.provider]); | ||
} | ||
} | ||
else if (isClass(provider)) { | ||
registerPreparedProvider(this.preparedProviders, [this], [{ provide: provider }]); | ||
} | ||
else { | ||
registerPreparedProvider(this.preparedProviders, [this], [provider]); | ||
} | ||
} | ||
return this.preparedProviders; | ||
} | ||
handleExports(buildContext) { | ||
if (this.exported) | ||
return; | ||
this.exported = true; | ||
//the import order is important. the last entry is the most important and should be able to overwrite | ||
//previous modules. In order to make that work, we call handleExports in reversed order. | ||
//this lets providers from the last import register their provider first, and make them available first | ||
//in the injector (which equals to be resolved first). | ||
for (let i = this.imports.length - 1; i >= 0; i--) { | ||
this.imports[i].setParent(this); | ||
this.imports[i].handleExports(buildContext); | ||
} | ||
// for (const m of this.imports) { | ||
// m.setParent(this); | ||
// m.handleExports(buildContext); | ||
// } | ||
if (!this.preparedProviders) | ||
return; | ||
if (!this.parent) | ||
return; | ||
if (this.exportsDisabled) | ||
return; | ||
const exportToken = (token, to) => { | ||
if (!this.preparedProviders) | ||
return; | ||
const preparedProvider = this.preparedProviders.get(token); | ||
//if it was not in provider, we continue | ||
if (!preparedProvider) | ||
return; | ||
//mark this provider as redirect to `exportTo` | ||
preparedProvider.resolveFrom = to; | ||
const parentProviders = to.getPreparedProviders(buildContext); | ||
const parentProvider = parentProviders.get(token); | ||
//if the parent has this token already defined, we just switch its module to ours, | ||
//so its able to inject our encapsulated services. | ||
if (parentProvider) { | ||
//we add our module as additional source for potential dependencies | ||
parentProvider.modules.push(this); | ||
} | ||
else { | ||
parentProviders.set(token, { modules: [this], providers: preparedProvider.providers.slice() }); | ||
} | ||
}; | ||
if (this.root) { | ||
const root = this.findRoot(); | ||
if (root !== this) { | ||
for (const token of this.preparedProviders.keys()) { | ||
exportToken(token, root); | ||
} | ||
} | ||
} | ||
else { | ||
for (const entry of this.exports) { | ||
if ((isClass(entry) && isPrototypeOfBase(entry, InjectorModule)) || entry instanceof InjectorModule) { | ||
const moduleInstance = isClass(entry) ? this.imports.find(v => v instanceof entry) : entry; | ||
if (!moduleInstance) { | ||
throw new Error(`Unknown module ${getClassName(entry)} exported from ${getClassName(this)}. The module was never imported.`); | ||
} | ||
//export everything to the parent that we received from that `entry` module | ||
for (const [token, preparedProvider] of this.preparedProviders.entries()) { | ||
if (preparedProvider.modules.includes(moduleInstance)) { | ||
//this provider was received from `entry` | ||
//mark this provider as redirect to `exportTo` | ||
preparedProvider.resolveFrom = this.parent; | ||
const parentProviders = this.parent.getPreparedProviders(buildContext); | ||
const parentProvider = parentProviders.get(token); | ||
//if the parent has this token already defined, we just switch its module to ours, | ||
//so its able to inject our encapsulated services. | ||
if (parentProvider) { | ||
//we add our module as additional source for potential dependencies | ||
parentProvider.modules.push(this); | ||
} | ||
else { | ||
parentProviders.set(token, { modules: [this, ...preparedProvider.modules], providers: preparedProvider.providers.slice() }); | ||
} | ||
} | ||
} | ||
} | ||
else { | ||
//export single token | ||
exportToken(entry, this.parent); | ||
} | ||
} | ||
} | ||
} | ||
findRoot() { | ||
if (this.parent) | ||
return this.parent.findRoot(); | ||
return this; | ||
} | ||
} | ||
//# sourceMappingURL=module.js.map |
@@ -1,2 +0,4 @@ | ||
import { ClassType } from '@deepkit/core'; | ||
import { AbstractClassType, ClassType } from '@deepkit/core'; | ||
import { InjectorToken } from './decorator'; | ||
import { InjectorModule } from './module'; | ||
export interface ProviderBase { | ||
@@ -9,7 +11,8 @@ /** | ||
} | ||
export declare type Token<T = any> = symbol | string | InjectorToken<T> | AbstractClassType<T>; | ||
export interface ValueProvider<T> extends ProviderBase { | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -22,5 +25,5 @@ * The value to inject. | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -33,5 +36,5 @@ * Class to instantiate for the `token`. | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -44,5 +47,5 @@ * Existing `token` to return. (equivalent to `injector.get(useExisting)`) | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
/** | ||
@@ -61,6 +64,11 @@ * A function to invoke to create a value for this `token`. The function is invoked with | ||
export declare type ProviderProvide<T = any> = ValueProvider<T> | ClassProvider<T> | ExistingProvider<T> | FactoryProvider<T>; | ||
interface TagRegistryEntry<T> { | ||
tagProvider: TagProvider<T>; | ||
module: InjectorModule; | ||
} | ||
export declare class TagRegistry { | ||
tags: TagProvider<any>[]; | ||
constructor(tags?: TagProvider<any>[]); | ||
resolve<T extends ClassType<Tag<any>>>(tag: T): TagProvider<InstanceType<T>>[]; | ||
tags: TagRegistryEntry<any>[]; | ||
constructor(tags?: TagRegistryEntry<any>[]); | ||
register(tagProvider: TagProvider<any>, module: InjectorModule): number; | ||
resolve<T extends ClassType<Tag<any>>>(tag: T): TagRegistryEntry<InstanceType<T>>[]; | ||
} | ||
@@ -91,2 +99,4 @@ export declare class TagProvider<T> { | ||
export declare function isInjectionProvider(obj: any): obj is Provider<any>; | ||
export declare function isTransient(provider: ProviderWithScope): boolean; | ||
export declare function getProviders(providers: ProviderWithScope[], requestScope: 'module' | 'session' | 'request' | string): Provider<any>[]; | ||
export {}; |
@@ -15,4 +15,7 @@ /* | ||
} | ||
register(tagProvider, module) { | ||
return this.tags.push({ tagProvider, module }); | ||
} | ||
resolve(tag) { | ||
return this.tags.filter(v => v.tag instanceof tag); | ||
return this.tags.filter(v => v.tagProvider.tag instanceof tag); | ||
} | ||
@@ -59,2 +62,9 @@ } | ||
} | ||
export function isTransient(provider) { | ||
if (isClass(provider)) | ||
return false; | ||
if (provider instanceof TagProvider) | ||
return false; | ||
return provider.transient === true; | ||
} | ||
export function getProviders(providers, requestScope) { | ||
@@ -61,0 +71,0 @@ const result = []; |
@@ -13,6 +13,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
}; | ||
import 'reflect-metadata'; | ||
import { expect, test } from '@jest/globals'; | ||
import { getClassSchema, t } from '@deepkit/type'; | ||
import 'reflect-metadata'; | ||
import { CircularDependencyError, createConfig, inject, injectable, Injector, InjectorContext } from '../src/injector'; | ||
import { CircularDependencyError, Injector } from '../src/injector'; | ||
import { inject, injectable, InjectorToken } from '../src/decorator'; | ||
import { createConfig } from '../src/config'; | ||
import { InjectorModule } from '../src/module'; | ||
@@ -30,6 +32,6 @@ export const a = 'asd'; | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
const injector = new Injector([MyServer, Connection]); | ||
const injector = Injector.from([MyServer, Connection]); | ||
expect(injector.get(Connection)).toBeInstanceOf(Connection); | ||
@@ -51,7 +53,6 @@ expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection, Missing]) | ||
], MyServer); | ||
const injector = new Injector([MyServer, Connection]); | ||
expect(() => injector.get(MyServer)).toThrow(`Unknown constructor argument 'missing: Missing' of MyServer(✓, ?). Make sure 'Missing' is provided.`); | ||
expect(() => Injector.from([MyServer, Connection])).toThrow(`Unknown dependency 'missing: Missing' of MyServer.missing`); | ||
}); | ||
@@ -69,6 +70,6 @@ test('wrong dep 1', () => { | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection, Object]) | ||
], MyServer); | ||
expect(() => new Injector([MyServer, Connection])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(✓, ?).`); | ||
expect(() => Injector.from([MyServer, Connection])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(✓, ?).`); | ||
}); | ||
@@ -82,6 +83,6 @@ test('wrong dep 2', () => { | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Object]) | ||
], MyServer); | ||
expect(() => new Injector([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(?).`); | ||
expect(() => Injector.from([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(?).`); | ||
}); | ||
@@ -96,5 +97,5 @@ test('wrong dep 3', () => { | ||
MyServer = __decorate([ | ||
injectable() | ||
injectable | ||
], MyServer); | ||
expect(() => new Injector([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer.missing.`); | ||
expect(() => Injector.from([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer.missing.`); | ||
}); | ||
@@ -109,7 +110,7 @@ test('injector key', () => { | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__param(0, inject('foo')), | ||
__metadata("design:paramtypes", [String]) | ||
], MyServer); | ||
const injector = new Injector([MyServer, { provide: 'foo', useValue: 'bar' }]); | ||
const injector = Injector.from([MyServer, { provide: 'foo', useValue: 'bar' }]); | ||
expect(injector.get('foo')).toBe('bar'); | ||
@@ -128,6 +129,6 @@ expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
const injector = new Injector([MyServer, { provide: Connection, transient: true }]); | ||
const injector = Injector.from([MyServer, { provide: Connection, transient: true }]); | ||
const c1 = injector.get(Connection); | ||
@@ -162,7 +163,7 @@ const c2 = injector.get(Connection); | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__param(0, inject('name')), | ||
__metadata("design:paramtypes", [String]) | ||
], MyServer); | ||
const injector = new Injector([MyServer, Connection, { provide: 'name', useValue: 'peter' }]); | ||
const injector = Injector.from([MyServer, Connection, { provide: 'name', useValue: 'peter' }]); | ||
const s = injector.get(MyServer); | ||
@@ -184,3 +185,3 @@ expect(s.connection).toBeInstanceOf(Connection); | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__param(0, inject(Connection2)), | ||
@@ -190,3 +191,3 @@ __metadata("design:paramtypes", [Connection]) | ||
{ | ||
const injector = new Injector([MyServer, Connection, Connection2]); | ||
const injector = Injector.from([MyServer, Connection, Connection2]); | ||
expect(injector.get(Connection)).toBeInstanceOf(Connection); | ||
@@ -206,11 +207,28 @@ expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new Injector([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow('Could not resolve injector token Connection'); | ||
expect(() => injector.get(MyServer)).toThrow(`Unknown constructor argument 'connection: Connection' of MyServer(?).`); | ||
expect(() => Injector.from([MyServer])).toThrow(`Unknown dependency 'connection: Connection' of MyServer.connection`); | ||
} | ||
}); | ||
test('injector optional unmet dependency', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
expect(connection).toBeUndefined(); | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
injectable, | ||
__param(0, t.optional), | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = Injector.from([MyServer]); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
test('injector optional dependency', () => { | ||
@@ -226,3 +244,3 @@ class Connection { | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__param(0, inject().optional), | ||
@@ -232,7 +250,72 @@ __metadata("design:paramtypes", [Connection]) | ||
{ | ||
const injector = new Injector([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow('Could not resolve injector token Connection'); | ||
const injector = Injector.from([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow(`Token 'Connection' in InjectorModule not found`); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
test('injector via t.optional', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
expect(connection).toBeUndefined(); | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
injectable, | ||
__param(0, t.optional), | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = Injector.from([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow(`Token 'Connection' in InjectorModule not found`); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
test('injector via t.type', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
injectable, | ||
__param(0, t.type(Connection)), | ||
__metadata("design:paramtypes", [Object]) | ||
], MyServer); | ||
{ | ||
const injector = Injector.from([MyServer, Connection]); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
expect(injector.get(MyServer).connection).toBeInstanceOf(Connection); | ||
} | ||
}); | ||
test('injector via t.type string', () => { | ||
class Connection { | ||
} | ||
let MyServer = class MyServer { | ||
constructor(connection) { | ||
this.connection = connection; | ||
} | ||
}; | ||
MyServer = __decorate([ | ||
injectable, | ||
__param(0, t.type('connection')), | ||
__metadata("design:paramtypes", [Object]) | ||
], MyServer); | ||
{ | ||
const injector = Injector.from([MyServer, { provide: 'connection', useClass: Connection }]); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
expect(injector.get(MyServer).connection).toBeInstanceOf(Connection); | ||
} | ||
}); | ||
test('injector token', () => { | ||
const Connection = new InjectorToken('connection'); | ||
{ | ||
const injector = Injector.from([{ provide: Connection, useValue: { doIt() { return 'hi'; } } }]); | ||
expect(injector.get(Connection).doIt()).toBe('hi'); | ||
} | ||
}); | ||
test('injector overwrite provider', () => { | ||
@@ -250,7 +333,7 @@ class Connection { | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new Injector([MyServer, { | ||
const injector = Injector.from([MyServer, { | ||
provide: Connection, useClass: Connection2 | ||
@@ -269,7 +352,7 @@ }]); | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [MyServer]) | ||
], MyServer); | ||
{ | ||
const injector = new Injector([MyServer]); | ||
const injector = Injector.from([MyServer]); | ||
expect(() => injector.get(MyServer)).toThrow(CircularDependencyError); | ||
@@ -286,3 +369,3 @@ } | ||
Connection = __decorate([ | ||
injectable(), | ||
injectable, | ||
__param(0, inject(() => MyServer)), | ||
@@ -298,7 +381,7 @@ __metadata("design:paramtypes", [Object]) | ||
MyServer = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Connection]) | ||
], MyServer); | ||
{ | ||
const injector = new Injector([MyServer, Connection]); | ||
const injector = Injector.from([MyServer, Connection]); | ||
expect(() => injector.get(MyServer)).toThrow(CircularDependencyError); | ||
@@ -312,3 +395,3 @@ expect(() => injector.get(MyServer)).toThrow('Circular dependency found MyServer -> Connection -> MyServer'); | ||
{ | ||
const injector = new Injector([{ provide: Service, useFactory: () => new Service() }]); | ||
const injector = Injector.from([{ provide: Service, useFactory: () => new Service() }]); | ||
const s1 = injector.get(Service); | ||
@@ -321,31 +404,7 @@ expect(s1).toBeInstanceOf(Service); | ||
}); | ||
test('injector stack parent', () => { | ||
const i1 = new Injector([ | ||
{ provide: 'level', deps: ['deep1'], useFactory: (d) => d }, | ||
{ provide: 'level2', deps: ['deep2'], useFactory: (d) => d }, | ||
]); | ||
const i2 = new Injector([{ provide: 'deep1', useValue: 2 }], [i1]); | ||
const i3 = new Injector([{ provide: 'deep2', useValue: 3 }], [i2]); | ||
expect(i2.get('level')).toBe(2); | ||
expect(i3.get('level')).toBe(2); | ||
expect(() => i2.get('level2')).toThrow(`Unknown factory dependency argument 'deep2' of useFactory`); | ||
expect(i3.get('level2')).toBe(3); | ||
}); | ||
test('injector stack parent fork', () => { | ||
const i1 = new Injector([ | ||
{ provide: 'level', deps: ['deep1'], useFactory: (d) => d }, | ||
{ provide: 'level2', deps: ['deep2'], useFactory: (d) => d }, | ||
]); | ||
const i2 = new Injector([{ provide: 'deep1', useValue: 2 }], [i1]).fork(); | ||
const i3 = new Injector([{ provide: 'deep2', useValue: 3 }], [i2]).fork(); | ||
expect(i2.get('level')).toBe(2); | ||
expect(i3.get('level')).toBe(2); | ||
expect(() => i2.get('level2')).toThrow(`Unknown factory dependency argument 'deep2' of useFactory`); | ||
expect(i3.get('level2')).toBe(3); | ||
}); | ||
test('injector config', () => { | ||
const FullConfig = createConfig({ | ||
const moduleConfig = createConfig({ | ||
debug: t.boolean.default(false) | ||
}); | ||
class ServiceConfig extends FullConfig.slice(['debug']) { | ||
class ServiceConfig extends moduleConfig.slice('debug') { | ||
} | ||
@@ -358,3 +417,3 @@ let MyService = class MyService { | ||
MyService = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [ServiceConfig]) | ||
@@ -368,4 +427,4 @@ ], MyService); | ||
MyService2 = __decorate([ | ||
injectable(), | ||
__param(0, inject(FullConfig)), | ||
injectable, | ||
__param(0, inject(moduleConfig)), | ||
__metadata("design:paramtypes", [Object]) | ||
@@ -379,7 +438,7 @@ ], MyService2); | ||
MyService3 = __decorate([ | ||
injectable(), | ||
__param(0, inject(FullConfig.all())), | ||
injectable, | ||
__param(0, inject(moduleConfig.all())), | ||
__metadata("design:paramtypes", [Object]) | ||
], MyService3); | ||
class Slice extends FullConfig.slice(['debug']) { | ||
class Slice extends moduleConfig.slice('debug') { | ||
} | ||
@@ -392,7 +451,8 @@ let MyService4 = class MyService4 { | ||
MyService4 = __decorate([ | ||
injectable(), | ||
injectable, | ||
__metadata("design:paramtypes", [Slice]) | ||
], MyService4); | ||
{ | ||
const i1 = new Injector([MyService, MyService2, MyService3, MyService4], []); | ||
const i1 = Injector.fromModule(new InjectorModule([MyService, MyService2, MyService3, MyService4]).setConfigDefinition(moduleConfig)); | ||
i1.module.configure({ debug: false }); | ||
expect(i1.get(MyService).config.debug).toBe(false); | ||
@@ -404,6 +464,4 @@ expect(i1.get(MyService2).config.debug).toBe(false); | ||
{ | ||
const myModule = new InjectorModule('asd', { debug: true }); | ||
const injectorContext = new InjectorContext(); | ||
injectorContext.registerModule(myModule, FullConfig); | ||
const i1 = new Injector([MyService, MyService2, MyService3, MyService4], [], injectorContext); | ||
const i1 = Injector.fromModule(new InjectorModule([MyService, MyService2, MyService3, MyService4]).setConfigDefinition(moduleConfig)); | ||
i1.module.configure({ debug: true }); | ||
expect(i1.get(MyService).config.debug).toBe(true); | ||
@@ -425,34 +483,22 @@ expect(i1.get(MyService2).config.debug).toBe(true); | ||
{ | ||
const injectorContext = new InjectorContext(); | ||
const i1 = new Injector([MyService], [], injectorContext); | ||
const i1 = Injector.from([MyService]); | ||
expect(i1.get(MyService).transporter).toEqual([]); | ||
} | ||
{ | ||
const injectorContext = new InjectorContext(); | ||
injectorContext.setupProvider(MyService).addTransporter('a'); | ||
injectorContext.setupProvider(MyService).addTransporter('b'); | ||
expect(injectorContext.configuredProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = new Injector([MyService], [], injectorContext); | ||
const module = new InjectorModule([MyService]); | ||
module.setupProvider(MyService).addTransporter('a'); | ||
module.setupProvider(MyService).addTransporter('b'); | ||
expect(module.setupProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = Injector.fromModule(module); | ||
expect(i1.get(MyService).transporter).toEqual(['a', 'b']); | ||
} | ||
{ | ||
const injectorContext = new InjectorContext(); | ||
injectorContext.setupProvider(MyService).transporter = ['a']; | ||
injectorContext.setupProvider(MyService).transporter = ['a', 'b', 'c']; | ||
expect(injectorContext.configuredProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = new Injector([MyService], [], injectorContext); | ||
const module = new InjectorModule([MyService]); | ||
module.setupProvider(MyService).transporter = ['a']; | ||
module.setupProvider(MyService).transporter = ['a', 'b', 'c']; | ||
expect(module.setupProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = Injector.fromModule(module); | ||
expect(i1.get(MyService).transporter).toEqual(['a', 'b', 'c']); | ||
} | ||
}); | ||
test('injector fork', () => { | ||
class MyService { | ||
} | ||
const i1 = new Injector([MyService]); | ||
const s1 = i1.get(MyService); | ||
expect(s1).toBeInstanceOf(MyService); | ||
const i2 = i1.fork(); | ||
const s2 = i2.get(MyService); | ||
expect(s2).toBeInstanceOf(MyService); | ||
expect(s2).not.toBe(s1); | ||
}); | ||
test('constructor one with @inject', () => { | ||
@@ -473,3 +519,3 @@ class HttpKernel { | ||
MyService = __decorate([ | ||
injectable(), | ||
injectable, | ||
__param(2, inject().optional), | ||
@@ -491,3 +537,21 @@ __metadata("design:paramtypes", [HttpKernel, | ||
} | ||
let Service = class Service { | ||
constructor(stopwatch, logger) { | ||
this.stopwatch = stopwatch; | ||
this.logger = logger; | ||
} | ||
}; | ||
Service = __decorate([ | ||
injectable, | ||
__param(1, inject(Logger)), | ||
__metadata("design:paramtypes", [Stopwatch, Object]) | ||
], Service); | ||
{ | ||
const schema = getClassSchema(Service); | ||
const methods = schema.getMethodProperties('constructor'); | ||
expect(methods.length).toBe(2); | ||
expect(methods[0].name).toBe('stopwatch'); | ||
expect(methods[1].name).toBe('logger'); | ||
} | ||
}); | ||
//# sourceMappingURL=injector.spec.js.map |
@@ -0,3 +1,5 @@ | ||
export * from './src/config'; | ||
export * from './src/decorator'; | ||
export * from './src/injector'; | ||
export * from './src/provider'; | ||
export * from './src/module'; | ||
export * from './src/provider'; |
{ | ||
"name": "@deepkit/injector", | ||
"version": "1.0.1-alpha.51", | ||
"version": "1.0.1-alpha.52", | ||
"description": "Deepkit Dependency Injection", | ||
@@ -30,7 +30,9 @@ "type": "commonjs", | ||
"devDependencies": { | ||
"@deepkit/core": "^1.0.1-alpha.48", | ||
"@deepkit/type": "^1.0.1-alpha.51", | ||
"@deepkit/core": "^1.0.1-alpha.52", | ||
"@deepkit/type": "^1.0.1-alpha.52", | ||
"benchmark": "^2.1.4", | ||
"reflect-metadata": "^0.1.13" | ||
}, | ||
"jest": { | ||
"testEnvironment": "node", | ||
"transform": { | ||
@@ -43,3 +45,3 @@ "^.+\\.(ts|tsx)$": "ts-jest" | ||
}, | ||
"gitHead": "e1758376746e55bbb23016e7a729c339985723d3" | ||
"gitHead": "537ea1f1691917da4697c8f31f8ce9f1d7c16679" | ||
} |
1134
src/injector.ts
@@ -1,837 +0,595 @@ | ||
/* | ||
* Deepkit Framework | ||
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the MIT License. | ||
* | ||
* You should have received a copy of the MIT License along with this program. | ||
*/ | ||
import { ClassSchema, ExtractClassDefinition, FieldDecoratorWrapper, getClassSchema, jsonSerializer, PlainSchemaProps, PropertySchema, t } from '@deepkit/type'; | ||
import { getProviders, isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, Provider, ProviderWithScope, Tag, TagProvider, TagRegistry } from './provider'; | ||
import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, NormalizedProvider, ProviderWithScope, Tag, TagProvider, TagRegistry, Token } from './provider'; | ||
import { ClassType, CompilerContext, CustomError, getClassName, isClass, isFunction, isPrototypeOfBase } from '@deepkit/core'; | ||
import { InjectorModule } from './module'; | ||
import { getClassSchema, isFieldDecorator, PropertySchema } from '@deepkit/type'; | ||
import { InjectOptions, InjectorReference, InjectorToken, isInjectDecorator } from './decorator'; | ||
import { ConfigDefinition, ConfigSlice, ConfigToken } from './config'; | ||
import { findModuleForConfig, getScope, InjectorModule, PreparedProvider } from './module'; | ||
export class ConfigToken<T extends {}> { | ||
constructor(public config: ConfigDefinition<T>, public name: keyof T & string) { | ||
} | ||
export class CircularDependencyError extends CustomError { | ||
} | ||
export class ConfigSlice<T extends {}> { | ||
public bag?: { [name: string]: any }; | ||
public config!: ConfigDefinition<T>; | ||
public names!: (keyof T & string)[]; | ||
export class TokenNotFoundError extends CustomError { | ||
} | ||
constructor(config: ConfigDefinition<T>, names: (keyof T & string)[]) { | ||
//we want that ConfigSlice acts as a regular plain object, which can be serialized at wish. | ||
Object.defineProperties(this, { | ||
config: { enumerable: false, value: config }, | ||
names: { enumerable: false, value: names }, | ||
bag: { enumerable: false, writable: true }, | ||
}); | ||
export class DependenciesUnmetError extends CustomError { | ||
} | ||
for (const name of names) { | ||
Object.defineProperty(this, name, { | ||
enumerable: true, | ||
get: () => { | ||
return this.bag ? this.bag[name] : undefined; | ||
} | ||
}); | ||
} | ||
} | ||
export function tokenLabel(token: any): string { | ||
if (token === null) return 'null'; | ||
if (token === undefined) return 'undefined'; | ||
if (token instanceof TagProvider) return 'Tag(' + getClassName(token.provider.provide) + ')'; | ||
if (isClass(token)) return getClassName(token); | ||
if (isFunction(token.toString)) return token.toString(); | ||
valueOf() { | ||
return { ...this }; | ||
} | ||
return token + ''; | ||
} | ||
export class ConfigDefinition<T extends {}> { | ||
protected module?: InjectorModule; | ||
function constructorParameterNotFound(ofName: string, name: string, position: number, token: any) { | ||
const argsCheck: string[] = []; | ||
for (let i = 0; i < position; i++) argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
public type!: T; | ||
constructor( | ||
public readonly schema: ClassSchema<T> | ||
) { | ||
} | ||
setModule(module: InjectorModule) { | ||
this.module = module; | ||
} | ||
hasModule(): boolean { | ||
return this.module !== undefined; | ||
} | ||
getModule(): InjectorModule { | ||
if (!this.module) throw new Error('ConfigDefinition module not set. Make sure your config is assigned to a single module. See createModule({config: x}).'); | ||
return this.module; | ||
} | ||
getConfigOrDefaults(): any { | ||
if (this.module) return this.module.getConfig(); | ||
return jsonSerializer.for(this.schema).validatedDeserialize({}); | ||
} | ||
all(): ClassType<T> { | ||
const self = this; | ||
return class extends ConfigSlice<T> { | ||
constructor() { | ||
super(self, [...self.schema.getProperties()].map(v => v.name) as any); | ||
} | ||
} as any; | ||
} | ||
slice<N extends (keyof T & string)[]>(names: N): ClassType<Pick<T, N[number]>> { | ||
const self = this; | ||
return class extends ConfigSlice<T> { | ||
constructor() { | ||
super(self, names); | ||
} | ||
} as any; | ||
} | ||
token<N extends (keyof T & string)>(name: N): ConfigToken<T> { | ||
return new ConfigToken(this, name); | ||
} | ||
throw new DependenciesUnmetError( | ||
`Unknown constructor argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
export class InjectorReference { | ||
constructor(public readonly to: any) { | ||
} | ||
function tokenNotfoundError(token: any, moduleName: string) { | ||
throw new TokenNotFoundError( | ||
`Token '${tokenLabel(token)}' in ${moduleName} not found. Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
export function injectorReference<T>(classTypeOrToken: T): any { | ||
return new InjectorReference(classTypeOrToken); | ||
} | ||
function factoryDependencyNotFound(ofName: string, name: string, position: number, token: any) { | ||
const argsCheck: string[] = []; | ||
for (let i = 0; i < position; i++) argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
export function createConfig<T extends PlainSchemaProps>(config: T): ConfigDefinition<ExtractClassDefinition<T>> { | ||
return new ConfigDefinition(t.schema(config)); | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new DependenciesUnmetError( | ||
`Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
export interface InjectDecorator { | ||
(target: object, property?: string, parameterIndexOrDescriptor?: any): any; | ||
/** | ||
* Mark as optional. | ||
*/ | ||
readonly optional: this; | ||
/** | ||
* Resolves the dependency token from the root injector. | ||
*/ | ||
readonly root: this; | ||
readonly options: {token: any, optional: boolean, root: boolean}; | ||
function propertyParameterNotFound(ofName: string, name: string, position: number, token: any) { | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new DependenciesUnmetError( | ||
`Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
export type InjectOptions = { | ||
token: any | ForwardRef<any>; | ||
optional: boolean; | ||
root: boolean; | ||
}; | ||
type ForwardRef<T> = () => T; | ||
let CircularDetector: any[] = []; | ||
let CircularDetectorResets: (() => void)[] = []; | ||
const injectSymbol = Symbol('inject'); | ||
export function isInjectDecorator(v: any): v is InjectDecorator { | ||
return isFunction(v) && v.hasOwnProperty(injectSymbol); | ||
function throwCircularDependency() { | ||
const path = CircularDetector.map(tokenLabel).join(' -> '); | ||
CircularDetector.length = 0; | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new CircularDependencyError(`Circular dependency found ${path}`); | ||
} | ||
export function inject(token?: any | ForwardRef<any>): InjectDecorator { | ||
const injectOptions: InjectOptions = { | ||
optional: false, | ||
root: false, | ||
token: token, | ||
}; | ||
export type SetupProviderCalls = { | ||
type: 'call', methodName: string | symbol | number, args: any[], order: number | ||
} | ||
| { type: 'property', property: string | symbol | number, value: any, order: number } | ||
| { type: 'stop', order: number } | ||
; | ||
const fn = (target: object, propertyOrMethodName?: string, parameterIndexOrDescriptor?: any) => { | ||
FieldDecoratorWrapper((target: object, property, returnType) => { | ||
property.data['deepkit/inject'] = injectOptions; | ||
property.setFromJSType(returnType); | ||
})(target, propertyOrMethodName, parameterIndexOrDescriptor); | ||
}; | ||
Object.defineProperty(fn, injectSymbol, {value: true, enumerable: false}); | ||
export class SetupProviderRegistry { | ||
public calls = new Map<Token, SetupProviderCalls[]>(); | ||
Object.defineProperty(fn, 'optional', { | ||
get() { | ||
injectOptions.optional = true; | ||
return fn; | ||
} | ||
}); | ||
public add(token: any, ...newCalls: SetupProviderCalls[]) { | ||
this.get(token).push(...newCalls); | ||
} | ||
Object.defineProperty(fn, 'options', { | ||
get() { | ||
return injectOptions; | ||
mergeInto(registry: SetupProviderRegistry): void { | ||
for (const [token, calls] of this.calls) { | ||
registry.add(token, ...calls); | ||
} | ||
}); | ||
} | ||
Object.defineProperty(fn, 'root', { | ||
get() { | ||
injectOptions.optional = true; | ||
return fn; | ||
public get(token: Token): SetupProviderCalls[] { | ||
let calls = this.calls.get(token); | ||
if (!calls) { | ||
calls = []; | ||
this.calls.set(token, calls); | ||
} | ||
}); | ||
return fn as InjectDecorator; | ||
} | ||
export class InjectToken { | ||
constructor(public readonly name: string) { | ||
return calls; | ||
} | ||
toString() { | ||
return 'InjectToken=' + this.name; | ||
} | ||
} | ||
export function injectable() { | ||
return (target: object) => { | ||
//don't do anything. This is just used to generate type metadata. | ||
}; | ||
interface Scope { | ||
name: string; | ||
instances: { [name: string]: any }; | ||
} | ||
export class CircularDependencyError extends CustomError { | ||
} | ||
export type ResolveToken<T> = T extends ClassType<infer R> ? R : T extends InjectorToken<infer R> ? R : T; | ||
export class TokenNotFoundError extends CustomError { | ||
} | ||
export function resolveToken(provider: ProviderWithScope): Token { | ||
if (isClass(provider)) return provider; | ||
if (provider instanceof TagProvider) return resolveToken(provider.provider); | ||
export class DependenciesUnmetError extends CustomError { | ||
return provider.provide; | ||
} | ||
export function tokenLabel(token: any): string { | ||
if (token === null) return 'null'; | ||
if (token === undefined) return 'undefined'; | ||
if (token instanceof TagProvider) return 'Tag(' + getClassName(token.provider.provide) + ')'; | ||
if (isClass(token)) return getClassName(token); | ||
if (isFunction(token.toString)) return token.toString(); | ||
return token + ''; | ||
export interface InjectorInterface { | ||
get<T>(token: T, scope?: Scope): ResolveToken<T>; | ||
} | ||
export interface ConfigContainer { | ||
get(path: string): any; | ||
} | ||
/** | ||
* This is the actual dependency injection container. | ||
* Every module has its own injector. | ||
*/ | ||
export class Injector implements InjectorInterface { | ||
private resolver?: (token: any, scope?: Scope) => any; | ||
private setter?: (token: any, value: any, scope?: Scope) => any; | ||
let CircularDetector: any[] = []; | ||
let CircularDetectorResets: (() => void)[] = []; | ||
/** | ||
* All unscoped provider instances. Scoped instances are attached to `Scope`. | ||
*/ | ||
private instances: { [name: string]: any } = {}; | ||
export interface BasicInjector { | ||
get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: BasicInjector): R; | ||
getInjectorForModule(module: InjectorModule): BasicInjector; | ||
} | ||
export class Injector implements BasicInjector { | ||
public circularCheck: boolean = true; | ||
protected resolved: any[] = []; | ||
protected retriever(injector: Injector, token: any, frontInjector?: Injector): any { | ||
for (const parent of injector.parents) { | ||
const v = 'retriever' in parent ? parent.retriever(parent, token, frontInjector) : parent.get(token, frontInjector); | ||
if (v !== undefined) return v; | ||
} | ||
return undefined; | ||
} | ||
constructor( | ||
protected providers: Provider[] = [], | ||
protected parents: (BasicInjector | Injector)[] = [], | ||
protected injectorContext: InjectorContext = new InjectorContext, | ||
protected configuredProviderRegistry: ConfiguredProviderRegistry | undefined = undefined, | ||
protected tagRegistry: TagRegistry = new TagRegistry(), | ||
protected contextResolver?: { getInjectorForModule(module: InjectorModule): BasicInjector } | ||
public readonly module: InjectorModule, | ||
private buildContext: BuildContext, | ||
) { | ||
if (!this.configuredProviderRegistry) this.configuredProviderRegistry = injectorContext.configuredProviderRegistry; | ||
if (this.providers.length) this.retriever = this.buildRetriever(); | ||
module.injector = this; | ||
this.build(buildContext); | ||
} | ||
getInjectorForModule(module: InjectorModule): BasicInjector { | ||
return this.contextResolver ? this.contextResolver.getInjectorForModule(module) : this; | ||
static from(providers: ProviderWithScope[], parent?: Injector): Injector { | ||
return new Injector(new InjectorModule(providers, parent?.module), new BuildContext); | ||
} | ||
/** | ||
* Creates a clone of this instance, maintains the provider structure, but drops provider instances. | ||
* Note: addProviders() in the new fork changes the origin, since providers array is not cloned. | ||
*/ | ||
public fork(parents?: Injector[], injectorContext?: InjectorContext) { | ||
const injector = new Injector(undefined, parents || this.parents, injectorContext, this.configuredProviderRegistry, this.tagRegistry, this.contextResolver); | ||
injector.providers = this.providers; | ||
injector.retriever = this.retriever; | ||
return injector; | ||
static fromModule(module: InjectorModule, parent?: Injector): Injector { | ||
return new Injector(module, new BuildContext); | ||
} | ||
/** | ||
* Changes the provider structure of this injector. | ||
* | ||
* Note: This is very performance sensitive. Every time you call this function a new dependency injector function | ||
* is generated, which si pretty slow. So, it's recommended to create a Injector with providers in the constructor | ||
* and not change it. | ||
*/ | ||
public addProviders(...providers: Provider[]) { | ||
this.providers.push(...providers); | ||
this.retriever = this.buildRetriever(); | ||
get<T>(token: T, scope?: Scope): ResolveToken<T> { | ||
if (!this.resolver) throw new Error('Injector was not built'); | ||
return this.resolver(token, scope); | ||
} | ||
public isRoot() { | ||
return this.parents.length === 0; | ||
set<T>(token: T, value: any, scope?: Scope): void { | ||
if (!this.setter) throw new Error('Injector was not built'); | ||
this.setter(token, value, scope); | ||
} | ||
protected createFactoryProperty(options: {name: string | number, token: any, optional: boolean}, compiler: CompilerContext, ofName: string, argPosition: number, notFoundFunction: string) { | ||
const token = options.token; | ||
if (token instanceof ConfigDefinition) { | ||
if (token.hasModule()) { | ||
const module = this.injectorContext.getModule(token.getModule().getName()); | ||
return compiler.reserveVariable('fullConfig', module.getConfig()); | ||
} else { | ||
return compiler.reserveVariable('fullConfig', token.getConfigOrDefaults()); | ||
} | ||
} else if (token instanceof ConfigToken) { | ||
if (token.config.hasModule()) { | ||
const module = this.injectorContext.getModule(token.config.getModule().getName()); | ||
const config = module.getConfig(); | ||
return compiler.reserveVariable(token.name, (config as any)[token.name]); | ||
} else { | ||
const config = token.config.getConfigOrDefaults(); | ||
return compiler.reserveVariable(token.name, (config as any)[token.name]); | ||
} | ||
} else if (isClass(token) && (Object.getPrototypeOf(Object.getPrototypeOf(token)) === ConfigSlice || Object.getPrototypeOf(token) === ConfigSlice)) { | ||
const value: ConfigSlice<any> = new token; | ||
if (!value.bag) { | ||
if (value.config.hasModule()) { | ||
const module = this.injectorContext.getModule(value.config.getModule().getName()); | ||
value.bag = module.getConfig(); | ||
} else { | ||
value.bag = value.config.getConfigOrDefaults(); | ||
} | ||
return compiler.reserveVariable('configSlice', value); | ||
} | ||
} else if (token === TagRegistry) { | ||
return compiler.reserveVariable('tagRegistry', this.tagRegistry); | ||
} else if (isPrototypeOfBase(token, Tag)) { | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const providers = compiler.reserveVariable('tagRegistry', this.tagRegistry.resolve(token)); | ||
return `new ${tokenVar}(${providers}.map(v => (frontInjector.retriever ? frontInjector.retriever(frontInjector, v, frontInjector) : frontInjector.get(v, frontInjector))))`; | ||
} else { | ||
if (token === undefined) { | ||
let of = `${ofName}.${options.name}`; | ||
if (argPosition >= 0) { | ||
const argsCheck: string[] = []; | ||
for (let i = 0; i < argPosition; i++) argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
of = `${ofName}(${argsCheck.join(', ')})`; | ||
} | ||
throw new DependenciesUnmetError( | ||
`Undefined dependency '${options.name}: undefined' of ${of}. Dependency '${options.name}' has no type. Imported reflect-metadata correctly? `+ | ||
`Use '@inject(PROVIDER) ${options.name}: T' if T is an interface. For circular references use @inject(() => T) ${options.name}: T.` | ||
); | ||
} | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; | ||
return `(frontInjector.retriever ? frontInjector.retriever(frontInjector, ${tokenVar}, frontInjector) : frontInjector.get(${tokenVar}, frontInjector)) ${orThrow}`; | ||
} | ||
return 'undefined'; | ||
clear() { | ||
this.instances = {}; | ||
} | ||
protected optionsFromProperty(property: PropertySchema): {token: any, name: string|number, optional: boolean} { | ||
const options = property.data['deepkit/inject'] as InjectOptions | undefined; | ||
let token: any = property.resolveClassType; | ||
protected build(buildContext: BuildContext): void { | ||
const resolverCompiler = new CompilerContext(); | ||
resolverCompiler.context.set('CircularDetector', CircularDetector); | ||
resolverCompiler.context.set('CircularDetectorResets', CircularDetectorResets); | ||
resolverCompiler.context.set('throwCircularDependency', throwCircularDependency); | ||
resolverCompiler.context.set('tokenNotfoundError', tokenNotfoundError); | ||
resolverCompiler.context.set('injector', this); | ||
if (options && options.token) { | ||
token = isFunction(options.token) ? options.token() : options.token; | ||
} | ||
const lines: string[] = []; | ||
const resets: string[] = []; | ||
const creating: string[] = []; | ||
return {token, name: property.name, optional: !!options && options.optional}; | ||
} | ||
const setterCompiler = new CompilerContext(); | ||
setterCompiler.context.set('injector', this); | ||
const setterLines: string[] = []; | ||
protected createFactory(compiler: CompilerContext, classType: ClassType): string { | ||
if (!classType) throw new Error('Can not create factory for undefined ClassType'); | ||
const schema = getClassSchema(classType); | ||
const args: string[] = []; | ||
const propertyAssignment: string[] = []; | ||
const classTypeVar = compiler.reserveVariable('classType', classType); | ||
for (const [token, prepared] of this.module.getPreparedProviders(buildContext).entries()) { | ||
//scopes will be created first, so they are returned instead of the unscoped instance | ||
prepared.providers.sort((a, b) => { | ||
if (a.scope && !b.scope) return -1; | ||
if (!a.scope && b.scope) return +1; | ||
return 0; | ||
}); | ||
for (const property of schema.getMethodProperties('constructor')) { | ||
args.push(this.createFactoryProperty(this.optionsFromProperty(property), compiler, getClassName(classType), args.length, 'constructorParameterNotFound')); | ||
} | ||
for (const provider of prepared.providers) { | ||
const scope = getScope(provider); | ||
const name = 'i' + this.buildContext.providerIndex.reserve(); | ||
creating.push(`let creating_${name} = false;`); | ||
resets.push(`creating_${name} = false;`); | ||
const accessor = scope ? 'scope.instances.' + name : 'injector.instances.' + name; | ||
for (const property of schema.getProperties()) { | ||
if (!('deepkit/inject' in property.data)) continue; | ||
if (property.methodName === 'constructor') continue; | ||
propertyAssignment.push(`v.${property.name} = ${this.createFactoryProperty(this.optionsFromProperty(property), compiler, getClassName(classType), -1, 'propertyParameterNotFound')};`); | ||
} | ||
setterLines.push(`case ${setterCompiler.reserveVariable('token', token)}: { | ||
${accessor} = value; | ||
break; | ||
}`); | ||
return `v = new ${classTypeVar}(${args.join(',')});\n${propertyAssignment.join('\n')}`; | ||
} | ||
if (prepared.resolveFrom) { | ||
//its a redirect | ||
lines.push(` | ||
case token === ${resolverCompiler.reserveConst(token)}: { | ||
return ${resolverCompiler.reserveConst(prepared.resolveFrom)}.injector.resolver(${resolverCompiler.reserveConst(token)}, scope); | ||
} | ||
`); | ||
protected buildRetriever(): (injector: Injector, token: any, frontInjector?: Injector) => any { | ||
const compiler = new CompilerContext(); | ||
const lines: string[] = []; | ||
const resets: string[] = []; | ||
this.resolved = []; | ||
lines.push(` | ||
case ${compiler.reserveVariable('injectorContextClassType', InjectorContext)}: return injector.injectorContext; | ||
case ${compiler.reserveVariable('injectorClassType', Injector)}: return injector; | ||
`); | ||
let resolvedIds = 0; | ||
const normalizedProviders = new Map<any, Provider>(); | ||
//make sure that providers that declare the same provider token will be filtered out so that the last will be used. | ||
for (const provider of this.providers) { | ||
if (provider instanceof TagProvider) { | ||
normalizedProviders.set(provider, provider); | ||
} else if (isValueProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} else if (isClassProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} else if (isExistingProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} else if (isFactoryProvider(provider)) { | ||
normalizedProviders.set(provider.provide, provider); | ||
} else if (isClass(provider)) { | ||
normalizedProviders.set(provider, provider); | ||
} else { | ||
//we own and instantiate the service | ||
lines.push(this.buildProvider(buildContext, resolverCompiler, name, accessor, scope, provider, prepared.modules)); | ||
} | ||
} | ||
} | ||
for (let provider of normalizedProviders.values()) { | ||
const resolvedId = resolvedIds++; | ||
this.resolved.push(undefined); | ||
let transient = false; | ||
let factory = ''; | ||
let token: any; | ||
const tagToken = provider instanceof TagProvider ? provider : undefined; | ||
if (provider instanceof TagProvider) { | ||
provider = provider.provider; | ||
const setter = setterCompiler.build(` | ||
switch (token) { | ||
${setterLines.join('\n')} | ||
} | ||
`, 'token', 'value', 'scope'); | ||
if (isValueProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
const valueVar = compiler.reserveVariable('useValue', provider.useValue); | ||
factory = `v = ${valueVar};`; | ||
} else if (isClassProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
factory = this.createFactory(compiler, provider.useClass || provider.provide); | ||
} else if (isExistingProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
factory = this.createFactory(compiler, provider.useExisting); | ||
} else if (isFactoryProvider(provider)) { | ||
transient = provider.transient === true; | ||
token = provider.provide; | ||
const resolver = resolverCompiler.raw(` | ||
${creating.join('\n')}; | ||
const args: string[] = []; | ||
let i = 0; | ||
for (const dep of provider.deps || []) { | ||
let optional = false | ||
let token = dep; | ||
CircularDetectorResets.push(() => { | ||
${resets.join('\n')}; | ||
}); | ||
if (isInjectDecorator(dep)) { | ||
optional = dep.options.optional; | ||
token = dep.options.token; | ||
} | ||
if (!token) { | ||
throw new Error(`No token defined for dependency ${i} in 'deps' of useFactory for ${tokenLabel(provider.provide)}`); | ||
} | ||
args.push(this.createFactoryProperty({ | ||
name: i++, | ||
token, | ||
optional, | ||
}, compiler, 'useFactory', args.length, 'factoryDependencyNotFound')); | ||
return function(token, scope) { | ||
switch (true) { | ||
${lines.join('\n')} | ||
} | ||
factory = `v = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; | ||
} else if (isClass(provider)) { | ||
token = provider; | ||
factory = this.createFactory(compiler, provider); | ||
} else { | ||
throw new Error('Invalid provider'); | ||
tokenNotfoundError(token, '${getClassName(this.module)}'); | ||
} | ||
`) as any; | ||
if (tagToken) token = tagToken; | ||
this.setter = setter; | ||
this.resolver = resolver; | ||
} | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const creatingVar = compiler.reserveVariable('creating', false); | ||
const configuredProviderCalls = this.configuredProviderRegistry?.get(token); | ||
protected buildProvider( | ||
buildContext: BuildContext, | ||
compiler: CompilerContext, | ||
name: string, | ||
accessor: string, | ||
scope: string, | ||
provider: NormalizedProvider, | ||
resolveDependenciesFrom: InjectorModule[], | ||
) { | ||
let transient = false; | ||
const token = provider.provide; | ||
let factory: { code: string, dependencies: number } = { code: '', dependencies: 0 }; | ||
const tokenVar = compiler.reserveConst(token); | ||
const configureProvider: string[] = []; | ||
if (configuredProviderCalls) { | ||
configuredProviderCalls.sort((a, b) => { | ||
return a.order - b.order; | ||
}); | ||
if (isValueProvider(provider)) { | ||
transient = provider.transient === true; | ||
const valueVar = compiler.reserveVariable('useValue', provider.useValue); | ||
factory.code = `${accessor} = ${valueVar};`; | ||
} else if (isClassProvider(provider)) { | ||
transient = provider.transient === true; | ||
for (const call of configuredProviderCalls) { | ||
if (call.type === 'stop') break; | ||
if (call.type === 'call') { | ||
const args: string[] = []; | ||
const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; | ||
for (const arg of call.args) { | ||
if (arg instanceof InjectorReference) { | ||
args.push(`frontInjector.get(${compiler.reserveVariable('forward', arg.to)})`); | ||
} else { | ||
args.push(`${compiler.reserveVariable('arg', arg)}`); | ||
} | ||
} | ||
configureProvider.push(`v.${methodName}(${args.join(', ')});`); | ||
} | ||
if (call.type === 'property') { | ||
const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; | ||
const value = call.value instanceof InjectorReference ? `frontInjector.get(${compiler.reserveVariable('forward', call.value.to)})` : compiler.reserveVariable('value', call.value); | ||
configureProvider.push(`v.${property} = ${value};`); | ||
} | ||
let useClass = provider.useClass; | ||
if (!useClass) { | ||
if (!isClass(provider.provide)) { | ||
throw new Error(`UseClassProvider needs to set either 'useClass' or 'provide' as a ClassType. Got ${provider.provide as any}`); | ||
} | ||
} else { | ||
configureProvider.push('//no custom provider setup'); | ||
useClass = provider.provide; | ||
} | ||
factory = this.createFactory(provider, accessor, compiler, useClass, resolveDependenciesFrom); | ||
} else if (isExistingProvider(provider)) { | ||
transient = provider.transient === true; | ||
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(provider.useExisting)}, scope)`; | ||
} else if (isFactoryProvider(provider)) { | ||
transient = provider.transient === true; | ||
resets.push(`${creatingVar} = false;`); | ||
const args: string[] = []; | ||
let i = 0; | ||
for (const dep of provider.deps || []) { | ||
let optional = false; | ||
let token = dep; | ||
lines.push(` | ||
//${tokenLabel(token)} | ||
case ${tokenVar}: { | ||
${transient ? 'let v;' : `let v = injector.resolved[${resolvedId}]; if (v !== undefined) return v;`} | ||
CircularDetector.push(${tokenVar}); | ||
if (${creatingVar}) { | ||
throwCircularDependency(); | ||
if (isInjectDecorator(dep)) { | ||
optional = dep.options.optional; | ||
token = dep.options.token; | ||
} | ||
if (isFieldDecorator(dep)) { | ||
const propertySchema = dep.buildPropertySchema(); | ||
optional = propertySchema.isOptional; | ||
if (propertySchema.type === 'literal' || propertySchema.type === 'class') { | ||
token = propertySchema.literalValue !== undefined ? propertySchema.literalValue : propertySchema.getResolvedClassType(); | ||
} | ||
${creatingVar} = true; | ||
${factory} | ||
${transient ? '' : `injector.resolved[${resolvedId}] = v;`} | ||
${creatingVar} = false; | ||
${configureProvider.join('\n')} | ||
CircularDetector.pop(); | ||
return v; | ||
} | ||
`); | ||
} | ||
const parents: string[] = []; | ||
for (let i = 0; i < this.parents.length; i++) { | ||
let retriever = 'retriever' in this.parents[i] ? `injector.parents[${i}].retriever(injector.parents[${i}], ` : `injector.parents[${i}].get(`; | ||
parents.push(` | ||
{ | ||
const v = ${retriever}token, frontInjector); | ||
if (v !== undefined) return v; | ||
if (!token) { | ||
throw new Error(`No token defined for dependency ${i} in 'deps' of useFactory for ${tokenLabel(provider.provide)}`); | ||
} | ||
`); | ||
factory.dependencies++; | ||
args.push(this.createFactoryProperty({ | ||
name: i++, | ||
token, | ||
optional, | ||
}, provider, compiler, resolveDependenciesFrom, 'useFactory', args.length, 'factoryDependencyNotFound')); | ||
} | ||
factory.code = `${accessor} = ${compiler.reserveVariable('factory', provider.useFactory)}(${args.join(', ')});`; | ||
} else { | ||
throw new Error('Invalid provider'); | ||
} | ||
compiler.context.set('CircularDetector', CircularDetector); | ||
compiler.context.set('throwCircularDependency', throwCircularDependency); | ||
compiler.context.set('CircularDetectorResets', CircularDetectorResets); | ||
compiler.context.set('constructorParameterNotFound', constructorParameterNotFound); | ||
compiler.context.set('factoryDependencyNotFound', factoryDependencyNotFound); | ||
compiler.context.set('propertyParameterNotFound', propertyParameterNotFound); | ||
const configureProvider: string[] = []; | ||
const configuredProviderCalls = resolveDependenciesFrom[0].setupProviderRegistry?.get(token); | ||
configuredProviderCalls.push(...buildContext.globalSetupProviderRegistry.get(token)); | ||
compiler.preCode = ` | ||
CircularDetectorResets.push(() => { | ||
${resets.join('\n')}; | ||
if (configuredProviderCalls) { | ||
configuredProviderCalls.sort((a, b) => { | ||
return a.order - b.order; | ||
}); | ||
`; | ||
return compiler.build(` | ||
frontInjector = frontInjector || injector; | ||
for (const call of configuredProviderCalls) { | ||
if (call.type === 'stop') break; | ||
if (call.type === 'call') { | ||
const args: string[] = []; | ||
const methodName = 'symbol' === typeof call.methodName ? '[' + compiler.reserveVariable('arg', call.methodName) + ']' : call.methodName; | ||
for (const arg of call.args) { | ||
if (arg instanceof InjectorReference) { | ||
const injector = arg.module ? compiler.reserveConst(arg.module) + '.injector' : 'injector'; | ||
args.push(`${injector}.resolver(${compiler.reserveConst(arg.to)}, scope)`); | ||
} else { | ||
args.push(`${compiler.reserveVariable('arg', arg)}`); | ||
} | ||
} | ||
switch (token) { | ||
${lines.join('\n')} | ||
configureProvider.push(`${accessor}.${methodName}(${args.join(', ')});`); | ||
} | ||
if (call.type === 'property') { | ||
const property = 'symbol' === typeof call.property ? '[' + compiler.reserveVariable('property', call.property) + ']' : call.property; | ||
const value = call.value instanceof InjectorReference ? `frontInjector.get(${compiler.reserveVariable('forward', call.value.to)})` : compiler.reserveVariable('value', call.value); | ||
configureProvider.push(`${accessor}.${property} = ${value};`); | ||
} | ||
} | ||
} else { | ||
configureProvider.push('//no custom provider setup'); | ||
} | ||
${parents.join('\n')} | ||
const scopeCheck = scope ? ` && scope && scope.name === ${JSON.stringify(scope)}` : ''; | ||
return undefined; | ||
`, 'injector', 'token', 'frontInjector') as any; | ||
} | ||
//circular dependencies can happen, when for example a service with InjectorContext injected manually instantiates a service. | ||
//if that service references back to the first one, it will be a circular loop. So we track that with `creating` state. | ||
const creatingVar = `creating_${name}`; | ||
const circularDependencyCheckStart = factory.dependencies ? `if (${creatingVar}) throwCircularDependency();${creatingVar} = true;` : ''; | ||
const circularDependencyCheckEnd = factory.dependencies ? `${creatingVar} = false;` : ''; | ||
public get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R { | ||
const v = this.retriever(this, token, frontInjector || this); | ||
if (v !== undefined) return v; | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new TokenNotFoundError(`Could not resolve injector token ${tokenLabel(token)}`); | ||
return ` | ||
//${tokenLabel(token)} | ||
case token === ${tokenVar}${scopeCheck}: { | ||
${!transient ? `if (${accessor} !== undefined) return ${accessor};` : ''} | ||
CircularDetector.push(${tokenVar}); | ||
${circularDependencyCheckStart} | ||
${factory.code} | ||
${circularDependencyCheckEnd} | ||
CircularDetector.pop(); | ||
${configureProvider.join('\n')} | ||
return ${accessor}; | ||
} | ||
`; | ||
} | ||
} | ||
function constructorParameterNotFound(ofName: string, name: string, position: number, token: any) { | ||
const argsCheck: string[] = []; | ||
for (let i = 0; i < position; i++) argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
protected createFactory( | ||
provider: NormalizedProvider, | ||
resolvedName: string, | ||
compiler: CompilerContext, | ||
classType: ClassType, | ||
resolveDependenciesFrom: InjectorModule[] | ||
): { code: string, dependencies: number } { | ||
if (!classType) throw new Error('Can not create factory for undefined ClassType'); | ||
const schema = getClassSchema(classType); | ||
const args: string[] = []; | ||
const propertyAssignment: string[] = []; | ||
const classTypeVar = compiler.reserveVariable('classType', classType); | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new DependenciesUnmetError( | ||
`Unknown constructor argument '${name}: ${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
let dependencies: number = 0; | ||
function factoryDependencyNotFound(ofName: string, name: string, position: number, token: any) { | ||
const argsCheck: string[] = []; | ||
for (let i = 0; i < position; i++) argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
for (const property of schema.getMethodProperties('constructor')) { | ||
if (!property) { | ||
throw new Error(`Constructor arguments hole in ${getClassName(classType)}`); | ||
} | ||
dependencies++; | ||
args.push(this.createFactoryProperty(this.optionsFromProperty(property), provider, compiler, resolveDependenciesFrom, getClassName(classType), args.length, 'constructorParameterNotFound')); | ||
} | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new DependenciesUnmetError( | ||
`Unknown factory dependency argument '${tokenLabel(token)}' of ${ofName}(${argsCheck.join(', ')}). Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
for (const property of schema.getProperties()) { | ||
if (!('deepkit/inject' in property.data)) continue; | ||
if (property.methodName === 'constructor') continue; | ||
dependencies++; | ||
try { | ||
const resolveProperty = this.createFactoryProperty(this.optionsFromProperty(property), provider, compiler, resolveDependenciesFrom, getClassName(classType), -1, 'propertyParameterNotFound'); | ||
propertyAssignment.push(`${resolvedName}.${property.name} = ${resolveProperty};`); | ||
} catch (error) { | ||
throw new Error(`Could not resolve property injection token ${getClassName(classType)}.${property.name}: ${error.message}`); | ||
} | ||
} | ||
function propertyParameterNotFound(ofName: string, name: string, position: number, token: any) { | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new DependenciesUnmetError( | ||
`Unknown property parameter ${name} of ${ofName}. Make sure '${tokenLabel(token)}' is provided.` | ||
); | ||
} | ||
function throwCircularDependency() { | ||
const path = CircularDetector.map(tokenLabel).join(' -> '); | ||
CircularDetector.length = 0; | ||
for (const reset of CircularDetectorResets) reset(); | ||
throw new CircularDependencyError(`Circular dependency found ${path}`); | ||
} | ||
export class MemoryInjector extends Injector { | ||
constructor(protected providers: ({ provide: any, useValue: any } | { provide: any, useFactory: () => any })[]) { | ||
super(); | ||
return { | ||
code: `${resolvedName} = new ${classTypeVar}(${args.join(',')});\n${propertyAssignment.join('\n')}`, | ||
dependencies | ||
}; | ||
} | ||
fork(parents?: Injector[]): Injector { | ||
return this; | ||
} | ||
protected createFactoryProperty( | ||
options: { name: string | number, token: any, optional: boolean }, | ||
fromProvider: NormalizedProvider, | ||
compiler: CompilerContext, | ||
resolveDependenciesFrom: InjectorModule[], | ||
ofName: string, | ||
argPosition: number, | ||
notFoundFunction: string | ||
): string { | ||
const token = options.token; | ||
protected retriever(injector: Injector, token: any) { | ||
for (const p of this.providers) { | ||
if (p.provide === token) return 'useFactory' in p ? p.useFactory() : p.useValue; | ||
} | ||
} | ||
//regarding configuration values: the attached module is not necessarily in resolveDependenciesFrom[0] | ||
//if the parent module overwrites its, then the parent module is at 0th position. | ||
if (token instanceof ConfigDefinition) { | ||
const module = findModuleForConfig(token, resolveDependenciesFrom); | ||
return compiler.reserveVariable('fullConfig', module.getConfig()); | ||
} else if (token instanceof ConfigToken) { | ||
const module = findModuleForConfig(token.config, resolveDependenciesFrom); | ||
const config = module.getConfig(); | ||
return compiler.reserveVariable(token.name, (config as any)[token.name]); | ||
} else if (isClass(token) && (Object.getPrototypeOf(Object.getPrototypeOf(token)) === ConfigSlice || Object.getPrototypeOf(token) === ConfigSlice)) { | ||
const value: ConfigSlice<any> = new token; | ||
const module = findModuleForConfig(value.config, resolveDependenciesFrom); | ||
value.bag = module.getConfig(); | ||
return compiler.reserveVariable('configSlice', value); | ||
} else if (token === TagRegistry) { | ||
return compiler.reserveVariable('tagRegistry', this.buildContext.tagRegistry); | ||
} else if (isPrototypeOfBase(token, Tag)) { | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
const resolvedVar = compiler.reserveVariable('tagResolved'); | ||
const entries = this.buildContext.tagRegistry.resolve(token); | ||
const args: string[] = []; | ||
for (const entry of entries) { | ||
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(entry.tagProvider.provider.provide)}, scope)`); | ||
} | ||
return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`; | ||
} else { | ||
let of = `${ofName}.${options.name}`; | ||
if (token === undefined) { | ||
if (argPosition >= 0) { | ||
const argsCheck: string[] = []; | ||
for (let i = 0; i < argPosition; i++) argsCheck.push('✓'); | ||
argsCheck.push('?'); | ||
of = `${ofName}(${argsCheck.join(', ')})`; | ||
} | ||
public get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R { | ||
const result = this.retriever(this, token); | ||
if (result === undefined) throw new TokenNotFoundError(`Could not resolve injector token ${tokenLabel(token)}`); | ||
return result; | ||
} | ||
} | ||
throw new DependenciesUnmetError( | ||
`Undefined dependency '${options.name}: undefined' of ${of}. Dependency '${options.name}' has no type. Imported reflect-metadata correctly? ` + | ||
`Use '@inject(PROVIDER) ${options.name}: T' if T is an interface. For circular references use @inject(() => T) ${options.name}: T.` | ||
); | ||
} | ||
const tokenVar = compiler.reserveVariable('token', token); | ||
export class ContextRegistry { | ||
public contexts: Context[] = []; | ||
let foundPreparedProvider: PreparedProvider | undefined = undefined; | ||
for (const module of resolveDependenciesFrom) { | ||
foundPreparedProvider = module.getPreparedProvider(token); | ||
if (foundPreparedProvider) { | ||
if (foundPreparedProvider) { | ||
//check if the found provider was actually exported to this current module. | ||
//if not it means that provider is encapsulated living only in its module and can not be accessed from other modules. | ||
const moduleHasAccessToThisProvider = foundPreparedProvider.modules.some(m => m === module); | ||
if (!moduleHasAccessToThisProvider) { | ||
foundPreparedProvider = undefined; | ||
} | ||
} | ||
} | ||
} | ||
get size(): number { | ||
return this.contexts.length; | ||
} | ||
if (!foundPreparedProvider) { | ||
//try if parents have anything | ||
const foundInModule = this.module.resolveToken(token); | ||
if (foundInModule) { | ||
foundPreparedProvider = foundInModule.getPreparedProvider(token); | ||
} | ||
} | ||
get(id: number): Context { | ||
return this.contexts[id]; | ||
} | ||
if (!foundPreparedProvider && options.optional) return 'undefined'; | ||
set(id: number, value: Context) { | ||
this.contexts[id] = value; | ||
} | ||
} | ||
if (!foundPreparedProvider) { | ||
throw new DependenciesUnmetError( | ||
`Unknown dependency '${options.name}: ${tokenLabel(token)}' of ${of}.` | ||
); | ||
} | ||
export class ScopedContextScopeCaches { | ||
protected caches: { [name: string]: ScopedContextCache } = {}; | ||
const allPossibleScopes = foundPreparedProvider.providers.map(getScope); | ||
const fromScope = getScope(fromProvider); | ||
const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1; | ||
constructor(protected size: number) { | ||
} | ||
if (!unscoped && !allPossibleScopes.includes(fromScope)) { | ||
throw new DependenciesUnmetError( | ||
`Dependency '${options.name}: ${tokenLabel(token)}' of ${of} can not be injected into ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` + | ||
`since ${tokenLabel(token)} only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.` | ||
); | ||
} | ||
getCache(scope: string): ScopedContextCache { | ||
let cache = this.caches[scope]; | ||
//when the dependency is FactoryProvider it might return undefined. | ||
//in this case, if the dependency is not optional, we throw an error. | ||
const orThrow = options.optional ? '' : `?? ${notFoundFunction}(${JSON.stringify(ofName)}, ${JSON.stringify(options.name)}, ${argPosition}, ${tokenVar})`; | ||
if (!cache) { | ||
cache = new ScopedContextCache(this.size); | ||
this.caches[scope] = cache; | ||
const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0]; | ||
if (resolveFromModule === this.module) { | ||
return `injector.resolver(${tokenVar}, scope)`; | ||
} | ||
return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope) ${orThrow}`; | ||
} | ||
return cache; | ||
} | ||
} | ||
export class ScopedContextCache { | ||
protected injectors: (Injector | undefined)[] = new Array(this.size); | ||
protected optionsFromProperty(property: PropertySchema): { token: any, name: string | number, optional: boolean } { | ||
const options = property.data['deepkit/inject'] as InjectOptions | undefined; | ||
let token: any = property.resolveClassType; | ||
constructor(protected size: number) { | ||
} | ||
if (options && options.token) { | ||
token = isFunction(options.token) ? options.token() : options.token; | ||
} else if (property.type === 'class') { | ||
token = property.getResolvedClassType(); | ||
} else if (property.type === 'literal') { | ||
token = property.literalValue; | ||
} | ||
get(contextId: number): Injector | undefined { | ||
return this.injectors[contextId]; | ||
return { token, name: property.name, optional: property.isOptional ? true : (!!options && options.optional) }; | ||
} | ||
set(contextId: number, injector: Injector) { | ||
this.injectors[contextId] = injector; | ||
} | ||
} | ||
export class Context { | ||
providers: ProviderWithScope[] = []; | ||
class BuildProviderIndex { | ||
protected offset: number = 0; | ||
constructor( | ||
public readonly module: InjectorModule, | ||
public readonly id: number, | ||
public readonly parent?: Context, | ||
) { | ||
reserve(): number { | ||
return this.offset++; | ||
} | ||
} | ||
export type ConfiguredProviderCalls = { | ||
type: 'call', methodName: string | symbol | number, args: any[], order: number | ||
} | ||
| { type: 'property', property: string | symbol | number, value: any, order: number } | ||
| { type: 'stop', order: number } | ||
; | ||
export class BuildContext { | ||
static ids: number = 0; | ||
public id: number = BuildContext.ids++; | ||
tagRegistry: TagRegistry = new TagRegistry; | ||
providerIndex: BuildProviderIndex = new BuildProviderIndex; | ||
export class ConfiguredProviderRegistry { | ||
public calls = new Map<any, ConfiguredProviderCalls[]>(); | ||
public add(token: any, ...newCalls: ConfiguredProviderCalls[]) { | ||
this.get(token).push(...newCalls); | ||
} | ||
public get(token: any): ConfiguredProviderCalls[] { | ||
let calls = this.calls.get(token); | ||
if (!calls) { | ||
calls = []; | ||
this.calls.set(token, calls); | ||
} | ||
return calls; | ||
} | ||
clone(): ConfiguredProviderRegistry { | ||
const c = new ConfiguredProviderRegistry; | ||
for (const [token, calls] of this.calls.entries()) { | ||
c.calls.set(token, calls.slice()); | ||
} | ||
return c; | ||
} | ||
/** | ||
* In the process of preparing providers, each module redirects their | ||
* global setup calls in this registry. | ||
*/ | ||
globalSetupProviderRegistry: SetupProviderRegistry = new SetupProviderRegistry; | ||
} | ||
export type ConfigureProvider<T> = { [name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name] }; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* A InjectorContext is responsible for taking a root InjectorModule and build all Injectors. | ||
* | ||
* It also can create scopes aka a sub InjectorContext with providers from a particular scope. | ||
*/ | ||
export function setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, registry: ConfiguredProviderRegistry, order: number): ConfigureProvider<T extends ClassType<infer C> ? C : T> { | ||
const proxy = new Proxy({}, { | ||
get(target, prop) { | ||
return (...args: any[]) => { | ||
registry.add(classTypeOrToken, { type: 'call', methodName: prop, args: args, order }); | ||
return proxy; | ||
}; | ||
}, | ||
set(target, prop, value) { | ||
registry.add(classTypeOrToken, { type: 'property', property: prop, value: value, order }); | ||
return true; | ||
} | ||
}); | ||
return proxy as any; | ||
} | ||
export class InjectorContext implements BasicInjector { | ||
protected injectors: (Injector | undefined)[] = new Array(this.contextManager.contexts.length); | ||
public readonly scopeCaches: ScopedContextScopeCaches; | ||
protected cache: ScopedContextCache; | ||
export class InjectorContext { | ||
constructor( | ||
public readonly contextManager: ContextRegistry = new ContextRegistry, | ||
public readonly scope: string = 'module', | ||
public readonly configuredProviderRegistry: ConfiguredProviderRegistry = new ConfiguredProviderRegistry, | ||
public readonly parent: InjectorContext | undefined = undefined, | ||
public readonly additionalInjectorParent: Injector | undefined = undefined, | ||
public readonly modules: { [name: string]: InjectorModule } = {}, | ||
scopeCaches?: ScopedContextScopeCaches, | ||
public tagRegistry: TagRegistry = new TagRegistry(), | ||
public rootModule: InjectorModule, | ||
public readonly scope?: Scope, | ||
protected buildContext: BuildContext = new BuildContext, | ||
) { | ||
this.scopeCaches = scopeCaches || new ScopedContextScopeCaches(this.contextManager.size); | ||
this.cache = this.scopeCaches.getCache(this.scope); | ||
} | ||
getModule(name: string): InjectorModule { | ||
if (!this.modules[name]) throw new Error(`No Module with name ${name} registered`); | ||
return this.modules[name]; | ||
get<T>(token: T | Token, module?: InjectorModule): ResolveToken<T> { | ||
return this.getInjector(module || this.rootModule).get(token, this.scope); | ||
} | ||
registerModule(module: InjectorModule, config?: ConfigDefinition<any>) { | ||
if (this.modules[module.getName()]) throw new Error(`Module ${module.getName()} already registered`); | ||
set<T>(token: T, value: any, module?: InjectorModule): void { | ||
return this.getInjector(module || this.rootModule).set(token, value, this.scope); | ||
} | ||
if (config) config.setModule(module); | ||
this.modules[module.getName()] = module; | ||
for (const [provider, calls] of module.getConfiguredProviderRegistry().calls) { | ||
this.configuredProviderRegistry.add(provider, ...calls); | ||
} | ||
static forProviders(providers: ProviderWithScope[]) { | ||
return new InjectorContext(new InjectorModule(providers)); | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Returns the unscoped injector. Use `.get(T, Scope)` for resolving scoped token. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, order: number = 0): ConfigureProvider<T extends ClassType<infer C> ? C : T> { | ||
return setupProvider(classTypeOrToken, this.configuredProviderRegistry, order); | ||
getInjector(module: InjectorModule): Injector { | ||
return module.getOrCreateInjector(this.buildContext); | ||
} | ||
getModuleNames(): string[] { | ||
return Object.keys(this.modules); | ||
getRootInjector(): Injector { | ||
return this.getInjector(this.rootModule); | ||
} | ||
static forProviders(providers: ProviderWithScope[]) { | ||
const registry = new ContextRegistry(); | ||
const context = new Context(new InjectorModule('', {}), 0); | ||
registry.set(0, context); | ||
context.providers.push(...providers); | ||
return new InjectorContext(registry); | ||
public createChildScope(scope: string): InjectorContext { | ||
return new InjectorContext(this.rootModule, { name: scope, instances: {} }, this.buildContext); | ||
} | ||
public getInjectorForModule(module: InjectorModule): Injector { | ||
return this.getInjector(module.contextId); | ||
} | ||
public getInjector(contextId: number): Injector { | ||
let injector = this.injectors[contextId]; | ||
if (injector) return injector; | ||
const parents: Injector[] = []; | ||
parents.push(this.parent ? this.parent.getInjector(contextId) : new Injector()); | ||
if (this.additionalInjectorParent) parents.push(this.additionalInjectorParent.fork(undefined, this)); | ||
const context = this.contextManager.get(contextId); | ||
if (context.parent) parents.push(this.getInjector(context.parent.id)); | ||
injector = this.cache.get(contextId); | ||
if (injector) { | ||
//we have one from cache. Clear it, and return | ||
injector = injector.fork(parents, this); | ||
return this.injectors[contextId] = injector; | ||
} | ||
const providers = getProviders(context.providers, this.scope); | ||
injector = new Injector(providers, parents, this, this.configuredProviderRegistry, this.tagRegistry); | ||
this.injectors[contextId] = injector; | ||
this.cache.set(contextId, injector); | ||
return injector; | ||
} | ||
public get<T, R = T extends ClassType<infer R> ? R : T>(token: T, frontInjector?: Injector): R { | ||
const injector = this.getInjector(0); | ||
return injector.get(token, frontInjector); | ||
} | ||
public createChildScope(scope: string, additionalInjectorParent?: Injector): InjectorContext { | ||
return new InjectorContext(this.contextManager, scope, this.configuredProviderRegistry, this, additionalInjectorParent, this.modules, this.scopeCaches, this.tagRegistry); | ||
} | ||
} |
@@ -1,19 +0,219 @@ | ||
import { ClassType } from '@deepkit/core'; | ||
import { ConfiguredProviderRegistry, ConfigureProvider, setupProvider } from './injector'; | ||
import { ConfigDefinition } from './config'; | ||
import { NormalizedProvider, ProviderWithScope, TagProvider, Token } from './provider'; | ||
import { arrayRemoveItem, ClassType, getClassName, isClass, isPrototypeOfBase } from '@deepkit/core'; | ||
import { InjectorToken } from './decorator'; | ||
import { BuildContext, Injector, SetupProviderRegistry } from './injector'; | ||
export class InjectorModule<N extends string = string, C extends { [name: string]: any } = any> { | ||
public contextId: number = 0; | ||
export type ConfigureProvider<T> = { [name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name] }; | ||
protected setupProviderRegistry = new ConfiguredProviderRegistry; | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
*/ | ||
export function setupProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, registry: SetupProviderRegistry, order: number): ConfigureProvider<T extends ClassType<infer C> ? C : T> { | ||
const proxy = new Proxy({}, { | ||
get(target, prop) { | ||
return (...args: any[]) => { | ||
registry.add(classTypeOrToken, { type: 'call', methodName: prop, args: args, order }); | ||
return proxy; | ||
}; | ||
}, | ||
set(target, prop, value) { | ||
registry.add(classTypeOrToken, { type: 'property', property: prop, value: value, order }); | ||
return true; | ||
} | ||
}); | ||
return proxy as any; | ||
} | ||
let moduleIds: number = 0; | ||
export interface PreparedProvider { | ||
/** | ||
* The modules from which dependencies can be resolved. The first item is always the module from which this provider was declared. | ||
* | ||
* This is per default the module in which the provider was declared, | ||
* but if the provider was moved (by exporting), then if | ||
* a) the parent had this provider already, then this array has additionally the one from which the provider was exported. | ||
* b) the parent had no provider of that token, then this array is just the module from which the provider was exported. | ||
* | ||
* This is important otherwise exported provider won't have access in their dependencies to their original (encapsulated) injector. | ||
*/ | ||
modules: InjectorModule[]; | ||
/** | ||
* A token can have multiple providers, for each scope its own entry. | ||
* Each scoped provider can only exist once. | ||
*/ | ||
providers: NormalizedProvider[]; | ||
/** | ||
* When this provider was exported to another module and thus is actually instantiated in another module, then this is set. | ||
* This is necessary to tell the module who declared this provider to not instantiate it, but redirects resolve requests | ||
* to `resolveFrom` instead. | ||
*/ | ||
resolveFrom?: InjectorModule; | ||
} | ||
function registerPreparedProvider(map: Map<any, PreparedProvider>, modules: InjectorModule[], providers: NormalizedProvider[]) { | ||
const token = providers[0].provide; | ||
const preparedProvider = map.get(token); | ||
if (preparedProvider) { | ||
for (const provider of providers) { | ||
const scope = getScope(provider); | ||
//check if given provider has a unknown scope, if so set it. | ||
//if the scope is known, overwrite it (we want always the last known provider to be valid) | ||
const knownProvider = preparedProvider.providers.findIndex(v => getScope(v) === scope); | ||
if (knownProvider === -1) { | ||
//scope not known, add it | ||
preparedProvider.providers.push(provider); | ||
} else { | ||
//scope already known, replace it | ||
preparedProvider.providers.splice(knownProvider, 1, provider); | ||
} | ||
} | ||
} else { | ||
//just add it | ||
map.set(token, { modules, providers: providers.slice(0) }); | ||
} | ||
} | ||
export function findModuleForConfig(config: ConfigDefinition<any>, modules: InjectorModule[]): InjectorModule { | ||
for (const m of modules) { | ||
if (m.configDefinition === config) return m; | ||
} | ||
throw new Error(`No module found for configuration ${config.schema.toString()}. Did you attach it to a module?`); | ||
} | ||
export type ExportType = ClassType | InjectorToken<any> | string | InjectorModule; | ||
export function isProvided(providers: ProviderWithScope[], token: any): boolean { | ||
return providers.find(v => !(v instanceof TagProvider) ? token === (isClass(v) ? v : v.provide) : false) !== undefined; | ||
} | ||
export function getScope(provider: ProviderWithScope): string { | ||
return (isClass(provider) ? '' : provider instanceof TagProvider ? provider.provider.scope : provider.scope) || ''; | ||
} | ||
export class InjectorModule<C extends { [name: string]: any } = any, IMPORT = InjectorModule<any, any>> { | ||
public id: number = moduleIds++; | ||
/** | ||
* Whether this module is for the root module. All its providers are automatically exported and moved to the root level. | ||
*/ | ||
public root: boolean = false; | ||
/** | ||
* The built injector. This is set once a Injector for this module has been created. | ||
*/ | ||
injector?: Injector; | ||
public setupProviderRegistry: SetupProviderRegistry = new SetupProviderRegistry; | ||
public globalSetupProviderRegistry: SetupProviderRegistry = new SetupProviderRegistry; | ||
imports: InjectorModule[] = []; | ||
/** | ||
* The first stage of building the injector is to resolve all providers and exports. | ||
* Then the actual injector functions can be built. | ||
*/ | ||
protected processed: boolean = false; | ||
protected exportsDisabled: boolean = false; | ||
public configDefinition?: ConfigDefinition<any>; | ||
constructor( | ||
public name: N, | ||
public config: C, | ||
public providers: ProviderWithScope[] = [], | ||
public parent?: InjectorModule, | ||
public config: C = {} as C, | ||
public exports: ExportType[] = [] | ||
) { | ||
if (this.parent) this.parent.registerAsChildren(this); | ||
} | ||
getName(): N { | ||
return this.name; | ||
registerAsChildren(child: InjectorModule): void { | ||
if (this.imports.includes(child)) return; | ||
this.imports.push(child); | ||
} | ||
/** | ||
* When the module exports providers the importer don't want to have then `disableExports` disable all exports. | ||
*/ | ||
disableExports(): this { | ||
this.exportsDisabled = true; | ||
return this; | ||
} | ||
/** | ||
* Makes all the providers, controllers, etc available at the root module, basically exporting everything. | ||
*/ | ||
forRoot(): this { | ||
this.root = true; | ||
return this; | ||
} | ||
/** | ||
* Reverts the root default setting to false. | ||
*/ | ||
notForRoot(): this { | ||
this.root = false; | ||
return this; | ||
} | ||
unregisterAsChildren(child: InjectorModule): void { | ||
if (!this.imports.includes(child)) return; | ||
child.parent = undefined; | ||
arrayRemoveItem(this.imports, child); | ||
} | ||
getChildren(): InjectorModule[] { | ||
return this.imports; | ||
} | ||
setConfigDefinition(config: ConfigDefinition<any>): this { | ||
this.configDefinition = config; | ||
return this; | ||
} | ||
setParent(parent: InjectorModule): this { | ||
if (this.parent === parent) return this; | ||
this.assertInjectorNotBuilt(); | ||
if (this.parent) this.parent.unregisterAsChildren(this); | ||
this.parent = parent; | ||
this.parent.registerAsChildren(this); | ||
return this; | ||
} | ||
getParent(): InjectorModule | undefined { | ||
return this.parent; | ||
} | ||
protected assertInjectorNotBuilt(): void { | ||
if (!this.injector) return; | ||
throw new Error(`Injector already built for ${getClassName(this)}. Can not modify its provider or tree structure.`); | ||
} | ||
addExport(...controller: ClassType[]): this { | ||
this.assertInjectorNotBuilt(); | ||
this.exports.push(...controller); | ||
return this; | ||
} | ||
isProvided(classType: ClassType): boolean { | ||
return isProvided(this.getProviders(), classType); | ||
} | ||
addProvider(...provider: ProviderWithScope[]): this { | ||
this.assertInjectorNotBuilt(); | ||
this.providers.push(...provider); | ||
return this; | ||
} | ||
getProviders(): ProviderWithScope[] { | ||
return this.providers; | ||
} | ||
getConfig(): C { | ||
@@ -23,17 +223,238 @@ return this.config; | ||
setConfig(config: C) { | ||
configure(config: Partial<C>): this { | ||
Object.assign(this.config, config); | ||
return this; | ||
} | ||
getConfiguredProviderRegistry(): ConfiguredProviderRegistry { | ||
return this.setupProviderRegistry; | ||
getImports(): InjectorModule[] { | ||
return this.imports; | ||
} | ||
getImportedModulesByClass<T extends InjectorModule>(classType: ClassType<T>): T[] { | ||
return this.getImports().filter(v => v instanceof classType) as T[]; | ||
} | ||
getImportedModuleByClass<T extends InjectorModule>(classType: ClassType<T>): T { | ||
const v = this.getImports().find(v => v instanceof classType); | ||
if (!v) { | ||
throw new Error(`No module ${getClassName(classType)} in ${getClassName(this)}#${this.id} imported.`); | ||
} | ||
return v as T; | ||
} | ||
getImportedModule<T extends InjectorModule>(module: T): T { | ||
const v = this.getImports().find(v => v.id === module.id); | ||
if (!v) { | ||
throw new Error(`No module ${getClassName(module)}#${module.id} in ${getClassName(this)}#${this.id} imported.`); | ||
} | ||
return v as T; | ||
} | ||
getExports() { | ||
return this.exports; | ||
} | ||
hasImport<T extends InjectorModule>(moduleClass: ClassType<T>): boolean { | ||
for (const importModule of this.getImports()) { | ||
if (importModule instanceof moduleClass) return true; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Returns a configuration object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider has been created by the dependency injection container. | ||
* Modifies this module and adds a new import, returning the same module. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: T, order: number = 0): ConfigureProvider<T extends ClassType<infer C> ? C : T> { | ||
addImport(...modules: InjectorModule<any>[]): this { | ||
this.assertInjectorNotBuilt(); | ||
for (const module of modules) { | ||
module.setParent(this); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Allows to register additional setup calls for a provider in this module. | ||
* The injector token needs to be available in the local module providers. | ||
* Use setupGlobalProvider to register globally setup calls (not limited to this module only). | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, order: number = 0): ConfigureProvider<T extends ClassType<infer C> ? C : T> { | ||
return setupProvider(classTypeOrToken, this.setupProviderRegistry, order); | ||
} | ||
/** | ||
* Allows to register additional setup calls for a provider in the whole module tree. | ||
* The injector token needs to be available in the local module providers. | ||
* | ||
* Returns a object that reflects the API of the given ClassType or token. Each call | ||
* is scheduled and executed once the provider is created by the dependency injection container. | ||
*/ | ||
setupGlobalProvider<T extends ClassType<T> | any>(classTypeOrToken: Token<T>, order: number = 0): ConfigureProvider<T extends ClassType<infer C> ? C : T> { | ||
return setupProvider(classTypeOrToken, this.globalSetupProviderRegistry, order); | ||
} | ||
getOrCreateInjector(buildContext: BuildContext): Injector { | ||
if (this.injector) return this.injector; | ||
//notify everyone we know to prepare providers | ||
if (this.parent) this.parent.getPreparedProviders(buildContext); | ||
this.getPreparedProviders(buildContext); | ||
//handle exports, from bottom to up | ||
if (this.parent) this.parent.handleExports(buildContext); | ||
this.handleExports(buildContext); | ||
//build the injector context | ||
if (this.parent) this.parent.getOrCreateInjector(buildContext); | ||
this.injector = new Injector(this, buildContext); | ||
for (const child of this.imports) child.getOrCreateInjector(buildContext); | ||
return this.injector; | ||
} | ||
protected preparedProviders?: Map<any, PreparedProvider>; | ||
getPreparedProvider(token: any): PreparedProvider | undefined { | ||
if (!this.preparedProviders) return; | ||
return this.preparedProviders.get(token); | ||
} | ||
resolveToken(token: any): InjectorModule | undefined { | ||
if (!this.preparedProviders) return; | ||
if (this.preparedProviders.has(token)) return this; | ||
if (this.parent) return this.parent.resolveToken(token); | ||
return; | ||
} | ||
/** | ||
* Prepared the module for a injector tree build. | ||
* | ||
* - Index providers by token so that last known provider is picked (so they can be overwritten). | ||
* - Register TagProvider in TagRegistry | ||
* - Put TagProvider in providers if not already made. | ||
* - Put exports to parent's module with the reference to this, so the dependencies are fetched from the correct module. | ||
*/ | ||
getPreparedProviders(buildContext: BuildContext): Map<any, PreparedProvider> { | ||
if (this.preparedProviders) return this.preparedProviders; | ||
for (const m of this.imports) { | ||
m.getPreparedProviders(buildContext); | ||
} | ||
this.preparedProviders = new Map<any, PreparedProvider>(); | ||
this.globalSetupProviderRegistry.mergeInto(buildContext.globalSetupProviderRegistry); | ||
//make sure that providers that declare the same provider token will be filtered out so that the last will be used. | ||
for (const provider of this.providers) { | ||
if (provider instanceof TagProvider) { | ||
buildContext.tagRegistry.register(provider, this); | ||
if (!this.preparedProviders.has(provider.provider.provide)) { | ||
//we dont want to overwrite that provider with a tag | ||
registerPreparedProvider(this.preparedProviders, [this], [provider.provider]); | ||
} | ||
} else if (isClass(provider)) { | ||
registerPreparedProvider(this.preparedProviders, [this], [{ provide: provider }]); | ||
} else { | ||
registerPreparedProvider(this.preparedProviders, [this], [provider]); | ||
} | ||
} | ||
return this.preparedProviders; | ||
} | ||
protected exported: boolean = false; | ||
protected handleExports(buildContext: BuildContext) { | ||
if (this.exported) return; | ||
this.exported = true; | ||
//the import order is important. the last entry is the most important and should be able to overwrite | ||
//previous modules. In order to make that work, we call handleExports in reversed order. | ||
//this lets providers from the last import register their provider first, and make them available first | ||
//in the injector (which equals to be resolved first). | ||
for (let i = this.imports.length - 1; i >= 0; i--) { | ||
this.imports[i].setParent(this); | ||
this.imports[i].handleExports(buildContext); | ||
} | ||
// for (const m of this.imports) { | ||
// m.setParent(this); | ||
// m.handleExports(buildContext); | ||
// } | ||
if (!this.preparedProviders) return; | ||
if (!this.parent) return; | ||
if (this.exportsDisabled) return; | ||
const exportToken = (token: any, to: InjectorModule) => { | ||
if (!this.preparedProviders) return; | ||
const preparedProvider = this.preparedProviders.get(token); | ||
//if it was not in provider, we continue | ||
if (!preparedProvider) return; | ||
//mark this provider as redirect to `exportTo` | ||
preparedProvider.resolveFrom = to; | ||
const parentProviders = to.getPreparedProviders(buildContext); | ||
const parentProvider = parentProviders.get(token); | ||
//if the parent has this token already defined, we just switch its module to ours, | ||
//so its able to inject our encapsulated services. | ||
if (parentProvider) { | ||
//we add our module as additional source for potential dependencies | ||
parentProvider.modules.push(this); | ||
} else { | ||
parentProviders.set(token, { modules: [this], providers: preparedProvider.providers.slice() }); | ||
} | ||
}; | ||
if (this.root) { | ||
const root = this.findRoot(); | ||
if (root !== this) { | ||
for (const token of this.preparedProviders.keys()) { | ||
exportToken(token, root); | ||
} | ||
} | ||
} else { | ||
for (const entry of this.exports) { | ||
if ((isClass(entry) && isPrototypeOfBase(entry, InjectorModule)) || entry instanceof InjectorModule) { | ||
const moduleInstance = isClass(entry) ? this.imports.find(v => v instanceof entry) : entry; | ||
if (!moduleInstance) { | ||
throw new Error(`Unknown module ${getClassName(entry)} exported from ${getClassName(this)}. The module was never imported.`); | ||
} | ||
//export everything to the parent that we received from that `entry` module | ||
for (const [token, preparedProvider] of this.preparedProviders.entries()) { | ||
if (preparedProvider.modules.includes(moduleInstance)) { | ||
//this provider was received from `entry` | ||
//mark this provider as redirect to `exportTo` | ||
preparedProvider.resolveFrom = this.parent; | ||
const parentProviders = this.parent.getPreparedProviders(buildContext); | ||
const parentProvider = parentProviders.get(token); | ||
//if the parent has this token already defined, we just switch its module to ours, | ||
//so its able to inject our encapsulated services. | ||
if (parentProvider) { | ||
//we add our module as additional source for potential dependencies | ||
parentProvider.modules.push(this); | ||
} else { | ||
parentProviders.set(token, { modules: [this, ...preparedProvider.modules], providers: preparedProvider.providers.slice() }); | ||
} | ||
} | ||
} | ||
} else { | ||
//export single token | ||
exportToken(entry, this.parent); | ||
} | ||
} | ||
} | ||
} | ||
findRoot(): InjectorModule { | ||
if (this.parent) return this.parent.findRoot(); | ||
return this; | ||
} | ||
} |
@@ -10,3 +10,5 @@ /* | ||
*/ | ||
import { ClassType, isClass } from '@deepkit/core'; | ||
import { AbstractClassType, ClassType, isClass } from '@deepkit/core'; | ||
import { InjectorToken } from './decorator'; | ||
import { InjectorModule } from './module'; | ||
@@ -21,7 +23,9 @@ export interface ProviderBase { | ||
export type Token<T = any> = symbol | string | InjectorToken<T> | AbstractClassType<T>; | ||
export interface ValueProvider<T> extends ProviderBase { | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
@@ -36,5 +40,5 @@ /** | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: ClassType<T>; | ||
provide: Token<T>; | ||
@@ -49,5 +53,5 @@ /** | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
@@ -62,5 +66,5 @@ /** | ||
/** | ||
* An injection token. (Typically an instance of `ClassType`, but can be `any`). | ||
* An injection token. Typically a class. | ||
*/ | ||
provide: symbol | string | ClassType<T>; | ||
provide: Token<T>; | ||
@@ -84,11 +88,20 @@ /** | ||
interface TagRegistryEntry<T> { | ||
tagProvider: TagProvider<T>; | ||
module: InjectorModule; | ||
} | ||
export class TagRegistry { | ||
constructor( | ||
public tags: TagProvider<any>[] = [] | ||
public tags: TagRegistryEntry<any>[] = [] | ||
) { | ||
} | ||
resolve<T extends ClassType<Tag<any>>>(tag: T): TagProvider<InstanceType<T>>[] { | ||
return this.tags.filter(v => v.tag instanceof tag); | ||
register(tagProvider: TagProvider<any>, module: InjectorModule) { | ||
return this.tags.push({tagProvider, module}); | ||
} | ||
resolve<T extends ClassType<Tag<any>>>(tag: T): TagRegistryEntry<InstanceType<T>>[] { | ||
return this.tags.filter(v => v.tagProvider.tag instanceof tag); | ||
} | ||
} | ||
@@ -164,2 +177,9 @@ | ||
export function isTransient(provider: ProviderWithScope): boolean { | ||
if (isClass(provider)) return false; | ||
if (provider instanceof TagProvider) return false; | ||
return provider.transient === true; | ||
} | ||
export function getProviders( | ||
@@ -166,0 +186,0 @@ providers: ProviderWithScope[], |
@@ -0,5 +1,7 @@ | ||
import 'reflect-metadata'; | ||
import { expect, test } from '@jest/globals'; | ||
import { getClassSchema, t } from '@deepkit/type'; | ||
import 'reflect-metadata'; | ||
import { CircularDependencyError, createConfig, inject, injectable, InjectOptions, Injector, InjectorContext } from '../src/injector'; | ||
import { CircularDependencyError, Injector } from '../src/injector'; | ||
import { inject, injectable, InjectOptions, InjectorToken } from '../src/decorator'; | ||
import { createConfig } from '../src/config'; | ||
import { InjectorModule } from '../src/module'; | ||
@@ -13,3 +15,3 @@ | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -21,3 +23,3 @@ constructor(private connection: Connection) { | ||
const injector = new Injector([MyServer, Connection]); | ||
const injector = Injector.from([MyServer, Connection]); | ||
expect(injector.get(Connection)).toBeInstanceOf(Connection); | ||
@@ -34,3 +36,3 @@ expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -42,4 +44,3 @@ constructor(private connection: Connection, private missing: Missing) { | ||
const injector = new Injector([MyServer, Connection]); | ||
expect(() => injector.get(MyServer)).toThrow(`Unknown constructor argument 'missing: Missing' of MyServer(✓, ?). Make sure 'Missing' is provided.`); | ||
expect(() => Injector.from([MyServer, Connection])).toThrow(`Unknown dependency 'missing: Missing' of MyServer.missing`); | ||
}); | ||
@@ -51,3 +52,3 @@ | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -59,7 +60,7 @@ constructor(private connection: Connection, private missing: any) { | ||
expect(() => new Injector([MyServer, Connection])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(✓, ?).`); | ||
expect(() => Injector.from([MyServer, Connection])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(✓, ?).`); | ||
}); | ||
test('wrong dep 2', () => { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -70,7 +71,7 @@ constructor(private missing: any) { | ||
expect(() => new Injector([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(?).`); | ||
expect(() => Injector.from([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer(?).`); | ||
}); | ||
test('wrong dep 3', () => { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -80,7 +81,7 @@ @inject() private missing: any; | ||
expect(() => new Injector([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer.missing.`); | ||
expect(() => Injector.from([MyServer])).toThrow(`Undefined dependency 'missing: undefined' of MyServer.missing.`); | ||
}); | ||
test('injector key', () => { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -92,3 +93,3 @@ constructor(@inject('foo') private foo: string) { | ||
const injector = new Injector([MyServer, { provide: 'foo', useValue: 'bar' }]); | ||
const injector = Injector.from([MyServer, { provide: 'foo', useValue: 'bar' }]); | ||
expect(injector.get('foo')).toBe('bar'); | ||
@@ -103,3 +104,3 @@ expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -111,3 +112,3 @@ constructor(public connection: Connection) { | ||
const injector = new Injector([MyServer, { provide: Connection, transient: true }]); | ||
const injector = Injector.from([MyServer, { provide: Connection, transient: true }]); | ||
const c1 = injector.get(Connection); | ||
@@ -137,3 +138,3 @@ const c2 = injector.get(Connection); | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -147,3 +148,3 @@ @inject() | ||
const injector = new Injector([MyServer, Connection, { provide: 'name', useValue: 'peter' }]); | ||
const injector = Injector.from([MyServer, Connection, { provide: 'name', useValue: 'peter' }]); | ||
const s = injector.get(MyServer); | ||
@@ -161,3 +162,3 @@ expect(s.connection).toBeInstanceOf(Connection); | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -170,3 +171,3 @@ constructor(@inject(Connection2) private connection: Connection) { | ||
{ | ||
const injector = new Injector([MyServer, Connection, Connection2]); | ||
const injector = Injector.from([MyServer, Connection, Connection2]); | ||
expect(injector.get(Connection)).toBeInstanceOf(Connection); | ||
@@ -181,3 +182,3 @@ expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -190,8 +191,23 @@ constructor(private connection?: Connection) { | ||
{ | ||
const injector = new Injector([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow('Could not resolve injector token Connection'); | ||
expect(() => injector.get(MyServer)).toThrow(`Unknown constructor argument 'connection: Connection' of MyServer(?).`); | ||
expect(() => Injector.from([MyServer])).toThrow(`Unknown dependency 'connection: Connection' of MyServer.connection`); | ||
} | ||
}); | ||
test('injector optional unmet dependency', () => { | ||
class Connection { | ||
} | ||
@injectable | ||
class MyServer { | ||
constructor(@t.optional private connection?: Connection) { | ||
expect(connection).toBeUndefined(); | ||
} | ||
} | ||
{ | ||
const injector = Injector.from([MyServer]); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
test('injector optional dependency', () => { | ||
@@ -201,3 +217,3 @@ class Connection { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -210,4 +226,4 @@ constructor(@inject().optional private connection?: Connection) { | ||
{ | ||
const injector = new Injector([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow('Could not resolve injector token Connection'); | ||
const injector = Injector.from([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow(`Token 'Connection' in InjectorModule not found`); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
@@ -217,2 +233,69 @@ } | ||
test('injector via t.optional', () => { | ||
class Connection { | ||
} | ||
@injectable | ||
class MyServer { | ||
constructor(@t.optional private connection?: Connection) { | ||
expect(connection).toBeUndefined(); | ||
} | ||
} | ||
{ | ||
const injector = Injector.from([MyServer]); | ||
expect(() => injector.get(Connection)).toThrow(`Token 'Connection' in InjectorModule not found`); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
} | ||
}); | ||
test('injector via t.type', () => { | ||
interface ConnectionInterface {} | ||
class Connection { | ||
} | ||
@injectable | ||
class MyServer { | ||
constructor(@t.type(Connection) public connection: ConnectionInterface) { | ||
} | ||
} | ||
{ | ||
const injector = Injector.from([MyServer, Connection]); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
expect(injector.get(MyServer).connection).toBeInstanceOf(Connection); | ||
} | ||
}); | ||
test('injector via t.type string', () => { | ||
interface ConnectionInterface {} | ||
class Connection { | ||
} | ||
@injectable | ||
class MyServer { | ||
constructor(@t.type('connection') public connection: ConnectionInterface) { | ||
} | ||
} | ||
{ | ||
const injector = Injector.from([MyServer, {provide: 'connection', useClass: Connection}]); | ||
expect(injector.get(MyServer)).toBeInstanceOf(MyServer); | ||
expect(injector.get(MyServer).connection).toBeInstanceOf(Connection); | ||
} | ||
}); | ||
test('injector token', () => { | ||
interface ConnectionInterface { | ||
doIt(): string; | ||
} | ||
const Connection = new InjectorToken<ConnectionInterface>('connection'); | ||
{ | ||
const injector = Injector.from([{provide: Connection, useValue: {doIt() {return 'hi'}}}]); | ||
expect(injector.get(Connection).doIt()).toBe('hi'); | ||
} | ||
}); | ||
test('injector overwrite provider', () => { | ||
@@ -225,3 +308,3 @@ class Connection { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -234,3 +317,3 @@ constructor(private connection: Connection) { | ||
{ | ||
const injector = new Injector([MyServer, { | ||
const injector = Injector.from([MyServer, { | ||
provide: Connection, useClass: Connection2 | ||
@@ -244,3 +327,3 @@ }]); | ||
test('injector direct circular dependency', () => { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -252,3 +335,3 @@ constructor(private myServer: MyServer) { | ||
{ | ||
const injector = new Injector([MyServer]); | ||
const injector = Injector.from([MyServer]); | ||
expect(() => injector.get(MyServer)).toThrow(CircularDependencyError as any); | ||
@@ -260,3 +343,3 @@ } | ||
test('injector circular dependency', () => { | ||
@injectable() | ||
@injectable | ||
class Connection { | ||
@@ -269,3 +352,3 @@ constructor(@inject(() => MyServer) myServer: any) { | ||
@injectable() | ||
@injectable | ||
class MyServer { | ||
@@ -279,3 +362,3 @@ constructor(connection: Connection) { | ||
{ | ||
const injector = new Injector([MyServer, Connection]); | ||
const injector = Injector.from([MyServer, Connection]); | ||
expect(() => injector.get(MyServer)).toThrow(CircularDependencyError as any); | ||
@@ -292,3 +375,3 @@ expect(() => injector.get(MyServer)).toThrow('Circular dependency found MyServer -> Connection -> MyServer'); | ||
{ | ||
const injector = new Injector([{ provide: Service, useFactory: () => new Service() }]); | ||
const injector = Injector.from([{ provide: Service, useFactory: () => new Service() }]); | ||
@@ -304,45 +387,11 @@ const s1 = injector.get(Service); | ||
test('injector stack parent', () => { | ||
const i1 = new Injector([ | ||
{ provide: 'level', deps: ['deep1'], useFactory: (d: any) => d }, | ||
{ provide: 'level2', deps: ['deep2'], useFactory: (d: any) => d }, | ||
]); | ||
const i2 = new Injector([{ provide: 'deep1', useValue: 2 }], [i1]); | ||
const i3 = new Injector([{ provide: 'deep2', useValue: 3 }], [i2]); | ||
expect(i2.get('level')).toBe(2); | ||
expect(i3.get('level')).toBe(2); | ||
expect(() => i2.get('level2')).toThrow(`Unknown factory dependency argument 'deep2' of useFactory`); | ||
expect(i3.get('level2')).toBe(3); | ||
}); | ||
test('injector stack parent fork', () => { | ||
const i1 = new Injector([ | ||
{ provide: 'level', deps: ['deep1'], useFactory: (d: any) => d }, | ||
{ provide: 'level2', deps: ['deep2'], useFactory: (d: any) => d }, | ||
]); | ||
const i2 = new Injector([{ provide: 'deep1', useValue: 2 }], [i1]).fork(); | ||
const i3 = new Injector([{ provide: 'deep2', useValue: 3 }], [i2]).fork(); | ||
expect(i2.get('level')).toBe(2); | ||
expect(i3.get('level')).toBe(2); | ||
expect(() => i2.get('level2')).toThrow(`Unknown factory dependency argument 'deep2' of useFactory`); | ||
expect(i3.get('level2')).toBe(3); | ||
}); | ||
test('injector config', () => { | ||
const FullConfig = createConfig({ | ||
const moduleConfig = createConfig({ | ||
debug: t.boolean.default(false) | ||
}); | ||
class ServiceConfig extends FullConfig.slice(['debug']) { | ||
class ServiceConfig extends moduleConfig.slice('debug') { | ||
} | ||
@injectable() | ||
@injectable | ||
class MyService { | ||
@@ -353,18 +402,18 @@ constructor(public config: ServiceConfig) { | ||
@injectable() | ||
@injectable | ||
class MyService2 { | ||
constructor(@inject(FullConfig) public config: typeof FullConfig.type) { | ||
constructor(@inject(moduleConfig) public config: typeof moduleConfig.type) { | ||
} | ||
} | ||
@injectable() | ||
@injectable | ||
class MyService3 { | ||
constructor(@inject(FullConfig.all()) public config: typeof FullConfig.type) { | ||
constructor(@inject(moduleConfig.all()) public config: typeof moduleConfig.type) { | ||
} | ||
} | ||
class Slice extends FullConfig.slice(['debug']) { | ||
class Slice extends moduleConfig.slice('debug') { | ||
} | ||
@injectable() | ||
@injectable | ||
class MyService4 { | ||
@@ -376,3 +425,4 @@ constructor(public config: Slice) { | ||
{ | ||
const i1 = new Injector([MyService, MyService2, MyService3, MyService4], []); | ||
const i1 = Injector.fromModule(new InjectorModule([MyService, MyService2, MyService3, MyService4]).setConfigDefinition(moduleConfig)); | ||
i1.module.configure({debug: false}); | ||
expect(i1.get(MyService).config.debug).toBe(false); | ||
@@ -385,6 +435,4 @@ expect(i1.get(MyService2).config.debug).toBe(false); | ||
{ | ||
const myModule = new InjectorModule('asd', { debug: true }); | ||
const injectorContext = new InjectorContext(); | ||
injectorContext.registerModule(myModule, FullConfig); | ||
const i1 = new Injector([MyService, MyService2, MyService3, MyService4], [], injectorContext); | ||
const i1 = Injector.fromModule(new InjectorModule([MyService, MyService2, MyService3, MyService4]).setConfigDefinition(moduleConfig)); | ||
i1.module.configure({debug: true}); | ||
expect(i1.get(MyService).config.debug).toBe(true); | ||
@@ -407,4 +455,3 @@ expect(i1.get(MyService2).config.debug).toBe(true); | ||
{ | ||
const injectorContext = new InjectorContext(); | ||
const i1 = new Injector([MyService], [], injectorContext); | ||
const i1 = Injector.from([MyService]); | ||
expect(i1.get(MyService).transporter).toEqual([]); | ||
@@ -414,7 +461,7 @@ } | ||
{ | ||
const injectorContext = new InjectorContext(); | ||
injectorContext.setupProvider(MyService).addTransporter('a'); | ||
injectorContext.setupProvider(MyService).addTransporter('b'); | ||
expect(injectorContext.configuredProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = new Injector([MyService], [], injectorContext); | ||
const module = new InjectorModule([MyService]); | ||
module.setupProvider(MyService).addTransporter('a'); | ||
module.setupProvider(MyService).addTransporter('b'); | ||
expect(module.setupProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = Injector.fromModule(module); | ||
expect(i1.get(MyService).transporter).toEqual(['a', 'b']); | ||
@@ -424,7 +471,7 @@ } | ||
{ | ||
const injectorContext = new InjectorContext(); | ||
injectorContext.setupProvider(MyService).transporter = ['a']; | ||
injectorContext.setupProvider(MyService).transporter = ['a', 'b', 'c']; | ||
expect(injectorContext.configuredProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = new Injector([MyService], [], injectorContext); | ||
const module = new InjectorModule([MyService]); | ||
module.setupProvider(MyService).transporter = ['a']; | ||
module.setupProvider(MyService).transporter = ['a', 'b', 'c']; | ||
expect(module.setupProviderRegistry.get(MyService).length).toBe(2); | ||
const i1 = Injector.fromModule(module); | ||
expect(i1.get(MyService).transporter).toEqual(['a', 'b', 'c']); | ||
@@ -434,17 +481,2 @@ } | ||
test('injector fork', () => { | ||
class MyService { | ||
} | ||
const i1 = new Injector([MyService]); | ||
const s1 = i1.get(MyService); | ||
expect(s1).toBeInstanceOf(MyService); | ||
const i2 = i1.fork(); | ||
const s2 = i2.get(MyService); | ||
expect(s2).toBeInstanceOf(MyService); | ||
expect(s2).not.toBe(s1); | ||
}); | ||
test('constructor one with @inject', () => { | ||
@@ -460,3 +492,3 @@ class HttpKernel { | ||
@injectable() | ||
@injectable | ||
class MyService { | ||
@@ -472,3 +504,2 @@ constructor( | ||
class SubService extends MyService { | ||
} | ||
@@ -487,2 +518,15 @@ | ||
} | ||
@injectable | ||
class Service { | ||
constructor(public stopwatch: Stopwatch, @inject(Logger) public logger: any) {} | ||
} | ||
{ | ||
const schema = getClassSchema(Service); | ||
const methods = schema.getMethodProperties('constructor'); | ||
expect(methods.length).toBe(2); | ||
expect(methods[0].name).toBe('stopwatch'); | ||
expect(methods[1].name).toBe('logger'); | ||
} | ||
}); |
@@ -11,2 +11,3 @@ { | ||
"moduleResolution": "node", | ||
"preserveSymlinks": true, | ||
"target": "es2018", | ||
@@ -32,2 +33,2 @@ "module": "CommonJS", | ||
] | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
562380
71
8298
4
1