@adonisjs/fold
Version:
Simplest and straightforward implementation of IoC container in JavaScript
536 lines (535 loc) • 22 kB
JavaScript
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 decorator?`;
return error;
}
return createError(`Cannot construct "[class ${binding.name}]" class. Container is not able to resolve its dependencies. Did you forget to use 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 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 };