app-container
Yet another IoC container for node applications with a few specific goals:
- support for asynchronous modules
- flexible declaration & dependency syntax
- support for initialization hooks
Installing
$ npm install --save app-container
Getting Started
Below is a super simple example. See /test/fixtures
for some more examples.
Define your code as modules that accept dependencies.
using an asynchronous factory function...
// in config.js const inject = { const config = await someAsyncModule return config}
using a plain old javascript object...
// in greet.jsconst inject = require: 'config' { console; };
using a constructor function...
// in hello.jsconst inject = type: 'constructor' require: 'greet' { thisgreeter = greet; } { thisgreeter; }
Create a new container.
// in container.js; // create a new containerconst container = namespace: 'inject' // this is the default namespace defaults: singleton: true // these are defaults to apply to all module declarations; // register modules matching a given pattern. the directory will be scanned recursively.container; ;
Load some dependencies...
// in index.js; { const hello = await container; hello;} if requiremain === module ;
Modules
Module's need to declare themselves to the container by exposing an object at a given namespace. If no namespace is defined when creating a container, the default namespace of inject
will be used. The simplest module declaration is shown below.
// in foo.jsconst inject = ; { return { /* ... */ } ;};
Or, in commonjs format:
// in foo.jsmodule { return { /* ... */ } ;}; moduleexportsinject = ;
If foo.js
resides at the root of one of our registered directories, then the container would register a new component named foo
with no dependencies and assume that foo
's default export (or module.exports) is a factory function. We could instead declare foo
as a constructor function like so:
// in foo.jsconst inject = type: 'constructor' ; { /* ... */ };
Or even a plain old javascript object.
// in foo.jsconst inject = ; { /* ... */ };
To declare a dependency, we can add a require
property to our declaration. In the following example, we add a dependency on another component, bar
. By the time our module's factory function is invoked, an instantiated bar instance will be passed in as an argument.
// in foo.jsconst inject = require: 'bar'; { return { /* ... */ } ;};
A component can have more than one dependency as well. By declaring require
as an array, each dependency will be created, initialized, and passed in as arguments to our factory function.
// in foo.jsconst inject = require: 'bar' 'baz' 'gar' 'gaz'; { return { /* ... */ } ;};
We can also declare our dependencies as an object. This allows us to rename them, and/or group them in various ways.
// in foo.jsconst inject = require: some: 'bar' other: 'baz' mods: gar: 'gar' gaz: 'gaz' ; { const gar gaz = mods; return { /* ... */ } ;};
Plugins
We can use special syntax to use plugins that support additional functionality.
The all!
plugin can be used to bulk load modules that match a pattern as an object.
// in foo.jsconst inject = require: 'all!^b' 'all!^g'; { return { /* ... */ } ;};
The any!
plugin is similar to all, except that it loads resolved modules as an array.
// in foo.jsconst inject = require: 'any!^b' 'any!^g'; { return { /* ... */ } ;};
The container!
plugin can be used to register/load dynamic components or change the behavior of the container at runtime.
// in repository.jsconst inject = require: 'container!' 'config' { if configbackend === 'in-memory' return container; return container;}
Asynchronous Components
The container supports asynchronous modules in two ways. 1) By returning a promise from a factory function:
// in foo.jsconst inject = name: 'foo' require: 'bar' 'baz'; { return Promise;}; // in bar.jsconst inject = name: 'bar' require: 'bar' 'baz'; { await return { /* ... */ } };
or, 2) by exposing an initialization method/function on the module instance that returns a promise.
// in foo.jsconst inject = name: 'foo' init: 'connect' require: 'bar' 'baz'; { return { /* ... */ } { return Promise; } ;};
Module Properties
A module declaration can declare any combination of the following properties.
Property | Type | Description |
---|---|---|
init | String | The name of a method/function to call to initialize the module instance after it's been created. |
name | String | A custom name to use to register with the container. If not provided, the relative path to the file (minus the extension) will be used instead when registering modules using glob |
require | Object String String[] | Module dependencies declarations |
singleton | Boolean | Whether or not the module should be treated as a singleton, meaning that if the module is required by two or more other modules, only one instance will ever be created, and all downstream modules will share the same instance. |
type | String | The default export (or module.exports) should either be a factory function, constructor function or something like a plain old javascript object or function that doesn't need instantiation/initialization. If no type is declared, the container will inspect it and assume that it is a factory function (if a function is exported), or an object, if something else is exported. You can override the default behavior by declaring a type property with a value of constructor or object . |
API
Container([options]) => container
Constructor function for creating container instances.
Parameters
Name | Type | Description |
---|---|---|
options | Object | |
options.defaults | Object | a map of default module options to apply to each module declaration |
options.namespace | String | override the default namespace inject |
Example
// instantiate a new container using the default namespace and setting the default// singleton flag to trueconst container = namespace: 'inject' defaults: singleton: true
glob(pattern, options)
The glob method allows for automagically registering multiple modules with the container instance using glob
to match files in a given directory.
Parameters
Name | Type | Description |
---|---|---|
pattern | String | a glob pattern for matching modules to register with the container. Only modules that match this pattern and declare a matching namespace will be registered. |
options | Object | an options object to pass to the underlying glob.sync call. |
Example
// recursively register all .js files in the same directory as this file// excluding index.js. see glob.js for more optionsconst container = container
load(...components) => Bluebird
Load one or more components
Parameters
Name | Type | Description |
---|---|---|
component | *Object | String* |
Example
// load a single moduleconst foo = await container; // load multiple modulesconst foo bar = await container;// orconst foo bar = await container; // the container returns a bluebird promise, so any bluebird function can be used.container; // use a component mapconst foo bar = await container; // use all or anyconst 'services/foo': foo 'services/bar': bar = await container; const services = await container;
register(mod, name, [options]) => Bluebird
Load one or more components
Parameters
Name | Type | Description |
---|---|---|
mod | *Function | Object* |
name | *Object | String* |
[options] | *Object | component options object (see module properties above) |
Example
; ;; const container = namespace: 'inject' defaults: singleton: true ; container;container; ;
Debugging
To enable debugging, you can use the DEBUG
environment variable.
$ export DEBUG=app-container*
Testing
run the test suite with code coverage
$ docker-compose run app-container
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes using conventional changelog standards (
git commit -am 'feat: adds my new feature'
) - Push to the branch (
git push origin my-new-feature
) - Ensure linting/security/tests are all passing
- Create new Pull Request
LICENSE
Copyright (c) 2017 Chris Ludden.
Licensed under the MIT License