UNPKG

contexify

Version:

A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.

352 lines 14 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import { DecoratorFactory, MetadataAccessor, MetadataInspector, ParameterDecoratorFactory, PropertyDecoratorFactory, Reflector } from "metarize"; import { Binding } from "../binding/binding.js"; import { filterByTag, isBindingAddress, isBindingTagFilter } from "../binding/binding-filter.js"; import { ContextView, createViewGetter } from "../context/context-view.js"; import { ResolutionSession } from "../resolution/resolution-session.js"; import createDebugger from "../utils/debug.js"; const INJECT_PARAMETERS_KEY = MetadataAccessor.create("inject:parameters"); const INJECT_PROPERTIES_KEY = MetadataAccessor.create("inject:properties"); const INJECT_METHODS_KEY = MetadataAccessor.create("inject:methods"); const debug = createDebugger("contexify:inject"); function inject(bindingSelector, metadata, resolve) { if (typeof bindingSelector === "function" && !resolve) { resolve = resolveValuesByFilter; } const injectionMetadata = Object.assign({ decorator: "@inject" }, metadata); if (injectionMetadata.bindingComparator && !resolve) { throw new Error("Binding comparator is only allowed with a binding filter"); } if (!bindingSelector && typeof resolve !== "function") { throw new Error("A non-empty binding selector or resolve function is required for @inject"); } return /* @__PURE__ */ __name(function markParameterOrPropertyAsInjected(target, member, methodDescriptorOrParameterIndex) { if (typeof methodDescriptorOrParameterIndex === "number") { const paramDecorator = ParameterDecoratorFactory.createDecorator( INJECT_PARAMETERS_KEY, { target, member, methodDescriptorOrParameterIndex, bindingSelector, metadata: injectionMetadata, resolve }, // Do not deep clone the spec as only metadata is mutable and it's // shallowly cloned { cloneInputSpec: false, decoratorName: injectionMetadata.decorator } ); paramDecorator(target, member, methodDescriptorOrParameterIndex); } else if (member) { if (target instanceof Function) { throw new Error("@inject is not supported for a static property: " + DecoratorFactory.getTargetName(target, member)); } if (methodDescriptorOrParameterIndex) { throw new Error("@inject cannot be used on a method: " + DecoratorFactory.getTargetName(target, member, methodDescriptorOrParameterIndex)); } const propDecorator = PropertyDecoratorFactory.createDecorator( INJECT_PROPERTIES_KEY, { target, member, methodDescriptorOrParameterIndex, bindingSelector, metadata: injectionMetadata, resolve }, // Do not deep clone the spec as only metadata is mutable and it's // shallowly cloned { cloneInputSpec: false, decoratorName: injectionMetadata.decorator } ); propDecorator(target, member); } else { throw new Error("@inject can only be used on a property or a method parameter"); } }, "markParameterOrPropertyAsInjected"); } __name(inject, "inject"); (function(Getter2) { function fromValue(value) { return () => Promise.resolve(value); } __name(fromValue, "fromValue"); Getter2.fromValue = fromValue; })(Getter || (Getter = {})); (function(inject2) { inject2.getter = /* @__PURE__ */ __name(function injectGetter(bindingSelector, metadata) { metadata = Object.assign({ decorator: "@inject.getter" }, metadata); return inject2(bindingSelector, metadata, isBindingAddress(bindingSelector) ? resolveAsGetter : resolveAsGetterByFilter); }, "injectGetter"); inject2.setter = /* @__PURE__ */ __name(function injectSetter(bindingKey, metadata) { metadata = Object.assign({ decorator: "@inject.setter" }, metadata); return inject2(bindingKey, metadata, resolveAsSetter); }, "injectSetter"); inject2.binding = /* @__PURE__ */ __name(function injectBinding(bindingKey, metadata) { metadata = Object.assign({ decorator: "@inject.binding" }, metadata); return inject2(bindingKey ?? "", metadata, resolveAsBinding); }, "injectBinding"); inject2.tag = /* @__PURE__ */ __name(function injectByTag(bindingTag, metadata) { metadata = Object.assign({ decorator: "@inject.tag", tag: bindingTag }, metadata); return inject2(filterByTag(bindingTag), metadata); }, "injectByTag"); inject2.view = /* @__PURE__ */ __name(function injectContextView(bindingFilter, metadata) { metadata = Object.assign({ decorator: "@inject.view" }, metadata); return inject2(bindingFilter, metadata, resolveAsContextView); }, "injectContextView"); inject2.context = /* @__PURE__ */ __name(function injectContext() { return inject2("", { decorator: "@inject.context" }, (ctx) => ctx); }, "injectContext"); })(inject || (inject = {})); function assertTargetType(injection, expectedType, expectedTypeName) { const targetName = ResolutionSession.describeInjection(injection).targetName; const targetType = inspectTargetType(injection); if (targetType && targetType !== expectedType) { expectedTypeName = expectedTypeName ?? expectedType.name; throw new Error(`The type of ${targetName} (${targetType.name}) is not ${expectedTypeName}`); } return targetName; } __name(assertTargetType, "assertTargetType"); function resolveAsGetter(ctx, injection, _session) { assertTargetType(injection, Function, "Getter function"); const bindingSelector = injection.bindingSelector; const options = { // We should start with a new session for `getter` resolution to avoid // possible circular dependencies session: void 0, ...injection.metadata }; return /* @__PURE__ */ __name(function getter() { return ctx.get(bindingSelector, options); }, "getter"); } __name(resolveAsGetter, "resolveAsGetter"); function resolveAsSetter(ctx, injection) { const targetName = assertTargetType(injection, Function, "Setter function"); const bindingSelector = injection.bindingSelector; if (!isBindingAddress(bindingSelector)) { throw new Error(`@inject.setter (${targetName}) does not allow BindingFilter.`); } if (bindingSelector === "") { throw new Error("Binding key is not set for @inject.setter"); } return /* @__PURE__ */ __name(function setter(value) { const binding = findOrCreateBindingForInjection(ctx, injection); binding.to(value); }, "setter"); } __name(resolveAsSetter, "resolveAsSetter"); function resolveAsBinding(ctx, injection, session) { const targetName = assertTargetType(injection, Binding); const bindingSelector = injection.bindingSelector; if (!isBindingAddress(bindingSelector)) { throw new Error(`@inject.binding (${targetName}) does not allow BindingFilter.`); } return findOrCreateBindingForInjection(ctx, injection, session); } __name(resolveAsBinding, "resolveAsBinding"); function findOrCreateBindingForInjection(ctx, injection, session) { if (injection.bindingSelector === "") return session?.currentBinding; const bindingCreation = injection.metadata && injection.metadata.bindingCreation; const binding = ctx.findOrCreateBinding(injection.bindingSelector, bindingCreation); return binding; } __name(findOrCreateBindingForInjection, "findOrCreateBindingForInjection"); function shouldSkipBaseConstructorInjection(targetClass) { const classDef = targetClass.toString(); const MAX_CLASS_DEF_LENGTH = 5e4; if (classDef.length > MAX_CLASS_DEF_LENGTH) { if (debug.enabled) { debug("Class definition too long, skipping regex check for %s", targetClass.constructor?.name || "unknown"); } return false; } return ( /* * Handle class decorators that return a new constructor * A class decorator can return a new constructor that mixes in * additional properties/methods. * * @example * ```ts * class extends baseConstructor { * // The constructor calls `super(...arguments)` * constructor() { * super(...arguments); * } * classProperty = 'a classProperty'; * classFunction() { * return 'a classFunction'; * } * }; * ``` * * We check the following pattern: * ```ts * constructor() { * super(...arguments); * } * ``` */ // Use non-greedy quantifiers (*? and +?) to reduce backtracking !classDef.match(/\s+constructor\s*?\(\s*?\)\s*?\{\s*?super\(\.\.\.arguments\)/) && /* * Handle subclasses without explicit constructors * * @example * ```ts * class BaseClass { * constructor(@inject('foo') protected foo: string) {} * // ... * } * * class SubClass extends BaseClass { * // No explicit constructor is present * * @inject('bar') * private bar: number; * // ... * }; * */ // Use non-greedy quantifiers and keep multiline flag // eslint-disable-next-line no-useless-escape classDef.match(/\s+constructor\s*?\([^)]*?\)\s*?\{/m) ); } __name(shouldSkipBaseConstructorInjection, "shouldSkipBaseConstructorInjection"); function describeInjectedArguments(target, method) { method = method ?? ""; const cache = MetadataInspector.getAllMethodMetadata(INJECT_METHODS_KEY, target, { ownMetadataOnly: true }) ?? {}; let meta = cache[method]; if (meta) return meta; const options = {}; if (method === "") { if (shouldSkipBaseConstructorInjection(target)) { options.ownMetadataOnly = true; } } else if (Object.hasOwn(target, method)) { options.ownMetadataOnly = true; } meta = MetadataInspector.getAllParameterMetadata(INJECT_PARAMETERS_KEY, target, method, options) ?? []; cache[method] = meta; MetadataInspector.defineMetadata(INJECT_METHODS_KEY, cache, target); return meta; } __name(describeInjectedArguments, "describeInjectedArguments"); function inspectTargetType(injection) { if (typeof injection.methodDescriptorOrParameterIndex === "number") { const designType = MetadataInspector.getDesignTypeForMethod(injection.target, injection.member); return designType?.parameterTypes?.[injection.methodDescriptorOrParameterIndex]; } return MetadataInspector.getDesignTypeForProperty(injection.target, injection.member); } __name(inspectTargetType, "inspectTargetType"); function resolveValuesByFilter(ctx, injection, session) { assertTargetType(injection, Array); const bindingFilter = injection.bindingSelector; const view = new ContextView(ctx, bindingFilter, injection.metadata.bindingComparator); return view.resolve(session); } __name(resolveValuesByFilter, "resolveValuesByFilter"); function resolveAsGetterByFilter(ctx, injection, session) { assertTargetType(injection, Function, "Getter function"); const bindingFilter = injection.bindingSelector; return createViewGetter(ctx, bindingFilter, injection.metadata.bindingComparator, session); } __name(resolveAsGetterByFilter, "resolveAsGetterByFilter"); function resolveAsContextView(ctx, injection) { assertTargetType(injection, ContextView); const bindingFilter = injection.bindingSelector; const view = new ContextView(ctx, bindingFilter, injection.metadata.bindingComparator); view.open(); return view; } __name(resolveAsContextView, "resolveAsContextView"); function describeInjectedProperties(target) { const metadata = MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, target) ?? {}; return metadata; } __name(describeInjectedProperties, "describeInjectedProperties"); function inspectInjections(binding) { const json = {}; const ctor = binding.valueConstructor ?? binding.providerConstructor; if (ctor == null) return json; const constructorInjections = describeInjectedArguments(ctor, "").map(inspectInjection); if (constructorInjections.length) { json.constructorArguments = constructorInjections; } const propertyInjections = describeInjectedProperties(ctor.prototype); const properties = {}; for (const p in propertyInjections) { properties[p] = inspectInjection(propertyInjections[p]); } if (Object.keys(properties).length) { json.properties = properties; } return json; } __name(inspectInjections, "inspectInjections"); function inspectInjection(injection) { const injectionInfo = ResolutionSession.describeInjection(injection); const descriptor = {}; if (injectionInfo.targetName) { descriptor.targetName = injectionInfo.targetName; } if (isBindingAddress(injectionInfo.bindingSelector)) { descriptor.bindingKey = injectionInfo.bindingSelector.toString(); } else if (isBindingTagFilter(injectionInfo.bindingSelector)) { descriptor.bindingTagPattern = JSON.parse(JSON.stringify(injectionInfo.bindingSelector.bindingTagPattern)); } else { descriptor.bindingFilter = injectionInfo.bindingSelector?.name ?? "<function>"; } if (injectionInfo.metadata) { if (injectionInfo.metadata.decorator && injectionInfo.metadata.decorator !== "@inject") { descriptor.decorator = injectionInfo.metadata.decorator; } if (injectionInfo.metadata.optional) { descriptor.optional = injectionInfo.metadata.optional; } } return descriptor; } __name(inspectInjection, "inspectInjection"); function hasInjections(cls) { return MetadataInspector.getClassMetadata(INJECT_PARAMETERS_KEY, cls) != null || Reflector.getMetadata(INJECT_PARAMETERS_KEY.toString(), cls.prototype) != null || MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, cls.prototype) != null; } __name(hasInjections, "hasInjections"); var Getter; export { Getter, assertTargetType, describeInjectedArguments, describeInjectedProperties, hasInjections, inject, inspectInjections, inspectTargetType }; //# sourceMappingURL=inject.js.map