diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d073ed..d737329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ + +# [0.2.0](https://github.com/BrainBacon/Nodep/compare/0.1.0...v0.2.0) (2015-11-11) + + +### Features + +* **nodep.js:** add dependency tree analysis ([fcba22a](https://github.com/BrainBacon/Nodep/commit/fcba22a)), closes [#1](https://github.com/BrainBacon/Nodep/issues/1) +* **nodep.js:** glob support for pattern matching ([762a22b](https://github.com/BrainBacon/Nodep/commit/762a22b)), closes [#2](https://github.com/BrainBacon/Nodep/issues/2) + + +### BREAKING CHANGES + +* $p.load has now moved to $p.init + + + # [0.1.0](https://github.com/BrainBacon/Nodep/compare/0.1.0...v0.1.0) (2015-09-23) diff --git a/README.md b/README.md index c98ed2d..cff1cb8 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ $ npm install --save nodep ```js var $p = require('nodep')(); -$p.load({ +$p.init({ myVar: localVariable -}).load([ +}).init([ 'anNpmPackage', './a/nested/local.dependency', './a.local.dependency' @@ -59,6 +59,24 @@ module.exports = function(localDependency, myVar, anNpmPackage) { - `./a.local.dependency` becomes `aLocalDependency` is executed and injectable +## Glob pattern matching for directories +### $p.init also supports glob syntax for loading multiple files from a directory or directory tree +- Any file without a `.js` extension will be ignored +- [glob docs and patterns](https://github.com/isaacs/node-glob) + +### Example +**index.js** +```js +var $p = require('nodep')(); + +$p.init('src/*').init([ + 'anNpmPackage', + './a/nested/local.dependency', + 'glob/patterns/*' +]); +``` + + ## Existing providers Register other instances of nodep into your project. @@ -155,13 +173,17 @@ var $p = require('nodep')(); * [.PATH_REPLACE_RESULT](#module_nodep..$p.PATH_REPLACE_RESULT) : String * [.REMOVE_COMMENTS_REGEXP](#module_nodep..$p.REMOVE_COMMENTS_REGEXP) : RegExp * [.REGISTER_TYPE_ERROR_MESSAGE](#module_nodep..$p.REGISTER_TYPE_ERROR_MESSAGE) : String + * [.CIRCULAR_DEPENDENCY_ERROR_MESSAGE](#module_nodep..$p.CIRCULAR_DEPENDENCY_ERROR_MESSAGE) : String * [.PROVIDER_TYPE_ERROR_MESSAGE](#module_nodep..$p.PROVIDER_TYPE_ERROR_MESSAGE) : String * [.camelCase(match, $1, offset)](#module_nodep..$p.camelCase) ⇒ String * [.name(path)](#module_nodep..$p.name) ⇒ String * [.args(fn)](#module_nodep..$p.args) + * [.applyArgs(name, args)](#module_nodep..$p.applyArgs) * [.decorator(name, dependency, [skipInject])](#module_nodep..$p.decorator) ⇒ Object - * [.register(path)](#module_nodep..$p.register) - * [.load(paths)](#module_nodep..$p.load) ⇒ Object + * [.easyRegister(path)](#module_nodep..$p.easyRegister) ⇒ Boolean + * [.register(paths)](#module_nodep..$p.register) + * [.resolveFiles(paths)](#module_nodep..$p.resolveFiles) ⇒ Array.<String> + * [.init(paths)](#module_nodep..$p.init) ⇒ Object * [.provider(instances)](#module_nodep..$p.provider) ⇒ Object * [.inject(name)](#module_nodep..$p.inject) ⇒ ? @@ -214,6 +236,11 @@ Expression to remove comments when parsing arguments for a dependency ### $p.REGISTER_TYPE_ERROR_MESSAGE : String Error message to send when trying to register a non-string type +**Kind**: static constant of [$p](#module_nodep..$p) + +### $p.CIRCULAR_DEPENDENCY_ERROR_MESSAGE : String +Error message to send when a circular reference is detected in the dependency tree + **Kind**: static constant of [$p](#module_nodep..$p) ### $p.PROVIDER_TYPE_ERROR_MESSAGE : String @@ -254,6 +281,17 @@ Will extract the order and name of injectable arguments in a given function | --- | --- | --- | | fn | function | the function to extract injection arguments from | + +### $p.applyArgs(name, args) +Function to apply args to a new dependency and register it + +**Kind**: static method of [$p](#module_nodep..$p) + +| Param | Type | Description | +| --- | --- | --- | +| name | String | the name of the new dependency to register | +| args | Array.<String> | the names of args to apply to the new dependeency | + ### $p.decorator(name, dependency, [skipInject]) ⇒ Object Main dependency injection function @@ -282,18 +320,40 @@ Dependency Handling: | dependency | ? | a value to assign to this dependency | | [skipInject] | Boolean | inject into a provided dependency of type function unless true | + +### $p.easyRegister(path) ⇒ Boolean +Easy dependency test, will register simple dependencies + +**Kind**: static method of [$p](#module_nodep..$p) +**Returns**: Boolean - true if register was successful + +| Param | Type | Description | +| --- | --- | --- | +| path | String | the name or filepath of a dependency to register to the provider | + -### $p.register(path) +### $p.register(paths) Default registration function in front of `$p.decorator` **Kind**: static method of [$p](#module_nodep..$p) | Param | Type | Description | | --- | --- | --- | -| path | String | the name or filepath of a dependency to register to the provider | +| paths | String | Array.<String> | the name or filepath of a dependency to register to the provider or an array of the former | + + +### $p.resolveFiles(paths) ⇒ Array.<String> +Function to normalize glob type paths into file paths omitting any non-js files + +**Kind**: static method of [$p](#module_nodep..$p) +**Returns**: Array.<String> - an array with globbed paths normalized and merged with regular paths + +| Param | Type | Description | +| --- | --- | --- | +| paths | Array.<String> | file paths and globbed paths | - -### $p.load(paths) ⇒ Object + +### $p.init(paths) ⇒ Object Load one or more dependencies into the provider Loading Mechanism: - All strings in an array loaded into $p will be initialized according to `$p.register` diff --git a/docs.hbs b/docs.hbs index 28c7512..1f225b9 100644 --- a/docs.hbs +++ b/docs.hbs @@ -1,5 +1,6 @@ {{#module name="header"~}}{{>body~}}{{/module}} -{{#module name="load"~}}{{>body~}}{{/module}} +{{#module name="init"~}}{{>body~}}{{/module}} +{{#module name="glob"~}}{{>body~}}{{/module}} {{#module name="provider"~}}{{>body~}}{{/module}} {{#module name="inject"~}}{{>body~}}{{/module}} {{#module name="decorator"~}}{{>body~}}{{/module}} diff --git a/nodep.js b/nodep.js index f44cc13..5516e07 100644 --- a/nodep.js +++ b/nodep.js @@ -27,8 +27,10 @@ var nodep = require('./package'); var _ = require('lodash'); +var glob = require('glob'); var REGISTER_TYPE_ERROR_MESSAGE = 'Dependency is not a string'; +var CIRCULAR_DEPENDENCY_ERROR_MESSAGE = 'Circular dependency detected'; var PROVIDER_TYPE_ERROR_MESSAGE = 'Module does not have dependencies'; /** @@ -169,6 +171,23 @@ module.exports = function() { return output; }, + /** + * Function to apply args to a new dependency and register it + * @function + * @param {String} name the name of the new dependency to register + * @param {Array} args the names of args to apply to the new dependeency + */ + applyArgs: function(name, dependency, args) { + this.dependencies[name] = dependency.apply(undefined, _.map(args, function(arg) { + var dep = this.dependencies[arg]; + if(_.isUndefined(dep)) { + dep = module.parent.require(arg); + this.dependencies[arg] = dep; + } + return dep; + }, this)); + }, + /** * ## Override existing dependencies * ```js @@ -208,16 +227,8 @@ module.exports = function() { return; } var args = this.args(dependency); - var self = this; // TODO determine what to make of the "this arg" value. Perhaps set it as $p? - this.dependencies[name] = dependency.apply(undefined, _.map(args, function(arg) { - var dep = self.dependencies[arg]; - if(_.isUndefined(dep)) { - dep = module.parent.require(arg); - self.dependencies[arg] = dep; - } - return dep; - })); + this.applyArgs(name, dependency, args); return this; }, @@ -229,24 +240,130 @@ module.exports = function() { REGISTER_TYPE_ERROR_MESSAGE: REGISTER_TYPE_ERROR_MESSAGE, /** - * Default registration function in front of `$p.decorator` + * Easy dependency test, will register simple dependencies * @function * @param {String} path the name or filepath of a dependency to register to the provider + * @returns {Boolean} true if register was successful */ - register: function(path) { + easyRegister: function(path, name) { if(!_.isString(path)) { throw new TypeError(REGISTER_TYPE_ERROR_MESSAGE); } - var name = this.name(path); + name = name || this.name(path); if(!_.isUndefined(this.dependencies[name])) { - return; + return true; } if(!_.includes(path, '/')) { this.dependencies[path] = module.parent.require(path); + return true; + } + return false; + }, + + /** + * Error message to send when a circular reference is detected in the dependency tree + * @constant + * @type {String} + */ + CIRCULAR_DEPENDENCY_ERROR_MESSAGE: CIRCULAR_DEPENDENCY_ERROR_MESSAGE, + + /** + * Default registration function in front of `$p.decorator` + * @function + * @param {String|Array} paths the name or filepath of a dependency to register to the provider or an array of the former + */ + register: function(paths) { + if(_.isArray(paths)) { + var index = {}; + _.forEach(paths, function(path) { + if(this.easyRegister(path)) { + return; + } + var name = this.name(path); + var dep = module.parent.require(path); + if(!_.isFunction(dep)) { + this.decorator(name, dep); + return; + } + var args = this.args(dep); + if(args.length < 1) { + this.register(path); + return; + } + index[name] = module.parent.require(path); + index[name].args = args; + }, this); + // TODO continue here + var checkDep = function(name, stack) { + if(!_.isUndefined(this.dependencies[name])) { + return; + } + if(_.includes(stack, name)) { + throw new Error(CIRCULAR_DEPENDENCY_ERROR_MESSAGE); + } + if(_.every(_.map(index[name].args, function(arg) { + return !!this.dependencies[arg]; + }, this))) { + this.applyArgs(name, index[name], index[name].args); + } + var childStack = _.cloneDeep(stack); + childStack.push(name); + _.forEach(index[name].args, function(arg) { + checkDep.apply(this, [arg, _.cloneDeep(childStack)]); + }, this); + checkDep.apply(this, [name, stack]); + }; + _.forEach(index, function(dep, name) { + checkDep.apply(this, [name, []]); + }, this); return; } - var dependency = module.parent.require(path); - this.decorator(name, dependency); + var name = this.name(paths); + if(this.easyRegister(paths, name)) { + return; + } + this.decorator(name, module.parent.require(paths)); + }, + + /** + * ## Glob pattern matching for directories + * ### $p.init also supports glob syntax for loading multiple files from a directory or directory tree + * - Any file without a `.js` extension will be ignored + * - [glob docs and patterns](https://github.com/isaacs/node-glob) + * + * ### Example + * **index.js** + * ```js + * var $p = require('nodep')(); + * + * $p.init('src/*').init([ + * 'anNpmPackage', + * './a/nested/local.dependency', + * 'glob/patterns/*' + * ]); + * ``` + * @module glob + */ + /** + * Function to normalize glob type paths into file paths omitting any non-js files + * @function + * @param {Array} paths file paths and globbed paths + * @returns {Array} an array with globbed paths normalized and merged with regular paths + */ + resolveFiles: function(paths) { + return _.flattenDeep(_.map(paths, function(path) { + if(_.isString(path) && glob.hasMagic(path)) { + return _.map(_.filter(glob.sync(path, { + nodir: true, + cwd: module.parent.paths[0].substring(0, module.parent.paths[0].lastIndexOf('/')) + }), function(file) { + return file.indexOf('.js') === file.length - 3; + }, this), function(file) { + return './' + file; + }, this); + } + return path; + }, this)); }, /** @@ -266,9 +383,9 @@ module.exports = function() { * ```js * var $p = require('nodep')(); * - * $p.load({ + * $p.init({ * myVar: localVariable - * }).load([ + * }).init([ * 'anNpmPackage', * './a/nested/local.dependency', * './a.local.dependency' @@ -288,7 +405,7 @@ module.exports = function() { * - `anNpmPackage` is loaded from `node_modules` * - `myVar` is injectable * - `./a.local.dependency` becomes `aLocalDependency` is executed and injectable - * @module load + * @module init */ /** * Load one or more dependencies into the provider @@ -300,18 +417,17 @@ module.exports = function() { * @param {(Array|Object|String)} paths a list, key/value store, or single dependency * @returns {Object} a reference to this provider */ - load: function(paths) { - var self = this; + init: function(paths) { if(_.isArray(paths)) { - _.forEach(paths, function(path) { - self.register(path); - }); + this.register(this.resolveFiles(paths)); } else if(_.isObject(paths)) { _.forEach(paths, function(path, name) { - if(_.isUndefined(self.dependencies[name])) { - self.dependencies[name] = path; + if(_.isUndefined(this.dependencies[name])) { + this.dependencies[name] = path; } - }); + }, this); + } else if(_.isString(paths) && glob.hasMagic(paths)) { + this.init([paths]); } else { this.register(paths); } @@ -354,7 +470,6 @@ module.exports = function() { * @returns {Object} a reference to this provider */ provider: function(instances) { - var self = this; var provide = function(instance) { if(_.isString(instance)) { instance = module.parent.require(instance); @@ -365,12 +480,12 @@ module.exports = function() { if(!instance || !instance.dependencies) { throw new TypeError(PROVIDER_TYPE_ERROR_MESSAGE); } - self.load(instance.dependencies); + this.init(instance.dependencies); }; if(_.isArray(instances)) { - _.forEach(instances, provide); + _.forEach(instances, provide, this); } else { - provide(instances); + provide.apply(this, [instances]); } return this; }, diff --git a/package.json b/package.json index 62cebef..f18e701 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodep", - "version": "0.1.0", + "version": "0.2.0", "description": "Node.js dependency injection", "main": "nodep.js", "repository": { @@ -23,6 +23,7 @@ "author": "Brian Jesse (http://brianjesse.com)", "license": "MIT", "dependencies": { + "glob": "^5.0.15", "lodash": "^3.10.1" }, "devDependencies": { diff --git a/test/mock/circular.js b/test/mock/circular.js new file mode 100644 index 0000000..8354d23 --- /dev/null +++ b/test/mock/circular.js @@ -0,0 +1,2 @@ +module.exports = function(circular) { +}; diff --git a/test/mock/provider.js b/test/mock/provider.js index 0bb9d70..25fec58 100644 --- a/test/mock/provider.js +++ b/test/mock/provider.js @@ -3,7 +3,7 @@ var $p = require('../../nodep')(); module.exports = function() { - $p.load({ + $p.init({ foo: true }); return $p; diff --git a/test/mock/provider.obj.js b/test/mock/provider.obj.js index 6a7e6b9..b09d307 100644 --- a/test/mock/provider.obj.js +++ b/test/mock/provider.obj.js @@ -2,7 +2,7 @@ var $p = require('../../nodep')(); -$p.load({ +$p.init({ foo: true }); diff --git a/test/spec.js b/test/spec.js index 038d525..5024c4e 100644 --- a/test/spec.js +++ b/test/spec.js @@ -1,6 +1,7 @@ 'use strict'; var assert = require('assert'); +var fs = require('fs'); var _ = require('lodash'); var nodep = require('../nodep'); @@ -136,6 +137,28 @@ describe('$p.decorator', function() { }); }); +describe('$p.easyRegister', function() { + beforeEach(reset); + + it('should reject a non-string type', function() { + assert.throws($p.easyRegister, TypeError, $p.REGISTER_TYPE_ERROR_MESSAGE); + }); + + it('should find a dependency', function() { + $p.dependencies.bar = true; + assert.strictEqual($p.easyRegister('./foo', 'bar'), true); + }); + + it('should register npm dependency', function() { + $p.easyRegister('lodash'); + assert.equal($p.dependencies.lodash, _); + }); + + it('should not register path', function() { + assert.strictEqual($p.easyRegister('./foo'), false); + }); +}); + describe('$p.register', function() { beforeEach(reset); @@ -202,18 +225,60 @@ describe('$p.register', function() { $p.register('./mock/args.commented'); assert.equal($p.dependencies.argsCommented, 69300); }); + + it('should inject dependency array', function() { + $p.register([ + './mock/no.args', + './mock/no.args.commented', + './mock/arg', + './mock/arg.commented', + './mock/args', + './mock/args.commented' + ]); + assert.equal($p.dependencies.argsCommented, 69300); + }); + + it('should inject dependencies in reverse', function() { + $p.register([ + './mock/args.commented', + './mock/args', + './mock/arg.commented', + './mock/arg', + './mock/no.args.commented', + './mock/no.args', + ]); + assert.equal($p.dependencies.argsCommented, 69300); + }); + + it('should detect circular dependency', function() { + assert.throws(function() { + $p.register(['./mock/circular']); + }, Error, $p.CIRCULAR_DEPENDENCY_ERROR_MESSAGE); + }); }); -describe('$p.load', function() { +describe('$p.resolveFiles', function() { + beforeEach(reset); + + it('should resolve file names from directory', function() { + var actual = $p.resolveFiles(['mock/*']); + var expected = _.map(fs.readdirSync('./test/mock'), function(path) { + return './mock/' + path; + }); + assert.strictEqual(_.difference(actual, expected).length, 0); + }); +}); + +describe('$p.init', function() { beforeEach(reset); it('should register dependency', function() { - $p.load('./mock/no.args'); + $p.init('./mock/no.args'); assert.equal($p.dependencies.noArgs, 1); }); it('should register dependency array', function() { - $p.load([ + $p.init([ './mock/no.args', './mock/no.args.commented' ]); @@ -222,43 +287,55 @@ describe('$p.load', function() { }); it('should register dependency object', function() { - $p.load({ + $p.init({ foo: 'bar' }); assert.equal($p.dependencies.foo, 'bar'); }); it('should not overwrite dependency from array', function() { - $p.load(['./mock/random']); + $p.init(['./mock/random']); var first = $p.dependencies.random; - $p.load(['./mock/random']); + $p.init(['./mock/random']); assert.equal($p.dependencies.random, first); }); it('should not overwrite dependency from object', function() { - $p.load({ + $p.init({ foo: 'bar' }); - $p.load({ + $p.init({ foo: 'baz' }); assert.equal($p.dependencies.foo, 'bar'); }); it('should return $p', function() { - assert.equal($p.load({ + assert.equal($p.init({ foo: 'bar' - }).load([ + }).init([ './mock/num' ]), $p); }); + + it('should call resolve files', function() { + var pattern = 'glob/*'; + var called = false; + $p.resolveFiles = function(paths) { + assert.deepStrictEqual(paths, [pattern]); + called = true; + }; + $p.register = function() {}; + $p.init(pattern); + assert.equal(called, true); + }); }); describe('$p.provider', function() { beforeEach(reset); it('should register other provider object', function() { - $p.provider(nodep().load({ + $p.provider(nodep().init({ foo: true })); assert.equal($p.dependencies.foo, true); @@ -266,7 +343,7 @@ describe('$p.provider', function() { it('should register other provider function', function() { $p.provider(function() { - return nodep().load({ + return nodep().init({ foo: true }); }); @@ -275,10 +352,10 @@ describe('$p.provider', function() { it('should register other provider array', function() { $p.provider([ - nodep().load({ + nodep().init({ foo: true }), - nodep().load({ + nodep().init({ bar: true }) ]);