awilix
Version:
Extremely powerful dependency injection container.
1,599 lines (1,589 loc) • 53 kB
JavaScript
import * as util from 'util';
import glob from 'fast-glob';
import * as path from 'path';
import { importModule } from './load-module-native.js';
import { camelCase } from 'camel-case';
import { pathToFileURL } from 'url';
/**
* Newline.
*/
const EOL = '\n';
/**
* An extendable error class.
* @author https://github.com/bjyoungblood/es6-error/
*/
class ExtendableError extends Error {
/**
* Constructor for the error.
*
* @param {String} message
* The error message.
*/
constructor(message) {
super(message);
// extending Error is weird and does not propagate `message`
Object.defineProperty(this, 'message', {
enumerable: false,
value: message,
});
Object.defineProperty(this, 'name', {
enumerable: false,
value: this.constructor.name,
});
// Not all browsers have this function.
/* istanbul ignore else */
if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, this.constructor);
}
else {
Object.defineProperty(this, 'stack', {
enumerable: false,
value: Error(message).stack,
writable: true,
configurable: true,
});
}
}
}
/**
* Base error for all Awilix-specific errors.
*/
class AwilixError extends ExtendableError {
}
/**
* Error thrown to indicate a type mismatch.
*/
class AwilixTypeError extends AwilixError {
/**
* Constructor, takes the function name, expected and given
* type to produce an error.
*
* @param {string} funcDescription
* Name of the function being guarded.
*
* @param {string} paramName
* The parameter there was an issue with.
*
* @param {string} expectedType
* Name of the expected type.
*
* @param {string} givenType
* Name of the given type.
*/
constructor(funcDescription, paramName, expectedType, givenType) {
super(`${funcDescription}: expected ${paramName} to be ${expectedType}, but got ${givenType}.`);
}
/**
* Asserts the given condition, throws an error otherwise.
*
* @param {*} condition
* The condition to check
*
* @param {string} funcDescription
* Name of the function being guarded.
*
* @param {string} paramName
* The parameter there was an issue with.
*
* @param {string} expectedType
* Name of the expected type.
*
* @param {string} givenType
* Name of the given type.
*/
static assert(condition, funcDescription, paramName, expectedType, givenType) {
if (!condition) {
throw new AwilixTypeError(funcDescription, paramName, expectedType, givenType);
}
return condition;
}
}
/**
* A nice error class so we can do an instanceOf check.
*/
class AwilixResolutionError extends AwilixError {
/**
* Constructor, takes the registered modules and unresolved tokens
* to create a message.
*
* @param {string|symbol} name
* The name of the module that could not be resolved.
*
* @param {string[]} resolutionStack
* The current resolution stack
*/
constructor(name, resolutionStack, message) {
const stringName = name.toString();
const nameStack = resolutionStack.map(({ name: val }) => val.toString());
nameStack.push(stringName);
const resolutionPathString = nameStack.join(' -> ');
let msg = `Could not resolve '${stringName}'.`;
if (message) {
msg += ` ${message}`;
}
msg += EOL + EOL;
msg += `Resolution path: ${resolutionPathString}`;
super(msg);
}
}
/**
* A nice error class so we can do an instanceOf check.
*/
class AwilixRegistrationError extends AwilixError {
/**
* Constructor, takes the registered modules and unresolved tokens
* to create a message.
*
* @param {string|symbol} name
* The name of the module that could not be registered.
*/
constructor(name, message) {
const stringName = name.toString();
let msg = `Could not register '${stringName}'.`;
if (message) {
msg += ` ${message}`;
}
super(msg);
}
}
/**
* Resolution modes.
*/
const InjectionMode = {
/**
* The dependencies will be resolved by injecting the cradle proxy.
*
* @type {String}
*/
PROXY: 'PROXY',
/**
* The dependencies will be resolved by inspecting parameter names of the function/constructor.
*
* @type {String}
*/
CLASSIC: 'CLASSIC',
};
/**
* Lifetime types.
*/
const Lifetime = {
/**
* The registration will be resolved once and only once.
* @type {String}
*/
SINGLETON: 'SINGLETON',
/**
* The registration will be resolved every time (never cached).
* @type {String}
*/
TRANSIENT: 'TRANSIENT',
/**
* The registration will be resolved once per scope.
* @type {String}
*/
SCOPED: 'SCOPED',
};
/**
* Returns true if and only if the first lifetime is strictly longer than the second.
*/
function isLifetimeLonger(a, b) {
return ((a === Lifetime.SINGLETON && b !== Lifetime.SINGLETON) ||
(a === Lifetime.SCOPED && b === Lifetime.TRANSIENT));
}
/**
* Creates a tokenizer for the specified source.
*
* @param source
*/
function createTokenizer(source) {
const end = source.length;
let pos = 0;
let type = 'EOF';
let value = '';
let flags = 0 /* TokenizerFlags.None */;
// These are used to greedily skip as much as possible.
// Whenever we reach a paren, we increment these.
let parenLeft = 0;
let parenRight = 0;
return {
next,
done,
};
/**
* Advances the tokenizer and returns the next token.
*/
function next(nextFlags = 0 /* TokenizerFlags.None */) {
flags = nextFlags;
advance();
return createToken();
}
/**
* Advances the tokenizer state.
*/
function advance() {
value = '';
type = 'EOF';
while (true) {
if (pos >= end) {
return (type = 'EOF');
}
const ch = source.charAt(pos);
// Whitespace is irrelevant
if (isWhiteSpace(ch)) {
pos++;
continue;
}
switch (ch) {
case '(':
pos++;
parenLeft++;
return (type = ch);
case ')':
pos++;
parenRight++;
return (type = ch);
case '*':
pos++;
return (type = ch);
case ',':
pos++;
return (type = ch);
case '=':
pos++;
if ((flags & 1 /* TokenizerFlags.Dumb */) === 0) {
// Not in dumb-mode, so attempt to skip.
skipExpression();
}
// We need to know that there's a default value so we can
// skip it if it does not exist when resolving.
return (type = ch);
case '/': {
pos++;
const nextCh = source.charAt(pos);
if (nextCh === '/') {
skipUntil((c) => c === '\n', true);
pos++;
}
if (nextCh === '*') {
skipUntil((c) => {
const closing = source.charAt(pos + 1);
return c === '*' && closing === '/';
}, true);
pos++;
}
break;
}
default:
// Scans an identifier.
if (isIdentifierStart(ch)) {
scanIdentifier();
return type;
}
// Elegantly skip over tokens we don't care about.
pos++;
}
}
}
/**
* Scans an identifier, given it's already been proven
* we are ready to do so.
*/
function scanIdentifier() {
const identStart = source.charAt(pos);
const start = ++pos;
while (isIdentifierPart(source.charAt(pos))) {
pos++;
}
value = '' + identStart + source.substring(start, pos);
type = value === 'function' || value === 'class' ? value : 'ident';
if (type !== 'ident') {
value = '';
}
return value;
}
/**
* Skips everything until the next comma or the end of the parameter list.
* Checks the parenthesis balance so we correctly skip function calls.
*/
function skipExpression() {
skipUntil((ch) => {
const isAtRoot = parenLeft === parenRight + 1;
if (ch === ',' && isAtRoot) {
return true;
}
if (ch === '(') {
parenLeft++;
return false;
}
if (ch === ')') {
parenRight++;
if (isAtRoot) {
return true;
}
}
return false;
});
}
/**
* Skips strings and whitespace until the predicate is true.
*
* @param callback stops skipping when this returns `true`.
* @param dumb if `true`, does not skip whitespace and strings;
* it only stops once the callback returns `true`.
*/
function skipUntil(callback, dumb = false) {
while (pos < source.length) {
const ch = source.charAt(pos);
if (callback(ch)) {
return;
}
if (!dumb) {
if (isWhiteSpace(ch)) {
pos++;
continue;
}
if (isStringQuote(ch)) {
skipString();
continue;
}
}
pos++;
}
}
/**
* Given the current position is at a string quote, skips the entire string.
*/
function skipString() {
const quote = source.charAt(pos);
pos++;
while (pos < source.length) {
const ch = source.charAt(pos);
const prev = source.charAt(pos - 1);
// Checks if the quote was escaped.
if (ch === quote && prev !== '\\') {
pos++;
return;
}
// Template strings are a bit tougher, we want to skip the interpolated values.
if (quote === '`') {
const next = source.charAt(pos + 1);
if (next === '$') {
const afterDollar = source.charAt(pos + 2);
if (afterDollar === '{') {
// This is the start of an interpolation; skip the ${
pos = pos + 2;
// Skip strings and whitespace until we reach the ending }.
// This includes skipping nested interpolated strings. :D
skipUntil((ch) => ch === '}');
}
}
}
pos++;
}
}
/**
* Creates a token from the current state.
*/
function createToken() {
if (value) {
return { value, type };
}
return { type };
}
/**
* Determines if we are done parsing.
*/
function done() {
return type === 'EOF';
}
}
/**
* Determines if the given character is a whitespace character.
*
* @param {string} ch
* @return {boolean}
*/
function isWhiteSpace(ch) {
switch (ch) {
case '\r':
case '\n':
case ' ':
return true;
}
return false;
}
/**
* Determines if the specified character is a string quote.
* @param {string} ch
* @return {boolean}
*/
function isStringQuote(ch) {
switch (ch) {
case "'":
case '"':
case '`':
return true;
}
return false;
}
// NOTE: I've added the `.` character, optionally prefixed by `?` so
// that member expression paths are seen as identifiers.
// This is so we don't get a constructor token for stuff
// like `MyClass.prototype?.constructor()`
const IDENT_START_EXPR = /^[_$a-zA-Z\xA0-\uFFFF]$/;
const IDENT_PART_EXPR = /^[?._$a-zA-Z0-9\xA0-\uFFFF]$/;
/**
* Determines if the character is a valid JS identifier start character.
*/
function isIdentifierStart(ch) {
return IDENT_START_EXPR.test(ch);
}
/**
* Determines if the character is a valid JS identifier start character.
*/
function isIdentifierPart(ch) {
return IDENT_PART_EXPR.test(ch);
}
/**
* Quick flatten utility to flatten a 2-dimensional array.
*
* @param {Array<Array<Item>>} array
* The array to flatten.
*
* @return {Array<Item>}
* The flattened array.
*/
function flatten(array) {
const result = [];
array.forEach((arr) => {
arr.forEach((item) => {
result.push(item);
});
});
return result;
}
/**
* Creates a { name: value } object if the input isn't already in that format.
*
* @param {string|object} name
* Either a string or an object.
*
* @param {*} value
* The value, only used if name is not an object.
*
* @return {object}
*/
function nameValueToObject(name, value) {
const obj = name;
if (typeof obj === 'string' || typeof obj === 'symbol') {
return { [name]: value };
}
return obj;
}
/**
* Returns the last item in the array.
*
* @param {*[]} arr
* The array.
*
* @return {*}
* The last element.
*/
function last(arr) {
return arr[arr.length - 1];
}
/**
* Determines if the given function is a class.
*
* @param {Function} fn
* @return {boolean}
*/
function isClass(
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
fn) {
if (typeof fn !== 'function') {
return false;
}
// Should only need 2 tokens.
const tokenizer = createTokenizer(fn.toString());
const first = tokenizer.next();
if (first.type === 'class') {
return true;
}
const second = tokenizer.next();
if (first.type === 'function' && second.value) {
if (second.value[0] === second.value[0].toUpperCase()) {
return true;
}
}
return false;
}
/**
* Determines if the given value is a function.
*
* @param {unknown} val
* Any value to check if it's a function.
*
* @return {boolean}
* true if the value is a function, false otherwise.
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function isFunction(val) {
return typeof val === 'function';
}
/**
* Returns the unique items in the array.
*
* @param {Array<T>}
* The array to remove dupes from.
*
* @return {Array<T>}
* The deduped array.
*/
function uniq(arr) {
return Array.from(new Set(arr));
}
// Regex to extract the module name.
const nameExpr = /(.*)\..*/i;
/**
* Internal method for globbing a single pattern.
*
* @param {String} globPattern
* The glob pattern.
*
* @param {String} opts.cwd
* Current working directory, used for resolving filepaths.
* Defaults to `process.cwd()`.
*
* @return {[{name, path, opts}]}
* The module names and paths.
*
* @api private
*/
function _listModules(globPattern, opts) {
opts = { cwd: process.cwd(), glob: glob.sync, ...opts };
let patternOpts = null;
if (Array.isArray(globPattern)) {
patternOpts = globPattern[1];
globPattern = globPattern[0];
}
// Replace Windows path separators with Posix path
globPattern = globPattern.replace(/\\/g, '/');
const result = opts.glob(globPattern, { cwd: opts.cwd });
const mapped = result.map((p) => ({
name: nameExpr.exec(path.basename(p))[1],
path: path.resolve(opts.cwd, p),
opts: patternOpts,
}));
return mapped;
}
/**
* Returns a list of {name, path} pairs,
* where the name is the module name, and path is the actual
* full path to the module.
*
* @param {String|Array<String>} globPatterns
* The glob pattern as a string or an array of strings.
*
* @param {String} opts.cwd
* Current working directory, used for resolving filepaths.
* Defaults to `process.cwd()`.
*
* @return {[{name, path}]}
* An array of objects with the module names and paths.
*/
function listModules(globPatterns, opts) {
if (Array.isArray(globPatterns)) {
return flatten(globPatterns.map((p) => _listModules(p, opts)));
}
return _listModules(globPatterns, opts);
}
/*
* Parses the parameter list of a function string, including ES6 class constructors.
*
* @param {string} source
* The source of a function to extract the parameter list from
*
* @return {Array<Parameter> | null}
* Returns an array of parameters, or `null` if no
* constructor was found for a class.
*/
function parseParameterList(source) {
const { next: _next, done } = createTokenizer(source);
const params = [];
let t = null;
nextToken();
while (!done()) {
switch (t.type) {
case 'class': {
const foundConstructor = advanceToConstructor();
// If we didn't find a constructor token, then we know that there
// are no dependencies in the defined class.
if (!foundConstructor) {
return null;
}
break;
}
case 'function': {
const next = nextToken();
if (next.type === 'ident' || next.type === '*') {
// This is the function name or a generator star. Skip it.
nextToken();
}
break;
}
case '(':
// Start parsing parameter names.
parseParams();
break;
case ')':
// We're now out of the parameter list.
return params;
// When we're encountering an identifier token
// at this level, it could be because it's an arrow function
// with a single parameter, e.g. `foo => ...`.
// This path won't be hit if we've already identified the `(` token.
case 'ident': {
// Likely a paren-less arrow function
// which can have no default args.
const param = { name: t.value, optional: false };
if (t.value === 'async') {
// Given it's the very first token, we can assume it's an async function,
// so skip the async keyword if the next token is not an equals sign, in which
// case it is a single-arg arrow func.
const next = nextToken();
if (next && next.type !== '=') {
break;
}
}
params.push(param);
return params;
}
/* istanbul ignore next */
default:
throw unexpected();
}
}
return params;
/**
* After having been placed within the parameter list of
* a function, parses the parameters.
*/
function parseParams() {
// Current token is a left-paren
let param = { name: '', optional: false };
while (!done()) {
nextToken();
switch (t.type) {
case 'ident':
param.name = t.value;
break;
case '=':
param.optional = true;
break;
case ',':
params.push(param);
param = { name: '', optional: false };
break;
case ')':
if (param.name) {
params.push(param);
}
return;
/* istanbul ignore next */
default:
throw unexpected();
}
}
}
/**
* Advances until we reach the constructor identifier followed by
* a `(` token.
*
* @returns `true` if a constructor was found, `false` otherwise.
*/
function advanceToConstructor() {
while (!done()) {
if (isConstructorToken()) {
// Consume the token
nextToken(1 /* TokenizerFlags.Dumb */);
// If the current token now isn't a `(`, then it wasn't the actual
// constructor.
if (t.type !== '(') {
continue;
}
return true;
}
nextToken(1 /* TokenizerFlags.Dumb */);
}
return false;
}
/**
* Determines if the current token represents a constructor, and the next token after it is a paren
* @return {boolean}
*/
function isConstructorToken() {
return t.type === 'ident' && t.value === 'constructor';
}
/**
* Advances the tokenizer and stores the previous token in history
*/
function nextToken(flags = 0 /* TokenizerFlags.None */) {
t = _next(flags);
return t;
}
/**
* Returns an error describing an unexpected token.
*/
/* istanbul ignore next */
function unexpected() {
return new SyntaxError(`Parsing parameter list, did not expect ${t.type} token${t.value ? ` (${t.value})` : ''}`);
}
}
// We parse the signature of any `Function`, so we want to allow `Function` types.
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
/**
* RESOLVER symbol can be used by modules loaded by
* `loadModules` to configure their lifetime, injection mode, etc.
*/
const RESOLVER = Symbol('Awilix Resolver Config');
/**
* Creates a simple value resolver where the given value will always be resolved. The value is
* marked as leak-safe since in strict mode, the value will only be resolved when it is not leaking
* upwards from a child scope to a parent singleton.
*
* @param {string} name The name to register the value as.
*
* @param {*} value The value to resolve.
*
* @return {object} The resolver.
*/
function asValue(value) {
return {
resolve: () => value,
isLeakSafe: true,
};
}
/**
* Creates a factory resolver, where the given factory function
* will be invoked with `new` when requested.
*
* @param {string} name
* The name to register the value as.
*
* @param {Function} fn
* The function to register.
*
* @param {object} opts
* Additional options for the resolver.
*
* @return {object}
* The resolver.
*/
function asFunction(fn, opts) {
if (!isFunction(fn)) {
throw new AwilixTypeError('asFunction', 'fn', 'function', fn);
}
const defaults = {
lifetime: Lifetime.TRANSIENT,
};
opts = makeOptions(defaults, opts, fn[RESOLVER]);
const resolve = generateResolve(fn);
const result = {
resolve,
...opts,
};
return createDisposableResolver(createBuildResolver(result));
}
/**
* Like a factory resolver, but for classes that require `new`.
*
* @param {string} name
* The name to register the value as.
*
* @param {Class} Type
* The function to register.
*
* @param {object} opts
* Additional options for the resolver.
*
* @return {object}
* The resolver.
*/
function asClass(Type, opts) {
if (!isFunction(Type)) {
throw new AwilixTypeError('asClass', 'Type', 'class', Type);
}
const defaults = {
lifetime: Lifetime.TRANSIENT,
};
opts = makeOptions(defaults, opts, Type[RESOLVER]);
// A function to handle object construction for us, as to make the generateResolve more reusable
const newClass = function newClass(...args) {
return Reflect.construct(Type, args);
};
const resolve = generateResolve(newClass, Type);
return createDisposableResolver(createBuildResolver({
...opts,
resolve,
}));
}
/**
* Resolves to the specified registration. Marked as leak-safe since the alias target is what should
* be checked for lifetime leaks.
*/
function aliasTo(name) {
return {
resolve(container) {
return container.resolve(name);
},
isLeakSafe: true,
};
}
/**
* Given an options object, creates a fluid interface
* to manage it.
*
* @param {*} obj
* The object to return.
*
* @return {object}
* The interface.
*/
function createBuildResolver(obj) {
function setLifetime(value) {
return createBuildResolver({
...this,
lifetime: value,
});
}
function setInjectionMode(value) {
return createBuildResolver({
...this,
injectionMode: value,
});
}
function inject(injector) {
return createBuildResolver({
...this,
injector,
});
}
return updateResolver(obj, {
setLifetime,
inject,
transient: partial(setLifetime, Lifetime.TRANSIENT),
scoped: partial(setLifetime, Lifetime.SCOPED),
singleton: partial(setLifetime, Lifetime.SINGLETON),
setInjectionMode,
proxy: partial(setInjectionMode, InjectionMode.PROXY),
classic: partial(setInjectionMode, InjectionMode.CLASSIC),
});
}
/**
* Given a resolver, returns an object with methods to manage the disposer
* function.
* @param obj
*/
function createDisposableResolver(obj) {
function disposer(dispose) {
return createDisposableResolver({
...this,
dispose,
});
}
return updateResolver(obj, {
disposer,
});
}
/**
* Partially apply arguments to the given function.
*/
function partial(fn, arg1) {
return function partiallyApplied() {
return fn.call(this, arg1);
};
}
/**
* Makes an options object based on defaults.
*
* @param {object} defaults
* Default options.
*
* @param {...} rest
* The input to check and possibly assign to the resulting object
*
* @return {object}
*/
function makeOptions(defaults, ...rest) {
return Object.assign({}, defaults, ...rest);
}
/**
* Creates a new resolver with props merged from both.
*
* @param source
* @param target
*/
function updateResolver(source, target) {
const result = {
...source,
...target,
};
return result;
}
/**
* Returns a wrapped `resolve` function that provides values
* from the injector and defers to `container.resolve`.
*
* @param {AwilixContainer} container
* @param {Object} locals
* @return {Function}
*/
function wrapWithLocals(container, locals) {
return function wrappedResolve(name, resolveOpts) {
if (name in locals) {
return locals[name];
}
return container.resolve(name, resolveOpts);
};
}
/**
* Returns a new Proxy that checks the result from `injector`
* for values before delegating to the actual container.
*
* @param {Object} cradle
* @param {Function} injector
* @return {Proxy}
*/
function createInjectorProxy(container, injector) {
const locals = injector(container);
const allKeys = uniq([
...Reflect.ownKeys(container.cradle),
...Reflect.ownKeys(locals),
]);
// TODO: Lots of duplication here from the container proxy.
// Need to refactor.
const proxy = new Proxy({}, {
/**
* Resolves the value by first checking the locals, then the container.
*/
get(target, name) {
if (name === Symbol.iterator) {
return function* iterateRegistrationsAndLocals() {
for (const prop in container.cradle) {
yield prop;
}
for (const prop in locals) {
yield prop;
}
};
}
if (name in locals) {
return locals[name];
}
return container.resolve(name);
},
/**
* Used for `Object.keys`.
*/
ownKeys() {
return allKeys;
},
/**
* Used for `Object.keys`.
*/
getOwnPropertyDescriptor(target, key) {
if (allKeys.indexOf(key) > -1) {
return {
enumerable: true,
configurable: true,
};
}
return undefined;
},
});
return proxy;
}
/**
* Returns a resolve function used to construct the dependency graph
*
* @this {Registration}
* The `this` context is a resolver.
*
* @param {Function} fn
* The function to construct
*
* @param {Function} dependencyParseTarget
* The function to parse for the dependencies of the construction target
*
* @param {boolean} isFunction
* Is the resolution target an actual function or a mask for a constructor?
*
* @return {Function}
* The function used for dependency resolution
*/
function generateResolve(fn, dependencyParseTarget) {
// If the function used for dependency parsing is falsy, use the supplied function
if (!dependencyParseTarget) {
dependencyParseTarget = fn;
}
// Parse out the dependencies
// NOTE: we do this regardless of whether PROXY is used or not,
// because if this fails, we want it to fail early (at startup) rather
// than at resolution time.
const dependencies = parseDependencies(dependencyParseTarget);
// Use a regular function instead of an arrow function to facilitate binding to the resolver.
return function resolve(container) {
// Because the container holds a global reolutionMode we need to determine it in the proper order of precedence:
// resolver -> container -> default value
const injectionMode = this.injectionMode ||
container.options.injectionMode ||
InjectionMode.PROXY;
if (injectionMode !== InjectionMode.CLASSIC) {
// If we have a custom injector, we need to wrap the cradle.
const cradle = this.injector
? createInjectorProxy(container, this.injector)
: container.cradle;
// Return the target injected with the cradle
return fn(cradle);
}
// We have dependencies so we need to resolve them manually
if (dependencies.length > 0) {
const resolve = this.injector
? wrapWithLocals(container, this.injector(container))
: container.resolve;
const children = dependencies.map((p) => resolve(p.name, { allowUnregistered: p.optional }));
return fn(...children);
}
return fn();
};
}
/**
* Parses the dependencies from the given function.
* If it's a class that extends another class, and it does
* not have a defined constructor, attempt to parse it's super constructor.
*/
function parseDependencies(fn) {
const result = parseParameterList(fn.toString());
if (!result) {
// No defined constructor for a class, check if there is a parent
// we can parse.
const parent = Object.getPrototypeOf(fn);
if (typeof parent === 'function' && parent !== Function.prototype) {
// Try to parse the parent
return parseDependencies(parent);
}
return [];
}
return result;
}
const nameFormatters = {
camelCase: (s) => camelCase(s),
};
/**
* Given an array of glob strings, will call `require`
* on them, and call their default exported function with the
* container as the first parameter.
*
* @param {AwilixContainer} dependencies.container
* The container to install loaded modules in.
*
* @param {Function} dependencies.listModules
* The listModules function to use for listing modules.
*
* @param {Function} dependencies.require
* The require function - it's a dependency because it makes testing easier.
*
* @param {String[]} globPatterns
* The array of globs to use when loading modules.
*
* @param {Object} opts
* Passed to `listModules`, e.g. `{ cwd: '...' }`.
*
* @param {(string, ModuleDescriptor) => string} opts.formatName
* Used to format the name the module is registered with in the container.
*
* @param {boolean} opts.esModules
* Set to `true` to use Node's native ECMAScriptModules modules
*
* @return {Object}
* Returns an object describing the result.
*/
function loadModules(dependencies, globPatterns, opts) {
opts ??= {};
const container = dependencies.container;
opts = optsWithDefaults(opts);
const modules = dependencies.listModules(globPatterns, opts);
if (opts.esModules) {
return loadEsModules(dependencies, container, modules, opts);
}
else {
const result = modules.map((m) => {
const loaded = dependencies.require(m.path);
return parseLoadedModule(loaded, m);
});
return registerModules(result, container, modules, opts);
}
}
/**
* Loads the modules using native ES6 modules and the async import()
* @param {AwilixContainer} container
* @param {ModuleDescriptor[]} modules
* @param {LoadModulesOptions} opts
*/
async function loadEsModules(dependencies, container, modules, opts) {
const importPromises = [];
for (const m of modules) {
const fileUrl = pathToFileURL(m.path).toString();
importPromises.push(dependencies.require(fileUrl));
}
const imports = await Promise.all(importPromises);
const result = [];
for (let i = 0; i < modules.length; i++) {
result.push(parseLoadedModule(imports[i], modules[i]));
}
return registerModules(result, container, modules, opts);
}
/**
* Parses the module which has been required
*
* @param {any} loaded
* @param {ModuleDescriptor} m
*/
function parseLoadedModule(loaded, m) {
const items = [];
// Meh, it happens.
if (!loaded) {
return items;
}
if (isFunction(loaded)) {
// for module.exports = ...
items.push({
name: m.name,
path: m.path,
value: loaded,
opts: m.opts,
});
return items;
}
if (loaded.default && isFunction(loaded.default)) {
// ES6 default export
items.push({
name: m.name,
path: m.path,
value: loaded.default,
opts: m.opts,
});
}
// loop through non-default exports, but require the RESOLVER property set for
// it to be a valid service module export.
for (const key of Object.keys(loaded)) {
if (key === 'default') {
// default case handled separately due to its different name (file name)
continue;
}
if (isFunction(loaded[key]) && RESOLVER in loaded[key]) {
items.push({
name: key,
path: m.path,
value: loaded[key],
opts: m.opts,
});
}
}
return items;
}
/**
* Registers the modules
*
* @param {ModuleDescriptorVal[][]} modulesToRegister
* @param {AwilixContainer} container
* @param {ModuleDescriptor[]} modules
* @param {LoadModulesOptions} opts
*/
function registerModules(modulesToRegister, container, modules, opts) {
modulesToRegister
.reduce((acc, cur) => acc.concat(cur), [])
.filter((x) => x)
.forEach(registerDescriptor.bind(null, container, opts));
return {
loadedModules: modules,
};
}
/**
* Returns a new options object with defaults applied.
*/
function optsWithDefaults(opts) {
return {
// Does a somewhat-deep merge on the registration options.
resolverOptions: {
lifetime: Lifetime.TRANSIENT,
...(opts && opts.resolverOptions),
},
...opts,
};
}
/**
* Given a module descriptor, reads it and registers it's value with the container.
*
* @param {AwilixContainer} container
* @param {LoadModulesOptions} opts
* @param {ModuleDescriptor} moduleDescriptor
*/
function registerDescriptor(container, opts, moduleDescriptor) {
const inlineConfig = moduleDescriptor.value[RESOLVER];
let name = inlineConfig && inlineConfig.name;
if (!name) {
name = moduleDescriptor.name;
let formatter = opts.formatName;
if (formatter) {
if (typeof formatter === 'string') {
formatter = nameFormatters[formatter];
}
if (formatter) {
name = formatter(name, moduleDescriptor);
}
}
}
let moduleDescriptorOpts = moduleDescriptor.opts;
if (typeof moduleDescriptorOpts === 'string') {
moduleDescriptorOpts = { lifetime: moduleDescriptorOpts };
}
const regOpts = {
...opts.resolverOptions,
...moduleDescriptorOpts,
...inlineConfig,
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const reg = regOpts.register
? regOpts.register
: isClass(moduleDescriptor.value)
? asClass
: asFunction;
container.register(name, reg(moduleDescriptor.value, regOpts));
}
/**
* Family tree symbol.
*/
const FAMILY_TREE = Symbol('familyTree');
/**
* Roll Up Registrations symbol.
*/
const ROLL_UP_REGISTRATIONS = Symbol('rollUpRegistrations');
/**
* The string representation when calling toString.
*/
const CRADLE_STRING_TAG = 'AwilixContainerCradle';
/**
* Creates an Awilix container instance.
*
* @param {Function} options.require The require function to use. Defaults to require.
*
* @param {string} options.injectionMode The mode used by the container to resolve dependencies.
* Defaults to 'Proxy'.
*
* @param {boolean} options.strict True if the container should run in strict mode with additional
* validation for resolver configuration correctness. Defaults to false.
*
* @return {AwilixContainer<T>} The container.
*/
function createContainer(options = {}) {
return createContainerInternal(options);
}
function createContainerInternal(options, parentContainer, parentResolutionStack) {
options = {
injectionMode: InjectionMode.PROXY,
strict: false,
...options,
};
/**
* Tracks the names and lifetimes of the modules being resolved. Used to detect circular
* dependencies and, in strict mode, lifetime leakage issues.
*/
const resolutionStack = parentResolutionStack ?? [];
// Internal registration store for this container.
const registrations = {};
/**
* The `Proxy` that is passed to functions so they can resolve their dependencies without
* knowing where they come from. I call it the "cradle" because
* it is where registered things come to life at resolution-time.
*/
const cradle = new Proxy({
[util.inspect.custom]: toStringRepresentationFn,
}, {
/**
* The `get` handler is invoked whenever a get-call for `container.cradle.*` is made.
*
* @param {object} _target
* The proxy target. Irrelevant.
*
* @param {string} name
* The property name.
*
* @return {*}
* Whatever the resolve call returns.
*/
get: (_target, name) => resolve(name),
/**
* Setting things on the cradle throws an error.
*
* @param {object} target
* @param {string} name
*/
set: (_target, name) => {
throw new Error(`Attempted setting property "${name}" on container cradle - this is not allowed.`);
},
/**
* Used for `Object.keys`.
*/
ownKeys() {
return Array.from(cradle);
},
/**
* Used for `Object.keys`.
*/
getOwnPropertyDescriptor(target, key) {
const regs = rollUpRegistrations();
if (Object.getOwnPropertyDescriptor(regs, key)) {
return {
enumerable: true,
configurable: true,
};
}
return undefined;
},
});
// The container being exposed.
const container = {
options,
cradle,
inspect,
cache: new Map(),
loadModules: loadModules$1,
createScope,
register: register,
build,
resolve,
hasRegistration,
dispose,
getRegistration,
[util.inspect.custom]: inspect,
[ROLL_UP_REGISTRATIONS]: rollUpRegistrations,
get registrations() {
return rollUpRegistrations();
},
};
// Track the family tree.
const familyTree = parentContainer
? [container].concat(parentContainer[FAMILY_TREE])
: [container];
container[FAMILY_TREE] = familyTree;
// We need a reference to the root container,
// so we can retrieve and store singletons.
const rootContainer = last(familyTree);
return container;
/**
* Used by util.inspect (which is used by console.log).
*/
function inspect() {
return `[AwilixContainer (${parentContainer ? 'scoped, ' : ''}registrations: ${Object.keys(container.registrations).length})]`;
}
/**
* Rolls up registrations from the family tree.
*
* This can get pretty expensive. Only used when
* iterating the cradle proxy, which is not something
* that should be done in day-to-day use, mostly for debugging.
*
* @param {boolean} bustCache
* Forces a recomputation.
*
* @return {object}
* The merged registrations object.
*/
function rollUpRegistrations() {
return {
...(parentContainer && parentContainer[ROLL_UP_REGISTRATIONS]()),
...registrations,
};
}
/**
* Used for providing an iterator to the cradle.
*/
function* cradleIterator() {
const registrations = rollUpRegistrations();
for (const registrationName in registrations) {
yield registrationName;
}
}
/**
* Creates a scoped container.
*
* @return {object}
* The scoped container.
*/
function createScope() {
return createContainerInternal(options, container, resolutionStack);
}
/**
* Adds a registration for a resolver.
*/
function register(arg1, arg2) {
const obj = nameValueToObject(arg1, arg2);
const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
for (const key of keys) {
const resolver = obj[key];
// If strict mode is enabled, check to ensure we are not registering a singleton on a non-root
// container.
if (options.strict && resolver.lifetime === Lifetime.SINGLETON) {
if (parentContainer) {
throw new AwilixRegistrationError(key, 'Cannot register a singleton on a scoped container.');
}
}
registrations[key] = resolver;
}
return container;
}
/**
* Returned to `util.inspect` and Symbol.toStringTag when attempting to resolve
* a custom inspector function on the cradle.
*/
function toStringRepresentationFn() {
return Object.prototype.toString.call(cradle);
}
/**
* Recursively gets a registration by name if it exists in the
* current container or any of its' parents.
*
* @param name {string | symbol} The registration name.
*/
function getRegistration(name) {
const resolver = registrations[name];
if (resolver) {
return resolver;
}
if (parentContainer) {
return parentContainer.getRegistration(name);
}
return null;
}
/**
* Resolves the registration with the given name.
*
* @param {string | symbol} name
* The name of the registration to resolve.
*
* @param {ResolveOptions} resolveOpts
* The resolve options.
*
* @return {any}
* Whatever was resolved.
*/
function resolve(name, resolveOpts) {
resolveOpts = resolveOpts || {};
try {
// Grab the registration by name.
const resolver = getRegistration(name);
if (resolutionStack.some(({ name: parentName }) => parentName === name)) {
throw new AwilixResolutionError(name, resolutionStack, 'Cyclic dependencies detected.');
}
// Used in JSON.stringify.
if (name === 'toJSON') {
return toStringRepresentationFn;
}
// Used in console.log.
if (name === 'constructor') {
return createContainer;
}
if (!resolver) {
// Checks for some edge cases.
switch (name) {
// The following checks ensure that console.log on the cradle does not
// throw an error (issue #7).
case util.inspect.custom:
case 'inspect':
case 'toString':
return toStringRepresentationFn;
case Symbol.toStringTag:
return CRADLE_STRING_TAG;
// Edge case: Promise unwrapping will look for a "then" property and attempt to call it.
// Return undefined so that we won't cause a resolution error. (issue #109)
case 'then':
return undefined;
// When using `Array.from` or spreading the cradle, this will
// return the registration names.
case Symbol.iterator:
return cradleIterator;
}
if (resolveOpts.allowUnregistered) {
return undefined;
}
throw new AwilixResolutionError(name, resolutionStack);
}
const lifetime = resolver.lifetime || Lifetime.TRANSIENT;
// if we are running in strict mode, this resolver is not explicitly marked leak-safe, and any
// of the parents have a shorter lifetime than the one requested, throw an error.
if (options.strict && !resolver.isLeakSafe) {
const maybeLongerLifetimeParentIndex = resolutionStack.findIndex(({ lifetime: parentLifetime }) => isLifetimeLonger(parentLifetime, lifetime));
if (maybeLongerLifetimeParentIndex > -1) {
throw new AwilixResolutionError(name, resolutionStack, `Dependency '${name.toString()}' has a shorter lifetime than its ancestor: '${resolutionStack[maybeLongerLifetimeParentIndex].name.toString()}'`);
}
}
// Pushes the currently-resolving module information onto the stack
resolutionStack.push({ name, lifetime });
// Do the thing
let cached;
let resolved;
switch (lifetime) {
case Lifetime.TRANSIENT:
// Transient lifetime means resolve every time.
resolved = resolver.resolve(container);
break;
case Lifetime.SINGLETON:
// Singleton lifetime means cache at all times, regardless of scope.
cached = rootContainer.cache.get(name);
if (!cached) {
// if we are running in strict mode, perform singleton resolution using the root
// container only.
resolved = resolver.resolve(options.strict ? rootContainer : container);
rootContainer.cache.set(name, { resolver, value: resolved });
}
else {
resolved = cached.value;
}
break;
case Lifetime.SCOPED:
// Scoped lifetime means that the container
// that resolves the registration also caches it.
// If this container cache does not have it,
// resolve and cache it rather than using the parent
// container's cache.
cached = container.cache.get(name);
if (cached !== undefined) {
// We found one!
resolved = cached.value;
break;
}
// If we still have not found one, we need to resolve and cache it.
resolved = resolver.resolve(container);
container.cache.set(name, { resolver, value: resolved });
break;
default:
throw new AwilixResolutionError(name, resolutionStack, `Unknown lifetime "${resolver.lifetime}"`);
}
// Pop it from the stack again, ready for the next resolution
resolutionStack.pop();
return resolved;
}
catch (err) {
// When we get an error we need to reset the stack. Mutate the existing array rather than
// updating the reference to ensure all parent containers' stacks are also updated.
resolutionStack.length = 0;
throw err;
}
}
/**
* Checks if the registration with the given name exists.
*
* @param {string | symbol} name
* The name of the registration t