@wessberg/di
Version:
A compile-time powered Dependency-Injection container for Typescript that holds services and can produce instances of them as required.
209 lines (206 loc) • 9.02 kB
JavaScript
// src/constant.ts
var CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER = `___CTOR_ARGS___`;
var CONSTRUCTOR_ARGUMENTS_SYMBOL = Symbol.for(CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER);
var DI_COMPILER_ERROR_HINT = `Note: You must use DI-Compiler (https://github.com/wessberg/di-compiler) for this library to work correctly. Please consult the readme for instructions on how to install and configure it for your project.`;
// src/error.ts
var InstantiationError = class _InstantiationError extends TypeError {
#name = "InstantiationError";
#originalError;
#identifier;
#parentChain;
get [Symbol.toStringTag]() {
return this.#name;
}
constructor(error, { identifier, parentChain, ...options }) {
const message = typeof error === "string" ? error : error instanceof Error ? error.message : typeof error === "object" && error != null && "message" in error && typeof error.message === "string" ? error.message : void 0;
const stack = typeof error === "string" ? void 0 : error instanceof Error ? error.stack : typeof error === "object" && error != null && "stack" in error && typeof error.stack === "string" ? error.stack : void 0;
const originalError = error instanceof Error ? error : { message, stack };
super(message, options);
this.#identifier = identifier;
this.#originalError = originalError;
this.#parentChain = parentChain != null && parentChain.length > 0 ? parentChain.map((item) => typeof item === "string" ? item : item.identifier) : [this.#identifier];
this.name = this[Symbol.toStringTag];
let currentOriginalError = this.#originalError;
while (currentOriginalError instanceof _InstantiationError) {
currentOriginalError = currentOriginalError.#originalError;
}
const lastService = this.#parentChain[this.#parentChain.length - 1];
const head = `Could not instantiate service: '${lastService}'`;
const body = currentOriginalError.message == null || currentOriginalError.message.length < 1 ? "" : `: ${currentOriginalError.message}`;
const tail = this.#parentChain.length > 1 ? ` Dependency chain: ${this.#parentChain.join(" -> ")}` : "";
this.message = `${head}${body}${tail}`;
if (currentOriginalError.stack != null) {
this.stack = currentOriginalError.stack;
}
}
};
// src/util.ts
function isClass(item) {
if (typeof item !== "function") {
return false;
}
const descriptor = Object.getOwnPropertyDescriptor(item, "prototype");
return !!descriptor && !descriptor.writable;
}
function isCustomConstructableService(item) {
return !isClass(item) && typeof item === "function";
}
// src/di-container.ts
var DIContainer = class {
get [Symbol.toStringTag]() {
return "DIContainer";
}
/**
* A map between interface names and the services that should be dependency injected
*/
#constructorArguments = /* @__PURE__ */ new Map();
/**
* A Map between identifying names for services and their IRegistrationRecords.
*/
#serviceRegistry = /* @__PURE__ */ new Map();
/**
* A map between identifying names for services and concrete instances of their implementation.
*/
#instances = /* @__PURE__ */ new Map();
registerSingleton(newExpression, options) {
if (options == null) {
throw new ReferenceError(`2 arguments required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`);
}
if (newExpression == null) {
return this.#register("SINGLETON", newExpression, options);
} else {
return this.#register("SINGLETON", newExpression, options);
}
}
registerTransient(newExpression, options) {
if (options == null) {
throw new ReferenceError(`2 arguments required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`);
}
if (newExpression == null) {
return this.#register("TRANSIENT", newExpression, options);
} else {
return this.#register("TRANSIENT", newExpression, options);
}
}
/**
* Gets an instance of the service matching the interface given as a generic type parameter.
* For example, 'container.get<IFoo>()' returns a concrete instance of the implementation associated with the
* generic interface name.
*
* You should not pass any options to the method if using the compiler. It will do that automatically.
*/
get(options) {
if (options == null) {
throw new ReferenceError(`1 argument required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`);
}
if (!this.has(options)) {
throw new InstantiationError(`The service wasn't found in the registry.`, { identifier: options.identifier });
}
return this.#constructInstance(options);
}
/**
* Returns true if a service has been registered matching the interface given as a generic type parameter.
* For example, 'container.get<IFoo>()' returns a concrete instance of the implementation associated with the
* generic interface name.
*
* You should not pass any options to the method if using the compiler. It will do that automatically.
*/
// @ts-expect-error The 'T' type parameter is required for compile-time reflection, even though it is not part of the signature.
has(options) {
if (options == null) {
throw new ReferenceError(`1 argument required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`);
}
return this.#serviceRegistry.has(options.identifier);
}
#register(kind, newExpression, options) {
const implementationArguments = "implementation" in options && options.implementation?.[CONSTRUCTOR_ARGUMENTS_SYMBOL] != null ? options.implementation[CONSTRUCTOR_ARGUMENTS_SYMBOL] : [];
this.#constructorArguments.set(options.identifier, implementationArguments);
this.#serviceRegistry.set(
options.identifier,
"implementation" in options && options.implementation != null ? { ...options, kind } : { ...options, kind, newExpression }
);
}
/**
* Returns true if an instance exists that matches the given identifier.
*/
#hasInstance(identifier) {
return this.#getInstance(identifier) != null;
}
/**
* Gets the cached instance, if any, associated with the given identifier.
*/
#getInstance(identifier) {
const instance = this.#instances.get(identifier);
return instance == null ? null : instance;
}
/**
* Gets an IRegistrationRecord associated with the given identifier.
*/
#getRegistrationRecord(identifier) {
return this.#serviceRegistry.get(identifier);
}
/**
* Caches the given instance so that it can be retrieved in the future.
*/
#setInstance(identifier, instance) {
this.#instances.set(identifier, instance);
return instance;
}
/**
* Gets a lazy reference to another service
*/
#getLazyIdentifier(lazyPointer) {
return new Proxy({}, { get: (_, key) => lazyPointer()[key] });
}
/**
* Constructs a new instance of the given identifier and returns it.
* It checks the constructor arguments and injects any services it might depend on recursively.
*/
#constructInstance({ identifier, parentChain = [] }) {
const registrationRecord = this.#getRegistrationRecord(identifier);
if (registrationRecord == null) {
return void 0;
}
if (this.#hasInstance(identifier) && registrationRecord.kind === "SINGLETON") {
return this.#getInstance(identifier);
}
let instance;
const me = {
identifier,
ref: this.#getLazyIdentifier(() => instance)
};
const implementation = "newExpression" in registrationRecord ? registrationRecord.newExpression : registrationRecord.implementation;
if (isClass(implementation)) {
const mappedArgs = this.#constructorArguments.get(identifier);
if (mappedArgs == null) {
throw new InstantiationError(`Could not find constructor arguments. Have you registered it as a service?`, { identifier, parentChain });
}
const instanceArgs = mappedArgs.map((dep) => {
if (dep === void 0) return void 0;
const matchedParent = parentChain.find((parent) => parent.identifier === dep);
if (matchedParent != null) return matchedParent.ref;
const nextParentChain = [...parentChain, me];
const constructedInstance = this.#constructInstance({
identifier: dep,
parentChain: nextParentChain
});
if (constructedInstance == null) {
throw new InstantiationError(`Dependency '${dep}' was not found in the service registry.`, { identifier: me.identifier, parentChain: nextParentChain });
}
return constructedInstance;
});
instance = new implementation(...instanceArgs);
} else if (isCustomConstructableService(implementation)) {
instance = implementation();
} else {
throw new InstantiationError(`No implementation was given!`, { identifier, parentChain });
}
return registrationRecord.kind === "SINGLETON" ? this.#setInstance(identifier, instance) : instance;
}
};
export {
CONSTRUCTOR_ARGUMENTS_SYMBOL,
CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER,
DIContainer
};
//# sourceMappingURL=index.js.map