bolus
v0.5.1
Published
Simple dependency injection module for Node.js with inspiration from AngularJS.
Downloads
28
Readme
bolus
Simple dependency injection module for Node.js with inspiration from AngularJS.
Installation
$ npm install bolus
Examples
Examples can be found in a separate GitHub repository: bolus-examples.
Usage
The basic idea behind bolus is to register factory functions with an injector and then resolve them later. Each factory function takes its dependencies as arguments and then returns any value. Here is a basic example:
// index.js
// Import the module.
var Injector = require("bolus");
// Create a new injector.
var injector = new Injector();
// Register a module named 'a' that returns the value 5 when resolved.
injector.register("a", function () {
return 5;
});
// Now register a module named 'b' that depends on 'a'.
injector.register("b", function (a) {
return a + 7;
});
// Now resolve 'b'.
var b = injector.resolve("b");
console.log(b); // prints 12
A more typical use case is when the modules 'a' and 'b' are in separate files. Let's move the factory functions into their own files:
// a.js
module.exports = function () {
return 5;
};
// b.js
module.exports = function (a) {
return a + 7;
};
Now our main code can be written simply as:
// index.js
// Import the module.
var Injector = require("bolus");
// Create a new injector.
var injector = new Injector();
// Register all JS files as modules.
injector.registerPath("**/*.js");
// Now resolve 'b'.
var b = injector.resolve("b");
console.log(b); // prints 12
By default, the registerPath method uses the basename of the files (in this case 'a' and 'b') as the registered name. (This can be overridden by specifying a nameMakerCallback.) As you can see, bolus tries to be DRY by using the file name as the registered module name and the named factory arguments as the dependency names. If you wish, you can also specify these explicitly:
module.exports = function (foo, bar) {
return foo + bar;
};
module.exports.$name = "some name";
module.exports.$inject = ["some dependency", "some other dependency"];
External Dependencies
When using bolus, you can (and should) register any external dependencies (including core Node.js modules) with the injector as well. This ensures consistency and eases testing. You do this by passing an object to the registerRequires method.
var Injector = require("bolus");
var injector = new Injector();
injector.registerRequires({
fs: "fs",
Sequelize: "sequelize"
});
The keys are names that will be registered with the injector, the values are the names of the modules that will be passed to Node's require. These can then be injected into a module in the usual way.
module.exports = function (a, fs, Sequelize) {
...
};
Unit Testing
One of the best features of dependency injection is the simplicity of testing. Rather than load the entire application, you can load just the code of interest in isolation and test it with controlled inputs. Here's how a Jasmine test of the 'a' module could look:
// a.spec.js
var Injector = require("bolus");
describe("a", function () {
it("should return 5", function () {
var injector = new Injector();
injector.registerPath("app/a.js");
var a = injector.resolve("a");
expect(a).toBe(5);
});
});
The idea is that you create a new, clean injector for every test, inject your module of interest and any dependencies, and then run your test.
If you use Jasmine or Mocha, bolus will handle the lifecycle of the injector for you. Simply call the Injector.loadTestGlobals method to place add all of the injector methods on the global scope. Each of these methods apply only to the injector for the current test. So the test for 'a' would become:
// a.spec.js
require("bolus").loadTestGlobals();
describe("a", function () {
it("should return 5", function () {
registerPath("app/a.js");
var a = resolve("a");
expect(a).toBe(5);
});
});
If you have multiple tests with the same setup, you might prefer to use a Jasmine beforeEach:
// a.spec.js
require("bolus").loadTestGlobals();
describe("a", function () {
var a;
beforeEach(function () {
registerPath("app/a.js");
a = resolve("a");
});
it("should return 5", function () {
expect(a).toBe(5);
});
});
You can also easily provide mocks for dependencies to simplify your testing. Here's what a Jasmine test for 'b' could look like:
// b.spec.js
require("bolus").loadTestGlobals();
describe("b", function () {
beforeEach(registerPath("app/b.js"));
it("should return 10 when a is 3", function () {
registerValue("a", 3);
var b = resolve("b");
expect(b).toBe(10);
});
it("should return 1 when a is -6", function () {
registerValue("a", -6);
var b = resolve("b");
expect(b).toBe(1);
});
});
Here, we have provided two different values for the dependency 'a'. Also, note the alternative form of the beforeEach injector usage.
Advanced Usage
The above usage should be enough for most use cases. However, there are some more advanced features available if needed.
Container Inspection
You can check if a dependency has been resolved with a given name using the isRegistered method.
injector.registerValue("foo", {});
injector.isRegistered("foo"); // true
You can also get the names of all registered names with getRegisteredNames.
injector.registerValue("foo", {});
injector.registerValue("bar", {});
injector.getRegisteredNames(); // ['$injector', 'foo', 'bar']
Using Classes
If you are using v4 or higher of Node.js, you can also create class module. This works similar to the functions but the dependencies are passed to the class constructor.
class SomeClass {
constructor(a) {
this._a = a;
}
someMethod() {
console.log(this._a);
}
}
module.exports = SomeClass;
Advanced Resolving
In addition to resolving a single module with the resolve method, you can also resolve more that one at a time.
var modules = injector.resolve(["a", "b"]);
Here, modules is an array whose values are the resolved modules corresponding to the given names. Using ES6 destructuring, this can be written
const [a, b] = injector.resolve(["a", "b"]);
Additionally, you can pass a function to resolve that will be invoked with the arguments resolved:
var result = injector.resolve(function (a, b) {
return a - b;
});
Notice that the return value of the function is passed through. This function usage, along with underscore notation, is very helpful in unit tests:
describe("some test", function () {
var something;
// An underscore prefix and suffix is used to not hide the 'something' variable.
// The injector will ignore the underscores.
beforeEach(resolve(function (_something_) {
something = _something_;
});
it("should do something", function () {
// run tests on 'something'
});
});
You can resolve a function like this in a file in one call:
var result = injector.resolvePath("path/to/some/file.js");
When resolving with a function, you can also pass along a 'locals' object that includes variables to provide or override dependencies.
injector.resolvePath("path/to/some/file.js", {
someKey: "someValue"
});
(This is used in the Express example here to pass along a different router for each route file.)
Optional Depdencies
Bolus also supports optional dependencies. You can use this by adding an 'optional' comment in front of a variable argument or appending a question mark to a name:
var a = resolve("a?"); // a === undefined
var fn = function (a) {
// a === undefined
};
fn.$inject = ["a?"];
var fn = function (/* optional */ a) {
// a === undefined
};
Accessing the Injector Within a Module
The injector itself is registered in the injector as '$injector' to allow for some more advanced usages. (See here and here in the Express example for a couple scenarios.) It can be injected like any other module:
module.exports = function ($injector) {
...
};
Warning: currently there is no check for circular dependencies when using resolve methods within a factory initialization. Be careful!
Development
Running Tests
Tests are run automatically on Travis CI. They can (and should) be triggered locally with:
$ npm test
Code Linting
JSHint and JSCS are used to ensure code quality. To run these, run:
$ npm run jshint
$ npm run jscs
Generating Documentation
The API reference documentation below is generated by jsdoc-to-markdown. To generate an updated README.md, run:
$ npm run docs
API Reference
Injector
Kind: global class
- Injector
- new Injector()
- instance
- .register(name, fn)
- .registerValue(name, value)
- .registerPath(patterns, [nameMaker], [mod])
- .registerRequires(reqs, [mod])
- .resolve(names, [context]) ⇒ * | Array.<*>
- .resolve(fn, [locals], [context]) ⇒ *
- .resolvePath(p, [locals], [context]) ⇒ *
- .isRegistered(name) ⇒ boolean
- .getRegisteredNames() ⇒ Array.<string>
- static
- inner
- ~nameMakerCallback ⇒ string
new Injector()
Initializes a new Injector.
Example
var injector = new Injector();
injector.register(name, fn)
Register a module.
Kind: instance method of Injector
| Param | Type | Description | | --- | --- | --- | | name | string | Name of the module. | | fn | function | A module function to register. |
Example
injector.register("foo", function (dependencyA, dependencyB) {
// Do something with dependencyA and dependencyB to initialize foo.
// Return any object.
});
injector.registerValue(name, value)
Register a fixed value.
Kind: instance method of Injector
| Param | Type | Description | | --- | --- | --- | | name | string | The name of the module. | | value | * | The value to register. |
Example
// Register the value 5 with the name "foo".
injector.registerValue("foo", 5);
Example
// Register a function with the name "doubler".
injector.registerValue("doubler", function (arg) {
return arg * 2;
});
injector.registerPath(patterns, [nameMaker], [mod])
Register module(s) with the given path pattern(s).
Kind: instance method of Injector
| Param | Type | Description | | --- | --- | --- | | patterns | string | Array.<string> | The pattern or patterns to match. This uses the glob-all module, which accepts negative patterns as well. | | [nameMaker] | nameMakerCallback | A function that creates a name for a module registered by path. | | [mod] | Module | The module to run require on. Defaults to the Injector module, which should typically behave correctly. Setting this to the current module is useful if you are using tools like gulp-jasmine which clear the local require cache. |
Example
// Register a single file.
injector.registerPath("path/to/module.js");
Example
// Register all JS files except spec files.
injector.registerPath(["**/*.js", "!**/*.spec.js"]);
Example
injector.registerPath("path/to/module.js", function (defaultName, realpath, fn) {
return defaultName.toUpperCase();
});
injector.registerRequires(reqs, [mod])
Requires modules and registers them with the name provided.
Kind: instance method of Injector
| Param | Type | Description | | --- | --- | --- | | reqs | Object.<string, string> | Object with keys as injector names and values as module names to require. | | [mod] | Module | The module to run require on. Defaults to the Injector module, which should typically behave correctly. |
Example
injector.registerRequires({
fs: "fs",
Sequelize: "sequelize"
});
injector.resolve(names, [context]) ⇒ * | Array.<*>
Resolve a module or multiple modules.
Kind: instance method of Injector
Returns: * | Array.<*> - The resolved value(s).
| Param | Type | Description | | --- | --- | --- | | names | string | Array.<string> | Name or names to resolve. | | [context] | string | Optional context to give for error messages. |
Example
var log = injector.resolve("log");
Example
var resolved = injector.resolve(["fs", "log"]);
var fs = resolved[0];
var log = resolved[1];
injector.resolve(fn, [locals], [context]) ⇒ *
Resolve a module or multiple modules.
Kind: instance method of Injector
Returns: * - The result of the executed function.
| Param | Type | Description | | --- | --- | --- | | fn | function | Function to execute. | | [locals] | Object.<string, *> | Local variables to inject into the function. | | [context] | string | Optional context to give for error messages. |
Example
// Resolve someNum and otherNum and set the result to the sum.
var result;
injector.resolve(function (someNum, otherNum) {
result = someNum + otherNum;
});
Example
// This is essentially the same thing using a return in the function.
var result = injector.resolve(function (someNum, otherNum) {
return someNum + otherNum;
});
Example
// You can also provide or override dependencies using the locals argument.
var result = injector.resolve(function (someNum, otherNum) {
return someNum + otherNum;
}, { otherNum: 5 });
injector.resolvePath(p, [locals], [context]) ⇒ *
Resolve a module with the given path.
Kind: instance method of Injector
Returns: * - The result of the executed function.
| Param | Type | Description | | --- | --- | --- | | p | string | The path to resolve. | | [locals] | Object.<string, *> | Local variables to inject into the function. | | [context] | string | Optional context to give for error messages. If omitted, path will be used. |
Example
var log = injector.resolvePath("path/to/log.js");
injector.isRegistered(name) ⇒ boolean
Checks whether a given name has been registered in the injector.
Kind: instance method of Injector
Returns: boolean - A flag indicating whether the name is registered.
| Param | Description | | --- | --- | | name | The name to check. |
injector.getRegisteredNames() ⇒ Array.<string>
Get an array of all names registered in the injector.
Kind: instance method of Injector
Returns: Array.<string> - An array of all registered names.
Injector.loadTestGlobals([before], [after])
Load convenience methods on the global scope for testing. Will expose all of the standard injector methods on the global scope with the same name. Before each test an injector will be created and after each it will be thrown away. The global methods will execute on the injector in that scope.
Kind: static method of Injector
| Param | Type | Description | | --- | --- | --- | | [before] | function | Function to run before test case to create the injector. Defaults to global.beforeEach or global.setup to match Jasmine or Mocha. | | [after] | function | Function to run before test case to create the injector. Defaults to global.afterEach or global.teardown to match Jasmine or Mocha. |
Injector~nameMakerCallback ⇒ string
A function that creates a name for a module registered by path.
Kind: inner typedef of Injector
Returns: string - The name to use (or falsy to use default).
| Param | Type | Description | | --- | --- | --- | | defaultName | string | The default name to use. This is equal to the value of the function's $name property or the basename of the file. | | realpath | string | The full path of the loaded module. | | fn | function | The actual module factory function. |