UNPKG

@adonisjs/fold

Version:

Simplest and straightforward implementation of IoC container in JavaScript

536 lines (535 loc) 22 kB
import "node:module"; import { debuglog, inspect } from "node:util"; import { InvalidArgumentsException, RuntimeException } from "@poppinss/utils/exception"; import diagnostics_channel from "node:diagnostics_channel"; import { defineStaticProperty, importDefault } from "@poppinss/utils"; import { parseImports } from "parse-imports"; var __defProp = Object.defineProperty; var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; var debug_default = debuglog("adonisjs:fold"); var Deferred = class { resolve; reject; promise = new Promise((resolve, reject) => { this.reject = reject; this.resolve = resolve; }); }; function isClass(value) { return typeof value === "function" && /^class(\s|{)/.test(value.toString()); } async function runAsAsync(callback, args) { return callback(...args); } function enqueue(callback) { let isComputingValue = false; let computedValue = { completed: false }; let computedError = { completed: false }; let queue = []; function resolvePromises(value) { isComputingValue = false; computedValue.completed = true; computedValue.value = value; queue.forEach((promise) => promise.resolve(value)); queue = []; } function rejectPromises(error) { isComputingValue = false; computedError.completed = true; computedError.error = error; queue.forEach((promise) => promise.reject(error)); queue = []; } return function(...args) { if (computedValue.completed) return computedValue.value; if (computedError.completed) throw computedError.error; if (isComputingValue) { const promise = new Deferred(); queue.push(promise); return promise.promise; } isComputingValue = true; return new Promise((resolve, reject) => { runAsAsync(callback, args).then((value) => { resolve({ value, cached: false }); resolvePromises({ value, cached: true }); }).catch((error) => { reject(error); rejectPromises(error); }); }); }; } async function resolveDefault(importPath, parentURL) { const resolvedPath = await import.meta.resolve(importPath, parentURL); const moduleExports = await import(resolvedPath); if (!moduleExports.default) throw new RuntimeException(`Missing export default from "${importPath}" module`, { cause: { source: resolvedPath } }); return moduleExports.default; } async function containerProvider(binding, property, resolver, runtimeValues) { const values = runtimeValues || []; if (!binding.containerInjections || !binding.containerInjections[property]) return values; const injections = binding.containerInjections[property].dependencies; const createError = binding.containerInjections[property].createError; if (values.length > injections.length) { if (debug_default.enabled) debug_default("created resolver plan. target: \"[class %s]\", property: \"%s\", injections: %O", binding.name, property, values.map((value, index) => { if (value !== void 0) return value; return injections[index]; })); return Promise.all(values.map((value, index) => { if (value !== void 0) return value; const injection = injections[index]; return resolver.resolveFor(binding, injection, void 0, createError); })); } if (debug_default.enabled) debug_default("created resolver plan. target: \"[class %s]\", property: \"%s\", injections: %O", binding.name, property, injections.map((injection, index) => { if (values[index] !== void 0) return values[index]; return injection; })); return Promise.all(injections.map((injection, index) => { if (values[index] !== void 0) return values[index]; return resolver.resolveFor(binding, injection, void 0, createError); })); } var tracing_channels_exports = /* @__PURE__ */ __exportAll({ containerMake: () => containerMake }); const containerMake = diagnostics_channel.tracingChannel("adonisjs.container.make"); var ContainerResolver = class ContainerResolver { #containerAliases; #containerContextualBindings; #containerBindings; #containerBindingValues; #containerSwaps; #containerHooks; #bindingValues = /* @__PURE__ */ new Map(); #options; constructor(container, options) { this.#containerBindings = container.bindings; this.#containerBindingValues = container.bindingValues; this.#containerSwaps = container.swaps; this.#containerHooks = container.hooks; this.#containerAliases = container.aliases; this.#containerContextualBindings = container.contextualBindings; this.#options = options; this.#bindingValues.set(ContainerResolver, this); } #invalidBindingException(parent, binding, createError) { if (parent) { const error = createError(`Cannot inject "${inspect(binding)}" in "[class ${parent.name}]"`); error.help = "The value is not a valid class"; return error; } return createError(`Cannot construct value "${inspect(binding)}" using container`); } #missingDependenciesException(parent, binding, createError) { if (parent) { const error = createError(`Cannot inject "[class ${binding.name}]" in "[class ${parent.name}]"`); error.help = `Container is not able to resolve "${parent.name}" class dependencies. Did you forget to use @inject() decorator?`; return error; } return createError(`Cannot construct "[class ${binding.name}]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?`); } #getBindingProvider(binding) { return binding.containerProvider; } #getBindingResolver(parent, binding) { const parentBindings = this.#containerContextualBindings.get(parent); if (!parentBindings) return; const bindingResolver = parentBindings.get(binding); if (!bindingResolver) return; return bindingResolver.resolver; } #emit(binding, value) { if (!this.#options.emitter) return; this.#options.emitter.emit("container_binding:resolved", { binding, value }); } async #execHooks(binding, value) { const callbacks = this.#containerHooks.get(binding); if (!callbacks || callbacks.size === 0) return; for (let callback of callbacks) await callback(value, this); } async #resolveFor(parent, binding, runtimeValues, createError = (message) => new RuntimeException(message)) { const isAClass = isClass(binding); if (typeof binding !== "string" && typeof binding !== "symbol" && !isAClass) throw this.#invalidBindingException(parent, binding, createError); if (isAClass && this.#containerSwaps.has(binding)) { const value = await this.#containerSwaps.get(binding)(this, runtimeValues); if (debug_default.enabled) debug_default("resolved swap for binding %O, resolved value :%O", binding, value); await this.#execHooks(binding, value); this.#emit(binding, value); return value; } const contextualResolver = isAClass && this.#getBindingResolver(parent, binding); if (contextualResolver) { const value = await contextualResolver(this, runtimeValues); if (debug_default.enabled) debug_default("resolved using contextual resolver binding %O, resolved value :%O", binding, value); await this.#execHooks(binding, value); this.#emit(binding, value); return value; } if (this.#bindingValues.has(binding)) { const value = this.#bindingValues.get(binding); if (debug_default.enabled) debug_default("resolved from resolver values %O, resolved value :%O", binding, value); this.#emit(binding, value); return value; } if (this.#containerBindingValues.has(binding)) { const value = this.#containerBindingValues.get(binding); if (debug_default.enabled) debug_default("resolved from container values %O, resolved value :%O", binding, value); this.#emit(binding, value); return value; } if (this.#containerBindings.has(binding)) { const containerBinding = this.#containerBindings.get(binding); let value; let executeHooks = true; if (containerBinding.isSingleton) { const result = await containerBinding.resolver(this, runtimeValues); value = result.value; executeHooks = !result.cached; } else value = await containerBinding.resolver(this, runtimeValues); if (debug_default.enabled) debug_default("resolved binding %O, resolved value :%O", binding, value); if (executeHooks) { const hooksPromise = this.#execHooks(binding, value); if (containerBinding.isSingleton) containerBinding.hooksPromise = hooksPromise.then(() => { delete containerBinding.hooksPromise; }); await hooksPromise; } if (containerBinding.isSingleton && containerBinding.hooksPromise) await containerBinding.hooksPromise; this.#emit(binding, value); return value; } if (isAClass) { let dependencies = []; const classConstructor = binding; const bindingProvider = this.#getBindingProvider(classConstructor); if (bindingProvider) dependencies = await bindingProvider(classConstructor, "_constructor", this, containerProvider, runtimeValues); else dependencies = await containerProvider(classConstructor, "_constructor", this, runtimeValues); if (dependencies.length < classConstructor.length) throw this.#missingDependenciesException(parent, binding, createError); const value = new binding(...dependencies); if (debug_default.enabled) debug_default("constructed class %O, resolved value :%O", binding, value); await this.#execHooks(binding, value); this.#emit(binding, value); return value; } throw createError(`Cannot resolve binding "${String(binding)}" from the container`); } hasBinding(binding) { return this.#containerAliases.has(binding) || this.#bindingValues.has(binding) || this.#containerBindingValues.has(binding) || this.#containerBindings.has(binding); } hasAllBindings(bindings) { return bindings.every((binding) => this.hasBinding(binding)); } async resolveFor(parent, binding, runtimeValues, createError = (message) => new RuntimeException(message)) { return containerMake.tracePromise(this.#resolveFor, containerMake.hasSubscribers ? { binding } : void 0, this, parent, binding, runtimeValues, createError); } async make(binding, runtimeValues, createError) { if (this.#containerAliases.has(binding)) return this.resolveFor(null, this.#containerAliases.get(binding), runtimeValues, createError); return this.resolveFor(null, binding, runtimeValues, createError); } async call(value, method, runtimeValues, createError = (message) => new RuntimeException(message)) { if (typeof value[method] !== "function") throw createError(`Missing method "${String(method)}" on "${inspect(value)}"`); if (debug_default.enabled) debug_default("calling method %s, on value :%O", method, value); let dependencies = []; const binding = value.constructor; const bindingProvider = this.#getBindingProvider(binding); if (bindingProvider) dependencies = await bindingProvider(binding, method, this, containerProvider, runtimeValues); else dependencies = await containerProvider(binding, method, this, runtimeValues); if (dependencies.length < value[method].length) throw createError(`Cannot call "${binding.name}.${String(method)}" method. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?`); return value[method](...dependencies); } bindValue(binding, value) { if (typeof binding !== "string" && typeof binding !== "symbol" && !isClass(binding)) throw new InvalidArgumentsException("The container binding key must be of type \"string\", \"symbol\", or a \"class constructor\""); debug_default("adding value to resolver \"%O\"", binding); this.#bindingValues.set(binding, value); } }; var ContextBindingsBuilder = class { #parent; #binding; #container; constructor(parent, container) { this.#parent = parent; this.#container = container; } asksFor(binding) { this.#binding = binding; return this; } provide(resolver) { if (!this.#binding) throw new RuntimeException("Missing value for contextual binding. Call \"asksFor\" method before calling the \"provide\" method"); this.#container.contextualBinding(this.#parent, this.#binding, resolver); } }; var Container = class { #aliases = /* @__PURE__ */ new Map(); #contextualBindings = /* @__PURE__ */ new Map(); #swaps = /* @__PURE__ */ new Map(); #bindings = /* @__PURE__ */ new Map(); #bindingValues = /* @__PURE__ */ new Map(); #hooks = /* @__PURE__ */ new Map(); #options; constructor(options) { this.#options = options || {}; } useEmitter(emitter) { this.#options.emitter = emitter; return this; } createResolver() { return new ContainerResolver({ bindings: this.#bindings, bindingValues: this.#bindingValues, swaps: this.#swaps, hooks: this.#hooks, aliases: this.#aliases, contextualBindings: this.#contextualBindings }, this.#options); } hasBinding(binding) { return this.#aliases.has(binding) || this.#bindingValues.has(binding) || this.#bindings.has(binding); } hasAllBindings(bindings) { return bindings.every((binding) => this.hasBinding(binding)); } make(binding, runtimeValues, createError) { return this.createResolver().make(binding, runtimeValues, createError); } call(value, method, runtimeValues, createError) { return this.createResolver().call(value, method, runtimeValues, createError); } alias(alias, value) { if (typeof alias !== "string" && typeof alias !== "symbol") throw new InvalidArgumentsException("The container alias key must be of type \"string\" or \"symbol\""); this.#aliases.set(alias, value); } bind(binding, resolver) { if (typeof binding !== "string" && typeof binding !== "symbol" && !isClass(binding)) throw new InvalidArgumentsException("The container binding key must be of type \"string\", \"symbol\", or a \"class constructor\""); debug_default("adding binding to container \"%O\"", binding); this.#bindings.set(binding, { resolver, isSingleton: false }); } bindValue(binding, value) { if (typeof binding !== "string" && typeof binding !== "symbol" && !isClass(binding)) throw new InvalidArgumentsException("The container binding key must be of type \"string\", \"symbol\", or a \"class constructor\""); debug_default("adding value to container %O", binding); this.#bindingValues.set(binding, value); } singleton(binding, resolver) { if (typeof binding !== "string" && typeof binding !== "symbol" && !isClass(binding)) throw new InvalidArgumentsException("The container binding key must be of type \"string\", \"symbol\", or a \"class constructor\""); debug_default("adding singleton to container %O", binding); this.#bindings.set(binding, { resolver: enqueue(resolver), isSingleton: true }); } swap(binding, resolver) { if (!isClass(binding)) throw new InvalidArgumentsException(`Cannot call swap on value "${inspect(binding)}". Only classes can be swapped`); debug_default("defining swap for %O", binding); this.#swaps.set(binding, resolver); } restore(binding) { debug_default("removing swap for %s", binding); this.#swaps.delete(binding); } restoreAll(bindings) { if (!bindings) { debug_default("removing all swaps"); this.#swaps.clear(); return; } for (let binding of bindings) this.restore(binding); } resolving(binding, callback) { binding = this.#aliases.get(binding) || binding; if (!this.#hooks.has(binding)) this.#hooks.set(binding, /* @__PURE__ */ new Set()); this.#hooks.get(binding).add(callback); } when(parent) { return new ContextBindingsBuilder(parent, this); } contextualBinding(parent, binding, resolver) { if (!isClass(binding)) throw new InvalidArgumentsException(`The binding value for contextual binding should be class`); if (!isClass(parent)) throw new InvalidArgumentsException(`The parent value for contextual binding should be class`); debug_default("adding contextual binding %O to %O", binding, parent); if (!this.#contextualBindings.has(parent)) this.#contextualBindings.set(parent, /* @__PURE__ */ new Map()); this.#contextualBindings.get(parent).set(binding, { resolver }); } }; function createDebuggingError(original) { return function createError(message) { const error = new RuntimeException(message); error.stack = original.stack; return error; }; } function initiateContainerInjections(target, method, createError) { defineStaticProperty(target, "containerInjections", { initialValue: {}, strategy: "inherit" }); target.containerInjections[method] = { createError, dependencies: [] }; } function defineConstructorInjections(target, createError) { const params = Reflect.getMetadata("design:paramtypes", target); /* c8 ignore next 3 */ if (!params) return; initiateContainerInjections(target, "_constructor", createError); if (debug_default.enabled) debug_default("defining constructor injections for %O, params %O", `[class: ${target.name}]`, params); for (const param of params) target.containerInjections._constructor.dependencies.push(param); } function defineMethodInjections(target, method, createError) { const constructor = target.constructor; const params = Reflect.getMetadata("design:paramtypes", target, method); /* c8 ignore next 3 */ if (!params) return; initiateContainerInjections(constructor, method, createError); if (debug_default.enabled) debug_default("defining method injections for %O, method %O, params %O", `[class ${constructor.name}]`, method, params); for (const param of params) constructor.containerInjections[method].dependencies.push(param); } function inject() { const createError = createDebuggingError(/* @__PURE__ */ new Error()); function injectDecorator(target, propertyKey) { if (!propertyKey) { defineConstructorInjections(target, createError); return; } defineMethodInjections(target, propertyKey, createError); } return injectDecorator; } function moduleCaller(target, method) { return { toCallable(container) { if (container) return async function(...args) { return container.call(await container.make(target), method, args); }; return async function(resolver, ...args) { return resolver.call(await resolver.make(target), method, args); }; }, toHandleMethod(container) { if (container) return { name: `${target.name}.${method}`, async handle(...args) { return container.call(await container.make(target), method, args); } }; return { name: `${target.name}.${method}`, async handle(resolver, ...args) { return resolver.call(await resolver.make(target), method, args); } }; } }; } function moduleImporter(importFn, method) { return { toCallable(container) { let defaultExport = null; if (container) return async function(...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await importDefault(importFn); return container.call(await container.make(defaultExport), method, args); }; return async function(resolver, ...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await importDefault(importFn); return resolver.call(await resolver.make(defaultExport), method, args); }; }, toHandleMethod(container) { let defaultExport = null; if (container) return { name: importFn.name, async handle(...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await importDefault(importFn); return container.call(await container.make(defaultExport), method, args); } }; return { name: importFn.name, async handle(resolver, ...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await importDefault(importFn); return resolver.call(await resolver.make(defaultExport), method, args); } }; } }; } function moduleExpression(expression, parentURL) { return { parse() { const parts = expression.split("."); if (parts.length === 1) return [expression, "handle"]; const method = parts.pop(); return [parts.join("."), method]; }, toCallable(container) { let defaultExport = null; const [importPath, method] = this.parse(); if (container) return async function(...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await resolveDefault(importPath, parentURL); return container.call(await container.make(defaultExport), method, args); }; return async function(resolver, ...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await resolveDefault(importPath, parentURL); return resolver.call(await resolver.make(defaultExport), method, args); }; }, toHandleMethod(container) { let defaultExport = null; const [importPath, method] = this.parse(); if (container) return { async handle(...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await resolveDefault(importPath, parentURL); return container.call(await container.make(defaultExport), method, args); } }; return { async handle(resolver, ...args) { if (!defaultExport || "hot" in import.meta) defaultExport = await resolveDefault(importPath, parentURL); return resolver.call(await resolver.make(defaultExport), method, args); } }; } }; } async function parseBindingReference(binding) { if (typeof binding === "string") { const tokens = binding.split("."); if (tokens.length === 1) return { moduleNameOrPath: binding, method: "handle" }; return { method: tokens.pop(), moduleNameOrPath: tokens.join(".") }; } const [bindingReference, method] = binding; const importedModule = [...await parseImports(bindingReference.toString())].find(($import) => $import.isDynamicImport && $import.moduleSpecifier.value); if (importedModule) return { moduleNameOrPath: importedModule.moduleSpecifier.value, method: method || "handle" }; return { moduleNameOrPath: bindingReference.name, method: method || "handle" }; } export { Container, ContainerResolver, inject, moduleCaller, moduleExpression, moduleImporter, parseBindingReference, tracing_channels_exports as tracingChannels };