472 lines
11 KiB
JavaScript
472 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
const CLASS_PATTERN = /^class[ {]/;
|
|
|
|
|
|
/**
|
|
* @param {function} fn
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
function isClass(fn) {
|
|
return CLASS_PATTERN.test(fn.toString());
|
|
}
|
|
|
|
/**
|
|
* @param {any} obj
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
function isArray(obj) {
|
|
return Array.isArray(obj);
|
|
}
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @param {string} prop
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
function hasOwnProp(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
/**
|
|
* @typedef {import('./index').InjectAnnotated } InjectAnnotated
|
|
*/
|
|
|
|
/**
|
|
* @template T
|
|
*
|
|
* @params {[...string[], T] | ...string[], T} args
|
|
*
|
|
* @return {T & InjectAnnotated}
|
|
*/
|
|
function annotate(...args) {
|
|
|
|
if (args.length === 1 && isArray(args[0])) {
|
|
args = args[0];
|
|
}
|
|
|
|
args = [ ...args ];
|
|
|
|
const fn = args.pop();
|
|
|
|
fn.$inject = args;
|
|
|
|
return fn;
|
|
}
|
|
|
|
|
|
// Current limitations:
|
|
// - can't put into "function arg" comments
|
|
// function /* (no parenthesis like this) */ (){}
|
|
// function abc( /* xx (no parenthesis like this) */ a, b) {}
|
|
//
|
|
// Just put the comment before function or inside:
|
|
// /* (((this is fine))) */ function(a, b) {}
|
|
// function abc(a) { /* (((this is fine))) */}
|
|
//
|
|
// - can't reliably auto-annotate constructor; we'll match the
|
|
// first constructor(...) pattern found which may be the one
|
|
// of a nested class, too.
|
|
|
|
const CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m;
|
|
const FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m;
|
|
const FN_ARG = /\/\*([^*]*)\*\//m;
|
|
|
|
/**
|
|
* @param {unknown} fn
|
|
*
|
|
* @return {string[]}
|
|
*/
|
|
function parseAnnotations(fn) {
|
|
|
|
if (typeof fn !== 'function') {
|
|
throw new Error(`Cannot annotate "${fn}". Expected a function!`);
|
|
}
|
|
|
|
const match = fn.toString().match(isClass(fn) ? CONSTRUCTOR_ARGS : FN_ARGS);
|
|
|
|
// may parse class without constructor
|
|
if (!match) {
|
|
return [];
|
|
}
|
|
|
|
const args = match[1] || match[2];
|
|
|
|
return args && args.split(',').map(arg => {
|
|
const argMatch = arg.match(FN_ARG);
|
|
return (argMatch && argMatch[1] || arg).trim();
|
|
}) || [];
|
|
}
|
|
|
|
/**
|
|
* @typedef { import('./index').ModuleDeclaration } ModuleDeclaration
|
|
* @typedef { import('./index').ModuleDefinition } ModuleDefinition
|
|
* @typedef { import('./index').InjectorContext } InjectorContext
|
|
*/
|
|
|
|
/**
|
|
* Create a new injector with the given modules.
|
|
*
|
|
* @param {ModuleDefinition[]} modules
|
|
* @param {InjectorContext} [parent]
|
|
*/
|
|
function Injector(modules, parent) {
|
|
parent = parent || {
|
|
get: function(name, strict) {
|
|
currentlyResolving.push(name);
|
|
|
|
if (strict === false) {
|
|
return null;
|
|
} else {
|
|
throw error(`No provider for "${ name }"!`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const currentlyResolving = [];
|
|
const providers = this._providers = Object.create(parent._providers || null);
|
|
const instances = this._instances = Object.create(null);
|
|
|
|
const self = instances.injector = this;
|
|
|
|
const error = function(msg) {
|
|
const stack = currentlyResolving.join(' -> ');
|
|
currentlyResolving.length = 0;
|
|
return new Error(stack ? `${ msg } (Resolving: ${ stack })` : msg);
|
|
};
|
|
|
|
/**
|
|
* Return a named service.
|
|
*
|
|
* @param {string} name
|
|
* @param {boolean} [strict=true] if false, resolve missing services to null
|
|
*
|
|
* @return {any}
|
|
*/
|
|
function get(name, strict) {
|
|
if (!providers[name] && name.indexOf('.') !== -1) {
|
|
const parts = name.split('.');
|
|
let pivot = get(parts.shift());
|
|
|
|
while (parts.length) {
|
|
pivot = pivot[parts.shift()];
|
|
}
|
|
|
|
return pivot;
|
|
}
|
|
|
|
if (hasOwnProp(instances, name)) {
|
|
return instances[name];
|
|
}
|
|
|
|
if (hasOwnProp(providers, name)) {
|
|
if (currentlyResolving.indexOf(name) !== -1) {
|
|
currentlyResolving.push(name);
|
|
throw error('Cannot resolve circular dependency!');
|
|
}
|
|
|
|
currentlyResolving.push(name);
|
|
instances[name] = providers[name][0](providers[name][1]);
|
|
currentlyResolving.pop();
|
|
|
|
return instances[name];
|
|
}
|
|
|
|
return parent.get(name, strict);
|
|
}
|
|
|
|
function fnDef(fn, locals) {
|
|
|
|
if (typeof locals === 'undefined') {
|
|
locals = {};
|
|
}
|
|
|
|
if (typeof fn !== 'function') {
|
|
if (isArray(fn)) {
|
|
fn = annotate(fn.slice());
|
|
} else {
|
|
throw error(`Cannot invoke "${ fn }". Expected a function!`);
|
|
}
|
|
}
|
|
|
|
const inject = fn.$inject || parseAnnotations(fn);
|
|
const dependencies = inject.map(dep => {
|
|
if (hasOwnProp(locals, dep)) {
|
|
return locals[dep];
|
|
} else {
|
|
return get(dep);
|
|
}
|
|
});
|
|
|
|
return {
|
|
fn: fn,
|
|
dependencies: dependencies
|
|
};
|
|
}
|
|
|
|
function instantiate(Type) {
|
|
const {
|
|
fn,
|
|
dependencies
|
|
} = fnDef(Type);
|
|
|
|
// instantiate var args constructor
|
|
const Constructor = Function.prototype.bind.apply(fn, [ null ].concat(dependencies));
|
|
|
|
return new Constructor();
|
|
}
|
|
|
|
function invoke(func, context, locals) {
|
|
const {
|
|
fn,
|
|
dependencies
|
|
} = fnDef(func, locals);
|
|
|
|
return fn.apply(context, dependencies);
|
|
}
|
|
|
|
/**
|
|
* @param {Injector} childInjector
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
function createPrivateInjectorFactory(childInjector) {
|
|
return annotate(key => childInjector.get(key));
|
|
}
|
|
|
|
/**
|
|
* @param {ModuleDefinition[]} modules
|
|
* @param {string[]} [forceNewInstances]
|
|
*
|
|
* @return {Injector}
|
|
*/
|
|
function createChild(modules, forceNewInstances) {
|
|
if (forceNewInstances && forceNewInstances.length) {
|
|
const fromParentModule = Object.create(null);
|
|
const matchedScopes = Object.create(null);
|
|
|
|
const privateInjectorsCache = [];
|
|
const privateChildInjectors = [];
|
|
const privateChildFactories = [];
|
|
|
|
let provider;
|
|
let cacheIdx;
|
|
let privateChildInjector;
|
|
let privateChildInjectorFactory;
|
|
|
|
for (let name in providers) {
|
|
provider = providers[name];
|
|
|
|
if (forceNewInstances.indexOf(name) !== -1) {
|
|
if (provider[2] === 'private') {
|
|
cacheIdx = privateInjectorsCache.indexOf(provider[3]);
|
|
if (cacheIdx === -1) {
|
|
privateChildInjector = provider[3].createChild([], forceNewInstances);
|
|
privateChildInjectorFactory = createPrivateInjectorFactory(privateChildInjector);
|
|
privateInjectorsCache.push(provider[3]);
|
|
privateChildInjectors.push(privateChildInjector);
|
|
privateChildFactories.push(privateChildInjectorFactory);
|
|
fromParentModule[name] = [ privateChildInjectorFactory, name, 'private', privateChildInjector ];
|
|
} else {
|
|
fromParentModule[name] = [ privateChildFactories[cacheIdx], name, 'private', privateChildInjectors[cacheIdx] ];
|
|
}
|
|
} else {
|
|
fromParentModule[name] = [ provider[2], provider[1] ];
|
|
}
|
|
matchedScopes[name] = true;
|
|
}
|
|
|
|
if ((provider[2] === 'factory' || provider[2] === 'type') && provider[1].$scope) {
|
|
/* jshint -W083 */
|
|
forceNewInstances.forEach(scope => {
|
|
if (provider[1].$scope.indexOf(scope) !== -1) {
|
|
fromParentModule[name] = [ provider[2], provider[1] ];
|
|
matchedScopes[scope] = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
forceNewInstances.forEach(scope => {
|
|
if (!matchedScopes[scope]) {
|
|
throw new Error('No provider for "' + scope + '". Cannot use provider from the parent!');
|
|
}
|
|
});
|
|
|
|
modules.unshift(fromParentModule);
|
|
}
|
|
|
|
return new Injector(modules, self);
|
|
}
|
|
|
|
const factoryMap = {
|
|
factory: invoke,
|
|
type: instantiate,
|
|
value: function(value) {
|
|
return value;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {ModuleDefinition} moduleDefinition
|
|
* @param {Injector} injector
|
|
*/
|
|
function createInitializer(moduleDefinition, injector) {
|
|
|
|
const initializers = moduleDefinition.__init__ || [];
|
|
|
|
return function() {
|
|
initializers.forEach(initializer => {
|
|
|
|
// eagerly resolve component (fn or string)
|
|
if (typeof initializer === 'string') {
|
|
injector.get(initializer);
|
|
} else {
|
|
injector.invoke(initializer);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {ModuleDefinition} moduleDefinition
|
|
*/
|
|
function loadModule(moduleDefinition) {
|
|
|
|
const moduleExports = moduleDefinition.__exports__;
|
|
|
|
// private module
|
|
if (moduleExports) {
|
|
const nestedModules = moduleDefinition.__modules__;
|
|
|
|
const clonedModule = Object.keys(moduleDefinition).reduce((clonedModule, key) => {
|
|
|
|
if (key !== '__exports__' && key !== '__modules__' && key !== '__init__' && key !== '__depends__') {
|
|
clonedModule[key] = moduleDefinition[key];
|
|
}
|
|
|
|
return clonedModule;
|
|
}, Object.create(null));
|
|
|
|
const childModules = (nestedModules || []).concat(clonedModule);
|
|
|
|
const privateInjector = createChild(childModules);
|
|
const getFromPrivateInjector = annotate(function(key) {
|
|
return privateInjector.get(key);
|
|
});
|
|
|
|
moduleExports.forEach(function(key) {
|
|
providers[key] = [ getFromPrivateInjector, key, 'private', privateInjector ];
|
|
});
|
|
|
|
// ensure child injector initializes
|
|
const initializers = (moduleDefinition.__init__ || []).slice();
|
|
|
|
initializers.unshift(function() {
|
|
privateInjector.init();
|
|
});
|
|
|
|
moduleDefinition = Object.assign({}, moduleDefinition, {
|
|
__init__: initializers
|
|
});
|
|
|
|
return createInitializer(moduleDefinition, privateInjector);
|
|
}
|
|
|
|
// normal module
|
|
Object.keys(moduleDefinition).forEach(function(key) {
|
|
|
|
if (key === '__init__' || key === '__depends__') {
|
|
return;
|
|
}
|
|
|
|
if (moduleDefinition[key][2] === 'private') {
|
|
providers[key] = moduleDefinition[key];
|
|
return;
|
|
}
|
|
|
|
const type = moduleDefinition[key][0];
|
|
const value = moduleDefinition[key][1];
|
|
|
|
providers[key] = [ factoryMap[type], arrayUnwrap(type, value), type ];
|
|
});
|
|
|
|
return createInitializer(moduleDefinition, self);
|
|
}
|
|
|
|
/**
|
|
* @param {ModuleDefinition[]} moduleDefinitions
|
|
* @param {ModuleDefinition} moduleDefinition
|
|
*
|
|
* @return {ModuleDefinition[]}
|
|
*/
|
|
function resolveDependencies(moduleDefinitions, moduleDefinition) {
|
|
|
|
if (moduleDefinitions.indexOf(moduleDefinition) !== -1) {
|
|
return moduleDefinitions;
|
|
}
|
|
|
|
moduleDefinitions = (moduleDefinition.__depends__ || []).reduce(resolveDependencies, moduleDefinitions);
|
|
|
|
if (moduleDefinitions.indexOf(moduleDefinition) !== -1) {
|
|
return moduleDefinitions;
|
|
}
|
|
|
|
return moduleDefinitions.concat(moduleDefinition);
|
|
}
|
|
|
|
/**
|
|
* @param {ModuleDefinition[]} moduleDefinitions
|
|
*
|
|
* @return { () => void } initializerFn
|
|
*/
|
|
function bootstrap(moduleDefinitions) {
|
|
|
|
const initializers = moduleDefinitions
|
|
.reduce(resolveDependencies, [])
|
|
.map(loadModule);
|
|
|
|
let initialized = false;
|
|
|
|
return function() {
|
|
|
|
if (initialized) {
|
|
return;
|
|
}
|
|
|
|
initialized = true;
|
|
|
|
initializers.forEach(initializer => initializer());
|
|
};
|
|
}
|
|
|
|
// public API
|
|
this.get = get;
|
|
this.invoke = invoke;
|
|
this.instantiate = instantiate;
|
|
this.createChild = createChild;
|
|
|
|
// setup
|
|
this.init = bootstrap(modules);
|
|
}
|
|
|
|
|
|
// helpers ///////////////
|
|
|
|
function arrayUnwrap(type, value) {
|
|
if (type !== 'value' && isArray(value)) {
|
|
value = annotate(value.slice());
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
exports.Injector = Injector;
|
|
exports.annotate = annotate;
|
|
exports.parseAnnotations = parseAnnotations;
|