app-container

1.1.2 • Public • Published

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
import someAsyncModule from 'some-async-module'
 
export const inject = { }
 
export default async function() {
  const config = await someAsyncModule.load()
  return config
}

using a plain old javascript object...

// in greet.js
export const inject = {
  require: ['config']
}
 
export default {
  greet(greeting, name) {
    console.log(`${greeting}${name}${this.config.punctuation}`);
  },
};

using a constructor function...

// in hello.js
export const inject = {
  type: 'constructor',
  require: ['greet']
}
 
export default class Foo {
  constructor(greet) {
    this.greeter = greet;
  }
 
  sayHello(name) {
    this.greeter.greet('Hello', name);
  }
}

Create a new container.

// in container.js
import Container from 'app-container';
 
// create a new container
const container = new 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.glob('**/*.js', { cwd: __dirname });
 
export default container;

Load some dependencies...

// in index.js
import container from './container';
 
export default async function main() {
  const hello = await container.load('hello');
  hello.sayHello('World');
}
 
if (require.main === module) {
  main();
}

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.js
export const inject = { };
 
export default function() {
  return {
    myMethod() { /* ... */ }
  };
};

Or, in commonjs format:

// in foo.js
module.exports = function() {
  return {
    myMethod() { /* ... */ }
  };
};
 
module.exports.inject = { };

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.js
export const inject = { type: 'constructor' };
 
export default class Foo {
  myMethod() { /* ... */ }
};

Or even a plain old javascript object.

// in foo.js
export const inject = { };
 
export default {
  myMethod() { /* ... */ }
};

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.js
export const inject = {
  require: 'bar'
};
 
export default function(bar) {
  return {
    myMethod() { /* ... */ }
  };
};

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.js
export const inject = {
  require: ['bar', 'baz', 'gar', 'gaz']
};
 
export default function(bar, baz, gar, gaz) {
  return {
    myMethod() { /* ... */ }
  };
};

We can also declare our dependencies as an object. This allows us to rename them, and/or group them in various ways.

// in foo.js
export const inject = {
  require: {
    some: 'bar',
    other: 'baz',
    mods: {
      gar: 'gar',
      gaz: 'gaz'
    }
  },
};
 
export default function({ some, other, mods }) {
  const { gar, gaz } = mods;
  return {
    myMethod() { /* ... */ }
  };
};

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.js
export const inject = {
  require: ['all!^b', 'all!^g']
};
 
export default function({ bar, baz }, { gar, gaz }) {
  return {
    myMethod() { /* ... */ }
  };
};

The any! plugin is similar to all, except that it loads resolved modules as an array.

// in foo.js
export const inject = {
  require: ['any!^b', 'any!^g']
};
 
export default function([bar, baz], [gar, gaz]) {
  return {
    myMethod() { /* ... */ }
  };
};

The container! plugin can be used to register/load dynamic components or change the behavior of the container at runtime.

// in repository.js
export const inject = {
  require: ['container!', 'config']
}
 
export default function(container, config) {
  if (config.backend === 'in-memory') {
    return container.load('inmem/repo');
  }
  return container.load('redis/repo');
}

Asynchronous Components

The container supports asynchronous modules in two ways. 1) By returning a promise from a factory function:

// in foo.js
export const inject = {
  name: 'foo',
  require: ['bar', 'baz']
};
 
export default function(bar, baz) {
  return Promise.resolve({
    myMethod() { /* ... */ }
  });
};
 
// in bar.js
export const inject = {
  name: 'bar',
  require: ['bar', 'baz']
};
 
export default async function(bar, baz) {
  await new Promise(resolve => setTimeout(resolve, 1000))
  return {
    myMethod() { /* ... */ }
  }
};

or, 2) by exposing an initialization method/function on the module instance that returns a promise.

// in foo.js
export const inject = {
  name: 'foo',
  init: 'connect',
  require: ['bar', 'baz']
};
 
export default function(bar, baz) {
  return {
    myMethod() { /* ... */ },
 
    connect() {
      return Promise.resolve();
    },
  };
};

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 true
const container = new 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 options
const container = new Container()
container.glob('**/*.js', { cwd: __dirname, ignore: ['index.js'] })

load(...components) => Bluebird

Load one or more components

Parameters
Name Type Description
component *Object String*
Example
// load a single module
const foo = await container.load('foo');
 
// load multiple modules
const [foo, bar] = await container.load('foo', 'bar');
// or
const [foo, bar] = await container.load(['foo', 'bar']);
 
// the container returns a bluebird promise, so any bluebird function can be used.
container.load('foo', 'bar')
.spread((foo, bar) => {
  // ..
});
 
// use a component map
const { foo, bar} = await container.load({ foo: 'services/foo', bar: 'services/bar' });
 
// use all or any
const { 'services/foo': foo, 'services/bar': bar } = await container.load('all!^services');
 
const services = await container.load('any!^services');

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
import Container from 'app-container';
 
import * as bar from './bar';
import * as foo from './foo';
 
const container = new Container({
  namespace: 'inject',
  defaults: { singleton: true },
});
 
container.register(bar, 'bar', bar.inject);
container.register(foo, foo.inject);
 
export default 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

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes using conventional changelog standards (git commit -am 'feat: adds my new feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Ensure linting/security/tests are all passing
  6. Create new Pull Request

LICENSE

Copyright (c) 2017 Chris Ludden.

Licensed under the MIT License

Package Sidebar

Install

npm i app-container

Weekly Downloads

111

Version

1.1.2

License

MIT

Unpacked Size

37.5 kB

Total Files

12

Last publish

Collaborators

  • cludden