@rmk-labs/typescript-dependency-injector
Version:
Dependency injection framework for TypeScript
532 lines (527 loc) • 16.6 kB
JavaScript
// src/utils.ts
function isProvider(value) {
return value !== null && typeof value === "object" && PROVIDER_SYMBOL in value && value[PROVIDER_SYMBOL] === true;
}
function resolveProviders(value, seen = /* @__PURE__ */ new WeakSet(), resolved = /* @__PURE__ */ new WeakMap()) {
if (value === null || value === void 0) {
return value;
}
if (typeof value !== "object" && typeof value !== "function") {
return value;
}
if (typeof value === "function") {
return value;
}
if (isProvider(value)) {
return value.provide();
}
if (resolved.has(value)) {
return resolved.get(value);
}
if (seen.has(value)) {
return value;
}
seen.add(value);
if (value instanceof Date) {
return value;
}
if (value instanceof RegExp) {
return value;
}
if (Array.isArray(value)) {
const resolvedArray = [];
resolved.set(value, resolvedArray);
for (const item of value) {
resolvedArray.push(resolveProviders(item, seen, resolved));
}
seen.delete(value);
return resolvedArray;
}
if (value instanceof Map) {
const resolvedMap = /* @__PURE__ */ new Map();
resolved.set(value, resolvedMap);
for (const [key, val] of value.entries()) {
resolvedMap.set(key, resolveProviders(val, seen, resolved));
}
seen.delete(value);
return resolvedMap;
}
if (value instanceof Set) {
const resolvedSet = /* @__PURE__ */ new Set();
resolved.set(value, resolvedSet);
for (const item of value.values()) {
resolvedSet.add(resolveProviders(item, seen, resolved));
}
seen.delete(value);
return resolvedSet;
}
const proto = Object.getPrototypeOf(value);
const isPlainObject = proto === Object.prototype || proto === null;
if (!isPlainObject) {
return value;
}
const resolvedObj = {};
resolved.set(value, resolvedObj);
for (const key of Object.keys(value)) {
resolvedObj[key] = resolveProviders(value[key], seen, resolved);
}
seen.delete(value);
return resolvedObj;
}
// src/extend.ts
var EXTEND_SYMBOL = Symbol("extend");
var _a;
_a = EXTEND_SYMBOL;
var Extend = class {
constructor(defaults) {
this.defaults = defaults;
this[_a] = true;
}
};
function isExtend(value) {
return !!value && typeof value === "object" && EXTEND_SYMBOL in value && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
value[EXTEND_SYMBOL] === true;
}
// src/providers.ts
var PROVIDER_SYMBOL = Symbol("@@Provider");
var _a2;
_a2 = PROVIDER_SYMBOL;
var BaseProvider = class {
constructor() {
this[_a2] = true;
this._overridingProviders = [];
}
/**
* Returns a readonly copy of the overriding providers stack.
*/
get overrides() {
return [...this._overridingProviders];
}
/**
* Overrides the current provider with another provider.
* When the provider is called, it will delegate to the last overriding provider.
* @param provider - The provider to override with
* @throws Error if the argument is not a provider
*/
override(provider) {
if (!provider || typeof provider.provide !== "function") {
throw new Error("Override argument must be a provider with a provide() method");
}
this._overridingProviders.push(provider);
}
/**
* Resets the last overriding provider and returns it.
* @returns The provider that was removed from the override stack
* @throws Error if there are no overriding providers
*/
resetLastOverriding() {
if (this._overridingProviders.length === 0) {
throw new Error("No overriding providers to reset");
}
return this._overridingProviders.pop();
}
/**
* Resets all overriding providers at once and returns them.
* @returns An array of all providers that were removed from the override stack
*/
resetOverride() {
const removed = [...this._overridingProviders];
this._overridingProviders = [];
return removed;
}
/**
* Returns true if the provider is currently overridden, false otherwise.
*/
isOverridden() {
return this._overridingProviders.length > 0;
}
/**
* Returns a Delegate provider that wraps this provider.
* The Delegate's provide() method returns this provider instance.
*/
get provider() {
return new Delegate(this);
}
/**
* Protected method for providers to delegate to the overriding provider if one exists.
* Should be called at the beginning of each provider's provide() method.
*/
_delegateIfOverridden(...args) {
if (this._overridingProviders.length > 0) {
const overridingProvider = this._overridingProviders[this._overridingProviders.length - 1];
return overridingProvider.provide(...args);
}
return void 0;
}
};
var Factory = class extends BaseProvider {
constructor(factory, ...injectedArgs) {
super();
this.factory = factory;
this.injectedArgs = injectedArgs;
this.isConstructor = typeof factory === "function" && factory.prototype !== void 0 && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
factory.prototype.constructor === factory;
}
provide(...args) {
const overridden = this._delegateIfOverridden(...args);
if (overridden !== void 0) {
return overridden;
}
const hasExtend = this.injectedArgs.some((arg) => isExtend(arg));
const resolvedArgs = this.injectedArgs.map((arg) => this._resolveArg(arg, args));
if (this.isConstructor) {
if (hasExtend) {
return new this.factory(...resolvedArgs);
}
return new this.factory(...args, ...resolvedArgs);
} else {
if (hasExtend) {
return this.factory(...resolvedArgs);
}
return this.factory(...args, ...resolvedArgs);
}
}
/**
* Resolves an argument, handling Extend instances specially.
* If the argument is an Extend instance and context args are provided,
* merges the context with defaults (context takes priority).
*/
_resolveArg(arg, contextArgs) {
if (isExtend(arg)) {
const defaults = arg.defaults;
const context = contextArgs.length > 0 ? contextArgs[0] : {};
const resolvedDefaults = {};
for (const key in defaults) {
if (!(key in context)) {
resolvedDefaults[key] = resolveProviders(defaults[key]);
}
}
return { ...resolvedDefaults, ...context };
}
return resolveProviders(arg);
}
};
var Singleton = class extends Factory {
constructor() {
super(...arguments);
this.instance = null;
}
provide(...args) {
const overridden = this._delegateIfOverridden(...args);
if (overridden !== void 0) {
return overridden;
}
if (this.instance === null) {
this.instance = super.provide(...args);
}
return this.instance;
}
resetInstance() {
this.instance = null;
}
};
var Delegate = class extends BaseProvider {
constructor(delegatedProvider) {
super();
this.delegatedProvider = delegatedProvider;
}
provide() {
const overridden = this._delegateIfOverridden();
if (overridden !== void 0) {
return overridden;
}
return this.delegatedProvider;
}
};
// src/container.ts
var DeclarativeContainer = class {
resetProviderOverrides() {
for (const key of Reflect.ownKeys(this)) {
const val = this[key];
if (val instanceof BaseProvider) {
val.resetOverride();
}
}
}
resetSingletonInstances() {
for (const key of Reflect.ownKeys(this)) {
const val = this[key];
if (val instanceof Singleton) {
val.resetInstance();
}
}
}
};
// src/inject.ts
var CONTAINER_PROVIDER_TOKENS = /* @__PURE__ */ new WeakMap();
var CONTAINER_PROVIDER_AS_VALUE_TOKENS = /* @__PURE__ */ new WeakMap();
var PARAM_INJECT_IDS = /* @__PURE__ */ new WeakMap();
var DECORATION_SITES = /* @__PURE__ */ new WeakMap();
var CONSTRUCTOR_DECORATION_SITES = /* @__PURE__ */ new WeakMap();
function getTokenFor(containerCtor, key) {
let byKey = CONTAINER_PROVIDER_TOKENS.get(containerCtor);
if (!byKey) {
byKey = /* @__PURE__ */ new Map();
CONTAINER_PROVIDER_TOKENS.set(containerCtor, byKey);
}
let token = byKey.get(key);
if (!token) {
token = Symbol(`di:token:${String(key)}`);
byKey.set(key, token);
}
return token;
}
function getProviderTokenFor(containerCtor, key) {
let byKey = CONTAINER_PROVIDER_AS_VALUE_TOKENS.get(containerCtor);
if (!byKey) {
byKey = /* @__PURE__ */ new Map();
CONTAINER_PROVIDER_AS_VALUE_TOKENS.set(containerCtor, byKey);
}
let token = byKey.get(key);
if (!token) {
token = Symbol(`di:provider-token:${String(key)}`);
byKey.set(key, token);
}
return token;
}
function resolveDecoratedFunction(target, propertyKey) {
if (propertyKey != null && target) {
return target[propertyKey];
}
if (typeof target === "function") {
return target;
}
return void 0;
}
function isProviderLike(value) {
return !!value && typeof value === "object" && PROVIDER_SYMBOL in value && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
value[PROVIDER_SYMBOL] === true;
}
function createInject(options) {
const { containerClass } = options;
const instance = new containerClass();
const markers = {};
const wiredContainers = /* @__PURE__ */ new Set();
const wrappedMethods = /* @__PURE__ */ new WeakMap();
for (const key of Reflect.ownKeys(instance)) {
const val = instance[key];
if (!isProviderLike(val)) continue;
const token = getTokenFor(containerClass, key);
const providerToken = getProviderTokenFor(containerClass, key);
const decorator = (_target, _propertyKey, parameterIndex) => {
if (_propertyKey === void 0) {
if (typeof _target === "function") {
let constructorSites = CONSTRUCTOR_DECORATION_SITES.get(containerClass);
if (!constructorSites) {
constructorSites = [];
CONSTRUCTOR_DECORATION_SITES.set(containerClass, constructorSites);
}
constructorSites.push({
constructor: _target,
paramIndex: parameterIndex,
token
});
}
return;
}
const fn = resolveDecoratedFunction(_target, _propertyKey);
if (!fn) return;
let paramMap = PARAM_INJECT_IDS.get(fn);
if (!paramMap) {
paramMap = /* @__PURE__ */ new Map();
PARAM_INJECT_IDS.set(fn, paramMap);
}
paramMap.set(parameterIndex, token);
let sites = DECORATION_SITES.get(containerClass);
if (!sites) {
sites = [];
DECORATION_SITES.set(containerClass, sites);
}
sites.push({
target: _target,
propertyKey: _propertyKey,
paramIndex: parameterIndex,
token
});
};
const providerDecorator = (_target, _propertyKey, parameterIndex) => {
if (_propertyKey === void 0) {
if (typeof _target === "function") {
let constructorSites = CONSTRUCTOR_DECORATION_SITES.get(containerClass);
if (!constructorSites) {
constructorSites = [];
CONSTRUCTOR_DECORATION_SITES.set(containerClass, constructorSites);
}
constructorSites.push({
constructor: _target,
paramIndex: parameterIndex,
token: providerToken
});
}
return;
}
const fn = resolveDecoratedFunction(_target, _propertyKey);
if (!fn) return;
let paramMap = PARAM_INJECT_IDS.get(fn);
if (!paramMap) {
paramMap = /* @__PURE__ */ new Map();
PARAM_INJECT_IDS.set(fn, paramMap);
}
paramMap.set(parameterIndex, providerToken);
let sites = DECORATION_SITES.get(containerClass);
if (!sites) {
sites = [];
DECORATION_SITES.set(containerClass, sites);
}
sites.push({
target: _target,
propertyKey: _propertyKey,
paramIndex: parameterIndex,
token: providerToken
});
};
Object.defineProperty(decorator, "provider", {
value: providerDecorator,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(markers, key, {
value: decorator,
enumerable: true,
configurable: false,
writable: false
});
}
function findProviderByToken(container, token) {
for (const key of Reflect.ownKeys(container)) {
const containerToken = getTokenFor(containerClass, key);
const providerContainerToken = getProviderTokenFor(containerClass, key);
if (containerToken === token) {
const provider = container[key];
if (isProviderLike(provider)) {
return provider;
}
} else if (providerContainerToken === token) {
const provider = container[key];
if (isProviderLike(provider)) {
return provider.provider;
}
}
}
return void 0;
}
function wire(container) {
wiredContainers.add(container);
const sites = DECORATION_SITES.get(containerClass);
if (!sites) return;
const sitesByTargetAndKey = /* @__PURE__ */ new Map();
for (const site of sites) {
let byKey = sitesByTargetAndKey.get(site.target);
if (!byKey) {
byKey = /* @__PURE__ */ new Map();
sitesByTargetAndKey.set(site.target, byKey);
}
let sitesForKey = byKey.get(site.propertyKey);
if (!sitesForKey) {
sitesForKey = [];
byKey.set(site.propertyKey, sitesForKey);
}
sitesForKey.push(site);
}
for (const [target, byKey] of sitesByTargetAndKey.entries()) {
let wrapped = wrappedMethods.get(target);
if (!wrapped) {
wrapped = /* @__PURE__ */ new Set();
wrappedMethods.set(target, wrapped);
}
for (const [propertyKey, sitesForKey] of byKey.entries()) {
if (wrapped.has(propertyKey)) continue;
const originalFn = target[propertyKey];
if (typeof originalFn !== "function") continue;
const wrapper = function(...args) {
const newArgs = [...args];
const activeContainer = wiredContainers.values().next().value;
if (activeContainer) {
const tokensByIndex = /* @__PURE__ */ new Map();
for (const site of sitesForKey) {
tokensByIndex.set(site.paramIndex, site.token);
}
for (const [paramIndex, token] of tokensByIndex.entries()) {
if (paramIndex >= newArgs.length || newArgs[paramIndex] === void 0) {
const provider = findProviderByToken(activeContainer, token);
if (provider) {
newArgs[paramIndex] = provider.provide();
}
}
}
}
return originalFn.apply(this, newArgs);
};
target[propertyKey] = wrapper;
wrapped.add(propertyKey);
}
}
}
function unwire(container) {
wiredContainers.delete(container);
}
function Injectable(target) {
const constructorSites = CONSTRUCTOR_DECORATION_SITES.get(containerClass);
if (!constructorSites) {
return target;
}
const sitesForConstructor = constructorSites.filter((site) => site.constructor === target);
if (sitesForConstructor.length === 0) {
return target;
}
const tokensByIndex = /* @__PURE__ */ new Map();
for (const site of sitesForConstructor) {
tokensByIndex.set(site.paramIndex, site.token);
}
const proxy = new Proxy(target, {
construct(Target, args) {
const newArgs = [...args];
const activeContainer = wiredContainers.values().next().value;
if (activeContainer) {
for (const [paramIndex, token] of tokensByIndex.entries()) {
if (paramIndex >= newArgs.length || newArgs[paramIndex] === void 0) {
const provider = findProviderByToken(activeContainer, token);
if (provider) {
newArgs[paramIndex] = provider.provide();
}
}
}
}
return new Target(...newArgs);
}
});
return proxy;
}
return { ...markers, wire, unwire, Injectable };
}
function getInjectedParamIds(fn) {
return PARAM_INJECT_IDS.get(fn);
}
function getMarkerFor(containerCtor, key) {
return getTokenFor(containerCtor, key);
}
function InstanceOf(_type) {
return void 0;
}
function ProviderOf(_type) {
return void 0;
}
export {
BaseProvider,
DeclarativeContainer,
Delegate,
Extend,
Factory,
InstanceOf,
ProviderOf,
Singleton,
createInject,
getInjectedParamIds,
getMarkerFor
};