sdic
Advanced tools
Comparing version 1.3.0 to 1.4.0
441
container.js
@@ -12,7 +12,7 @@ const fs = require('fs'); | ||
const throwError = (err) => { | ||
if (err instanceof Error) { | ||
throw err; | ||
} else { | ||
throw new Error(err); | ||
} | ||
if (err instanceof Error) { | ||
throw err; | ||
} else { | ||
throw new Error(err); | ||
} | ||
}; | ||
@@ -23,26 +23,26 @@ | ||
module.exports = (basepath) => { | ||
let basePathRegexp = new RegExp('^' + basepath); | ||
let extensions = Object.keys(require('module')._extensions); | ||
let basePathRegexp = new RegExp('^' + basepath); | ||
let extensions = Object.keys(require('module')._extensions); | ||
return (allowedExtensions = 'js|json|coffee') => { | ||
return (allowedExtensions = 'js|json|coffee') => { | ||
let factories = {}; | ||
let factories = {}; | ||
let loadDir = (basename, dir, opts = {}) => { | ||
delete opts.alias; // if alias is used for a folder, do not pass it to loadFile fn, it'd damage module name | ||
// load files from given path | ||
let files = (opts.recursive === false) ? fs.readdirSync(dir) : readDirR(dir); | ||
let loadDir = (basename, dir, opts = {}) => { | ||
delete opts.alias; // if alias is used for a folder, do not pass it to loadFile fn, it'd damage module name | ||
// load files from given path | ||
let files = (opts.recursive === false) ? fs.readdirSync(dir) : readDirR(dir); | ||
if (opts.filter) { | ||
files = files.filter(file => (new RegExp(opts.filter)).test(file)); | ||
} | ||
if (opts.filter) { | ||
files = files.filter(file => (new RegExp(opts.filter)).test(file)); | ||
} | ||
for (let i = 0; i < files.length; i++) { | ||
loadFile(_path.join(dir, files[i]), _path.join(basename, '/', files[i]), opts); | ||
} | ||
}; | ||
for (let i = 0; i < files.length; i++) { | ||
loadFile(_path.join(dir, files[i]), _path.join(basename, '/', files[i]), opts); | ||
} | ||
}; | ||
let createModuleName = (initialName, relPath, opts = {}) => { | ||
let createModuleName = (initialName, relPath, opts = {}) => { | ||
let moduleName = initialName; | ||
if ('alias' in opts && !(_isString(opts.alias) && /^[a-zA-Z_$]{1}[a-zA-Z0-9_$]+$/.test(opts.alias))) { | ||
if ('alias' in opts && !(_isString(opts.alias) && /^[a-zA-Z_$]{1}[a-zA-Z0-9_$]+$/.test(opts.alias))) { | ||
return throwError(`Invalid alias: ${opts.alias}`); | ||
@@ -85,23 +85,27 @@ } | ||
if (opts.es6 === true) { | ||
return moduleName; | ||
} | ||
return moduleName.charAt(0).toLowerCase() + moduleName.substr(1); | ||
}; | ||
}; | ||
let loadFile = (file, relPath = '', opts = {}) => { | ||
if (allowedExtensions && !(allowedExtensions.includes(file.match(/\w+$/)[0]))) return; | ||
if (Array.isArray(opts.ignore) && opts.ignore.length) { | ||
if (opts.ignore.find(pattern => (new RegExp(pattern)).test(file.replace(basePathRegexp, ''))) !== undefined) { | ||
return; | ||
} | ||
} | ||
let loadFile = (file, relPath = '', opts = {}) => { | ||
if (allowedExtensions && !(allowedExtensions.includes(file.match(/\w+$/)[0]))) return; | ||
if (Array.isArray(opts.ignore) && opts.ignore.length) { | ||
if (opts.ignore.find(pattern => (new RegExp(pattern)).test(file.replace(basePathRegexp, ''))) !== undefined) { | ||
return; | ||
} | ||
} | ||
let moduleFile = file.replace(/\.\w+$/, ''); | ||
let moduleName = createModuleName(_path.basename(moduleFile), relPath, opts); | ||
let moduleFile = file.replace(/\.\w+$/, ''); | ||
let moduleName = createModuleName(_path.basename(moduleFile), relPath, opts); | ||
// Register module | ||
let module = require(moduleFile); | ||
if (_isPlainObject(module)) { | ||
let content = fs.readFileSync(file); | ||
if (!content) { | ||
return throwError(`Cannot load file contents: ${moduleFile}`); | ||
} | ||
// Register module | ||
let module = require(moduleFile); | ||
if (_isPlainObject(module)) { | ||
let content = fs.readFileSync(file); | ||
if (!content) { | ||
return throwError(`Cannot load file contents: ${moduleFile}`); | ||
} | ||
@@ -114,3 +118,3 @@ content = content.toString(); | ||
container.register( | ||
key === 'default' ? moduleName : createModuleName(key, relPath, opts), | ||
key === 'default' ? moduleName : createModuleName(key, relPath, _extend(opts, {es6: true})), | ||
module[key], | ||
@@ -124,43 +128,43 @@ _extend(opts, {dependencies: resolveArguments(module[key])}) | ||
content = null; | ||
} else { | ||
content = null; | ||
} else { | ||
container.register(moduleName, module, opts); | ||
} | ||
}; | ||
} | ||
}; | ||
let resolveArguments = (fn) => { | ||
// match argument list | ||
return getParamNames(fn).filter(String).map((v) => v.trim()) | ||
}; | ||
let resolveArguments = (fn) => { | ||
// match argument list | ||
return getParamNames(fn).filter(String).map((v) => v.trim()) | ||
}; | ||
let resolveCacheFlag = (name, visited = []) => { | ||
let resolveCacheFlag = (name, visited = []) => { | ||
// check for circular dependencies | ||
if (visited.includes(name)) { | ||
visited.push(name); | ||
return throwError(`Circular dependency: ${visited.join(' > ')}`); | ||
} | ||
// check for circular dependencies | ||
if (visited.includes(name)) { | ||
visited.push(name); | ||
return throwError(`Circular dependency: ${visited.join(' > ')}`); | ||
} | ||
visited.push(name); | ||
visited.push(name); | ||
// if any of factory's dependencies has cache flag set to false - inherit it | ||
let factory = factories[name]; | ||
if (!factory) { | ||
return throwError(`Dependency does not exist: ${name} (${visited.join(' > ')})`); | ||
} | ||
// if any of factory's dependencies has cache flag set to false - inherit it | ||
let factory = factories[name]; | ||
if (!factory) { | ||
return throwError(`Dependency does not exist: ${name} (${visited.join(' > ')})`); | ||
} | ||
if (factory.opts.cache === false) return false; | ||
if (factory.opts.cache === false) return false; | ||
if (factory.dependencies.length > 0) { | ||
for (let i = 0; i < factory.dependencies.length; i++) { | ||
try { | ||
if (!resolveCacheFlag(factory.dependencies[i], JSON.parse(JSON.stringify(visited)))) return false; | ||
} catch (e) { | ||
return throwError(e); | ||
} | ||
} | ||
} | ||
if (factory.dependencies.length > 0) { | ||
for (let i = 0; i < factory.dependencies.length; i++) { | ||
try { | ||
if (!resolveCacheFlag(factory.dependencies[i], JSON.parse(JSON.stringify(visited)))) return false; | ||
} catch (e) { | ||
return throwError(e); | ||
} | ||
} | ||
} | ||
return true; | ||
}; | ||
return true; | ||
}; | ||
@@ -175,182 +179,187 @@ let makeClassInstance = (constructor, args) => { | ||
let getModuleInstance = (name, overrides = {}, visited = []) => { | ||
let getModuleInstance = (name, overrides = {}, visited = []) => { | ||
// check for circular dependencies | ||
if (visited.includes(name)) { | ||
visited.push(name); | ||
return throwError(`Circular dependency: ${visited.join(' > ')}`); | ||
} | ||
// check for circular dependencies | ||
if (visited.includes(name)) { | ||
visited.push(name); | ||
return throwError(`Circular dependency: ${visited.join(' > ')}`); | ||
} | ||
visited.push(name); | ||
visited.push(name); | ||
// try to retrieve factory | ||
let factory = factories[name]; | ||
if (!factory) { | ||
return throwError(`Module does not exist: ${name}`); | ||
} | ||
// try to retrieve factory | ||
let factory = factories[name]; | ||
if (!factory) { | ||
return throwError(`Module does not exist: ${name}`); | ||
} | ||
// resolve if an instance should be cached | ||
if (!('cache' in factory.opts) || factory.opts.cache === null) { | ||
factory.opts.cache = resolveCacheFlag(name); | ||
} | ||
// resolve if an instance should be cached | ||
if (!('cache' in factory.opts) || factory.opts.cache === null) { | ||
factory.opts.cache = resolveCacheFlag(name); | ||
} | ||
let storeInstance = _isEmpty(overrides) && factory.opts.cache; | ||
let storeInstance = _isEmpty(overrides) && factory.opts.cache; | ||
// instance already created - return | ||
if (factory.instance && storeInstance) { | ||
return factory.instance; | ||
} | ||
// instance already created - return | ||
if (factory.instance && storeInstance) { | ||
return factory.instance; | ||
} | ||
// resolve factory arguments | ||
let args = factory.dependencies.map((dependency) => { | ||
if (dependency in overrides) { | ||
return overrides[dependency]; | ||
} | ||
// resolve factory arguments | ||
let args = factory.dependencies.map((dependency) => { | ||
if (dependency in overrides) { | ||
return overrides[dependency]; | ||
} | ||
return getModuleInstance(dependency, overrides, JSON.parse(JSON.stringify(visited))); | ||
}); | ||
return getModuleInstance(dependency, overrides, JSON.parse(JSON.stringify(visited))); | ||
}); | ||
// create instance | ||
let instance; | ||
try { | ||
try { | ||
// try to create instance of functional module | ||
instance = factory.fn.apply(factory, args); | ||
} catch (err) { | ||
if (!/Cannot call a class as a function/.test(err.message)) { | ||
throw err; | ||
} | ||
let instance; | ||
// try to create instance of class module | ||
instance = makeClassInstance(factory.fn, args); | ||
} | ||
} catch (err) { | ||
err.message = `Cannot create an instance of: ${name}. Error: ${err.message}`; | ||
return throwError(err); | ||
} | ||
if (factory.opts.isConstructor) { | ||
instance = factory.fn; | ||
} else { | ||
try { | ||
try { | ||
// try to create instance of functional module | ||
instance = factory.fn.apply(factory, args); | ||
} catch (err) { | ||
if (!/Cannot call a class as a function/.test(err.message)) { | ||
throw err; | ||
} | ||
// store instance in cache | ||
if (storeInstance) { | ||
factory.instance = instance; | ||
// try to create instance of class module | ||
instance = makeClassInstance(factory.fn, args); | ||
} | ||
} catch (err) { | ||
err.message = `Cannot create an instance of: ${name}. Error: ${err.message}`; | ||
return throwError(err); | ||
} | ||
} | ||
return instance; | ||
}; | ||
// store instance in cache | ||
if (storeInstance) { | ||
factory.instance = instance; | ||
} | ||
// PUBLIC INTERFACE | ||
let container = { | ||
getAll: () => factories, | ||
return instance; | ||
}; | ||
get: (name, overrides = {}) => getModuleInstance(name, overrides), | ||
// PUBLIC INTERFACE | ||
let container = { | ||
getAll: () => factories, | ||
getByTag: (tag) => { | ||
let instances = {}; | ||
for (let key in factories) { | ||
if (factories[key].opts.tags.includes(tag)) { | ||
instances[key] = container.get(key); | ||
} | ||
} | ||
get: (name, overrides = {}) => getModuleInstance(name, overrides), | ||
return instances; | ||
}, | ||
getByTag: (tag) => { | ||
let instances = {}; | ||
for (let key in factories) { | ||
if (factories[key].opts.tags.includes(tag)) { | ||
instances[key] = container.get(key); | ||
} | ||
} | ||
register: (name, fn, opts = {}) => { | ||
if (fn === undefined) { | ||
return throwError('Unable to register empty (undefined) module'); | ||
} | ||
return instances; | ||
}, | ||
// throw exception if service already exists | ||
if (name in factories) { | ||
return throwError(`Module is already registered: ${name}`); | ||
} | ||
register: (name, fn, opts = {}) => { | ||
if (fn === undefined) { | ||
return throwError('Unable to register empty (undefined) module'); | ||
} | ||
opts = _extend({cache: null, tags: []}, opts); | ||
// remove folder opts | ||
['recursive', 'reverseName', 'ignore', 'filter'].map(opt => delete opts[opt]); | ||
// throw exception if service already exists | ||
if (name in factories) { | ||
return throwError(`Module is already registered: ${name}`); | ||
} | ||
// store service for later | ||
if (_isFunction(fn)) { | ||
factories[name] = { | ||
fn: fn, | ||
dependencies: opts.dependencies ? opts.dependencies : resolveArguments(fn), | ||
opts: opts | ||
}; | ||
} else { | ||
factories[name] = { | ||
fn: () => JSON.parse(JSON.stringify(fn)), | ||
dependencies: [], | ||
opts: opts | ||
}; | ||
} | ||
}, | ||
opts = _extend({cache: null, tags: []}, opts); | ||
// remove folder opts | ||
['recursive', 'reverseName', 'ignore', 'filter'].map(opt => delete opts[opt]); | ||
load: (path, opts = {}) => { | ||
// store service for later | ||
if (_isFunction(fn)) { | ||
factories[name] = { | ||
fn: fn, | ||
dependencies: opts.dependencies ? opts.dependencies : resolveArguments(fn), | ||
opts: opts | ||
}; | ||
} else { | ||
factories[name] = { | ||
fn: () => JSON.parse(JSON.stringify(fn)), | ||
dependencies: [], | ||
opts: opts | ||
}; | ||
} | ||
}, | ||
if ('prefix' in opts && !(_isString(opts.prefix) && /^[a-zA-Z_$]{1}[a-zA-Z0-9_$]+$/.test(opts.prefix))) { | ||
throwError(`Invalid prefix: ${opts.prefix}`); | ||
} | ||
load: (path, opts = {}) => { | ||
if ('postfix' in opts && !(_isString(opts.postfix) && /^[a-zA-Z0-9_$]+$/.test(opts.postfix))) { | ||
throwError(`Invalid postfix: ${opts.postfix}`) | ||
} | ||
if ('prefix' in opts && !(_isString(opts.prefix) && /^[a-zA-Z_$]{1}[a-zA-Z0-9_$]+$/.test(opts.prefix))) { | ||
throwError(`Invalid prefix: ${opts.prefix}`); | ||
} | ||
// resolve absolute file path | ||
let possibleFiles = [ | ||
path, | ||
_path.join(basepath, path) | ||
]; | ||
if ('postfix' in opts && !(_isString(opts.postfix) && /^[a-zA-Z0-9_$]+$/.test(opts.postfix))) { | ||
throwError(`Invalid postfix: ${opts.postfix}`) | ||
} | ||
extensions.forEach((ext) => { | ||
possibleFiles.push(path + ext); | ||
possibleFiles.push(_path.join(basepath, path + ext)); | ||
}); | ||
// resolve absolute file path | ||
let possibleFiles = [ | ||
path, | ||
_path.join(basepath, path) | ||
]; | ||
let realpath; | ||
for (let i = 0; i < possibleFiles.length; i++) { | ||
if (fs.existsSync(possibleFiles[i])) { | ||
realpath = fs.realpathSync(possibleFiles[i]); | ||
break; | ||
} | ||
} | ||
extensions.forEach((ext) => { | ||
possibleFiles.push(path + ext); | ||
possibleFiles.push(_path.join(basepath, path + ext)); | ||
}); | ||
if (typeof realpath === 'undefined') { | ||
return throwError(`Unable to load file: ${path}`); | ||
} | ||
let realpath; | ||
for (let i = 0; i < possibleFiles.length; i++) { | ||
if (fs.existsSync(possibleFiles[i])) { | ||
realpath = fs.realpathSync(possibleFiles[i]); | ||
break; | ||
} | ||
} | ||
// load particular file or directory | ||
if (fs.statSync(realpath).isDirectory()) { | ||
if ('alias' in opts) { | ||
if (!([null, false, undefined, ''].includes(opts.alias) || (_isString(opts.alias) && /^[a-zA-Z_$]{1}[a-zA-Z0-9_$\-]+$/.test(opts.alias)))) { | ||
return throwError(`Invalid alias: ${opts.alias}`); | ||
} | ||
} | ||
loadDir('alias' in opts ? (opts.alias || '') : _path.basename(realpath), realpath, opts); | ||
} else { | ||
loadFile(realpath, '', opts); | ||
} | ||
}, | ||
if (typeof realpath === 'undefined') { | ||
return throwError(`Unable to load file: ${path}`); | ||
} | ||
override: (name, fn, opts = {}) => { | ||
if (name in factories) { | ||
delete factories[name]; | ||
} | ||
// load particular file or directory | ||
if (fs.statSync(realpath).isDirectory()) { | ||
if ('alias' in opts) { | ||
if (!([null, false, undefined, ''].includes(opts.alias) || (_isString(opts.alias) && /^[a-zA-Z_$]{1}[a-zA-Z0-9_$\-]+$/.test(opts.alias)))) { | ||
return throwError(`Invalid alias: ${opts.alias}`); | ||
} | ||
} | ||
loadDir('alias' in opts ? (opts.alias || '') : _path.basename(realpath), realpath, opts); | ||
} else { | ||
loadFile(realpath, '', opts); | ||
} | ||
}, | ||
container.register(name, fn, opts); | ||
}, | ||
override: (name, fn, opts = {}) => { | ||
if (name in factories) { | ||
delete factories[name]; | ||
} | ||
unregister: (name) => { | ||
delete factories[name]; | ||
}, | ||
container.register(name, fn, opts); | ||
}, | ||
clear: () => { | ||
factories = {}; | ||
container.register('container', container); | ||
} | ||
}; | ||
unregister: (name) => { | ||
delete factories[name]; | ||
}, | ||
// register itself as a service | ||
container.register('container', container); | ||
clear: () => { | ||
factories = {}; | ||
container.register('container', container); | ||
} | ||
}; | ||
return container; | ||
} | ||
// register itself as a service | ||
container.register('container', container); | ||
return container; | ||
} | ||
}; |
{ | ||
"name": "sdic", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"description": "Simple dependency injection container", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -43,3 +43,4 @@ # SDIC: Simple Dependency Injection Container | ||
* deduplicate - remove multiple same string occurences (default: false) | ||
* uppercaseFirst - create a module name with uppercased first letter (default: false, module name starts with lowercased letter) | ||
* uppercaseFirst - create a module name with uppercased first letter (default: false - module name starts with lowercased letter) | ||
* isConstructor - loaded module is a constructor, load and return it as a constructor, do return an instances (default: false - modules as singletons by default) | ||
@@ -172,2 +173,21 @@ ```javascript | ||
**ES6 note:** container returns instances of modules by default (aka singletons). If you want to register a class (constructor function), you have to use option: `isConstructor: true` | ||
```javascript | ||
class FooBar {} | ||
container.register('FooBar', FooBar); | ||
container.get('FooBar'); // --> returns an instance of FooBar (default behaviour) | ||
``` | ||
```javascript | ||
class FooBar {} | ||
container.register('FooBar', FooBar, {isConstructor: true}); | ||
container.get('FooBar'); // --> returns class FooBar | ||
``` | ||
```javascript | ||
// all modules will be loaded as constructors | ||
container.load('/path/to/constructors_folder', {isConstructor: true}); | ||
``` | ||
### Manual module registration | ||
@@ -174,0 +194,0 @@ |
@@ -1,2 +0,2 @@ | ||
export const FirstFunctionalService = () => { | ||
export const firstFunctionalService = () => { | ||
return { | ||
@@ -7,3 +7,3 @@ method: () => ({passed: true}) | ||
export function SecondFunctionalService (fooService) { | ||
export function secondFunctionalService (fooService) { | ||
return { | ||
@@ -10,0 +10,0 @@ method: () => fooService.method() |
@@ -169,7 +169,7 @@ const expect = require('chai').expect; | ||
expect(container.getAll()).to.contain.all.keys(['firstFunctionalService', 'secondFunctionalService', 'classService', 'services']); | ||
expect(container.getAll()).to.contain.all.keys(['firstFunctionalService', 'secondFunctionalService', 'ClassService', 'services']); | ||
expect(Object.keys(container.getAll()).length).to.eql(6); // 5 + container itself | ||
expect(container.get('firstFunctionalService').method()).to.deep.equal({passed: true}); | ||
expect(container.get('secondFunctionalService').method()).to.deep.equal({passed: true}); | ||
expect(container.get('classService').method()).to.deep.equal({passed: true}); | ||
expect(container.get('ClassService').method()).to.deep.equal({passed: true}); | ||
expect(container.get('services').method()).to.deep.equal({passed: true}); // default export | ||
@@ -193,3 +193,20 @@ }); | ||
}); | ||
it('respects isConstructor flag', () => { | ||
container.register('fooService', () => ({method: () => ({passed: true})})); | ||
container.load('./named-exports/services', {isConstructor: true}); | ||
const ClassService = container.get('ClassService'); | ||
const instance = new ClassService(); | ||
expect(instance).to.be.an.instanceof(ClassService); | ||
class AnotherClass { | ||
} | ||
container.register('AnotherClass', AnotherClass, {isConstructor: true}); | ||
const AnotherClassPtr = container.get('AnotherClass'); | ||
const anotherInstance = new AnotherClassPtr(); | ||
expect(anotherInstance).to.be.an.instanceof(AnotherClass); | ||
}); | ||
}); | ||
}); |
50147
820
349