di-ninja

The Dependency-Injection Framework for JavaScript NodeJS and Browser

Usage no npm install needed!

<script type="module">
  import diNinja from 'https://cdn.skypack.dev/di-ninja';
</script>

README

Di-Ninja Di-Ninja ICON

npm version JavaScript Style Guide Unix Build Windows Build Dependencies devDependencies Known Vulnerabilities NPM Status

The Dependency-Injection Framework for JavaScript NodeJS and Browser.

Installation

$ npm i di-ninja

Goals

  • Implement IoC by Composition-Root design pattern, allowing to keep all things decoupled and to wire application components and config at one unique root place.

  • Replace the singleton anti-pattern with dependency-injection by refacto export of instances to export of classes and factories.

  • Get a pure JavaScript non-dogmatic cross-transpiller Dependency-Injection framework.

  • Encourage adherence to the best practices of Object Oriented design ( SOLID, IoC, Dependency-Injection, Composition-Root, Strategy ...).

  • Improve code testability.

  • Extend the Art of JavaScript development.

Paradigm - Dependency Injection vs Modules

Why to not simply just use ES6 import or CommonJS require ?
Di-Ninja don't pretend to replace these features, that would be useless.
Using import/export for getting class or factory function is the good way to use it, that's why it was made for.
But using modules to export singleton is an anti-pattern and leading to global state. It predisposes application to more need for refacto in future, less testability (unable without hack).
How to know when to use dependency injection over import/require, it's very simple:
when it's about the implementation, always use dependency injection, even if it's a factory that is exported, else if it's not an instance but a stateless input/output function with a general purpose, usually coming from a third party library, you can use import. In case of doubt always use dependency injection !

Documentation

Summary

  1. Getting Started

  2. Dependencies declarations approaches

  3. Composition Root

  4. Decorator injection approach 1. abstract class 2. reference class

  5. Dependencies Resolution

  6. Recursive classes or factories

  7. Recursive params

  8. Types of params 1. interface 2. value 3. factory 5. valueFactory 6. classFactory 4. require

  9. Rules

  10. dependencies 1. params 2. calls 3. lazyCalls

  11. instantiation 1. classDef 2. instanceOf 3. substitutions

  12. single instance 1. shared 2. singleton 3. sharedInTree

  13. rule inheritance 1. inheritInstanceOf 2. inheritPrototype 3. inheritMixins 4. decorator

  14. asynchrone dependencies resolution 1. asyncResolve 2. asyncCallsSerie 3. asyncCallsParamsSerie

  15. dependency file location 1. autoload 2. path

  16. Container

  17. rules

  18. rulesDefault

  19. dependencies

  20. autoloadPathResolver

  21. autoloadExtensions

  22. autoloadFailOnMissingFile

  23. defaultVar

  24. defaultRuleVar

  25. defaultDecoratorVar

  26. defaultArgsVar

  27. defaultFactory

  28. defaultFunctionWrapper

  29. promiseFactory

  30. promiseInterfaces

  31. interfacePrototype

  32. interfaceTypeCheck

  33. globalKey

  34. ruleCache

  35. polyfillRequireContext

  36. Examples

1. Getting Started

import container from 'di-ninja'

const di = container()

di.addRules(rules)

di.get('MyClassName')

2. Dependencies declarations approaches

To define dependencies, you can use Composition-Root or Decorator injection approach for each components individually.
Differents approaches can be used for differents methods injections on same component.
Dependencies definition can be overrided, the last call of addRule or @di decorator will take precedence.

2.1 Composition-Root

The Composition Root is the highest level of your application, the top overlay.
It's here that you will configure many rules for your components and wire them together.
Using only the Composition Root design pattern has the advantage to let your components totaly unopinionated, all your classes and factories can keep uncoupled from the dependency injector (di-ninja).

example with ES6 class syntax

class A{
  constructor(b){
    this.b = b
  }
}
class B{
  constructor(){
    this.foo = bar
  }
}

di.addRules({
  'A': {
    classDef: A,
    params: [ 'B' ],
  },
  'B': {
    classDef: B,
  },
}
})

di.get('A')

example with function as constructor or factory


//function as a constructor
function A(b){
  this.b = b
}

//function as a factory
function B(){
  const object = { foo: 'bar' }
  
  // if we return an object or other value than undefined,
  // this function will be treated by javascript as a factory
  
  return object
}

di.addRules({
  'A': {
    classDef: A,
    params: [ 'B' ],
  },
  'B': {
    classDef: B,
  },
})

di.get('A')

2.2 Decorator injection approach

The Decorator injection approach let your components define their own dependencies.
These dependencies declarations can rely on container level defined abstractions (recommanded), or on direct class or factory definition.
It can be used in addition to the Composition-Root and replace the rule's key params and also the parameters of call argument for rule's key calls and lazyCalls.

2.2.1 abstract class

example with ES6 class syntax

di.addRule('B',{
  classDef: B,
})

@di('A',[ 'B' ])
class A{
  constructor(b){
    this.b = b
  }
}

di.get('A')

example with function as constructor or factory

di.addRule('B',{
  classDef: B,
})

function A(b){
  this.b = b
}
di( 'A', ['B'] )( A )

di.get('A')
2.2.2 reference class
@di('A',[ B ])
class A{
  constructor(b){
    this.b = b
  }
}

di.get('A')

3. Dependencies Resolution

The dependencies are resolved according to rule's key params and parameters of call argument for rule's key calls and lazyCalls.

3.1 Recursive classes or factories

You can use factories or classes, and obviously, all dependencies are resolved recursively.

class A{
  constructor(b){
    this.b = b
  }
}
class B{
  constructor(c){
    this.c = c
  }
}
function C(){
  return 'Hello world !'
}
di.addRules({
  A: {
    classDef: A,
    params: [ 'B' ],
  },
  B: {
    classDef: B,
    params: [ 'C' ],
  },
  C: {
    classDef: C,
  },
})

const a = di.get('A') //will resolve C and pass it's return to new B, then it will pass the new B to new A

//it will be equivalent to
const a = new A( new B( C() ) )

3.2 Recursive params

You can nest dependencies declarations to infinite. It's very common use for config.
(for others params behaviors see params)

class A{
  constructor(config, aSecondInstanceOfB){
    this.b = config.wathever.anotherKey.b
    this.b2 = aSecondInstanceOfB
  }
}
class B{}
di.addRules({
  A: {
    classDef: A,
    params: [ {
      wathever: {
        anotherKey: {
          b: 'B'
        },
      },
    }, 'B' ],
  },
  B: {
    classDef: B,
  },
})

const a = di.get('A')

//it will be equivalent to
const a = new A( {
  wathever: {
    anotherKey: {
      b: new B(),
    },
  },
}, new B() )

3.3. Types of params

You can wrap each value of param with a di-ninja class that will tell container how to resolve the dependency.
By default all values and subvalues of params are traversed when it's an Object or Array, are wrapped with classFactory when it's a function, and else by interface.
All these behaviors can be configured, but the default config is well and the documentation rely on it. (see defaultVar, defaultRuleVar, defaultDecoratorVar, defaultArgsVar,
defaultFactory, defaultFunctionWrapper)

(for others params behaviors see params)

3.3.1 interface

Container will resolve dependency as, an instance of class or a value from factory, defined by corresponding rule's key.
This is the default wrapper for string.

class A{
  constructor(b){
    this.b = b
  }
}
class B{}

di.addRule('A', { classDef: A })
di.addRule('B', { classDef: B })

di.addRule('A', { params: [ di.interface('B') ] })

//with default config, previous rule will be equivalent to next one
di.addRule('A', { params: [ 'B' ] })


const a = di.get('A')

//will be equivalent to
const a = new A( new B() )
3.3.2 value

Container will resolve dependency with the specified value. The value type can be anything: scalar, object, array, function...

class A{
  constructor(bar){
    this.foo = bar
  }
}

di.addRule('A', {
  classDef: A,
  params: [ di.value('bar') ],
})

const a = di.get('A')

//will be equivalent to
const a = new A( 'bar' )
3.3.3 factory

The behavior of this method can be configured with container config's key defaultFactory.
By default it's an alias for valueFactory.

3.3.4 valueFactory

Container will resolve dependency with the value returned by the given function.

class A{
  constructor(bar){
    this.foo = bar
  }
}

function getFoo(){
  return 'bar'
}

di.addRule('A', {
  classDef: A,
  params: [ di.factory( getFoo ) ],
})

const a = di.get('A')

//will be equivalent to
const a = new A( getFoo() )
3.3.5 classFactory

Container will resolve dependency with an instance of the referenced class (or the returned value of a factory).
This is the default wrapper for classes references.

class A{
  constructor(b){
    this.b = b
  }
}
class B{}

di.addRule('A', { classDef: A })

di.addRule('A', { params: [ di.classFactory(B) ] })

//with default config, previous rule will be equivalent to next one
di.addRule('A', { params: [ B ] })


const a = di.get('A')

//will be equivalent to
const a = new A( new B() )
3.3.4 require

Container will resolve dependency with an instance (or value returned by the function) of the class (or factory) (CJS export or ES6 export default) exported by specified file.
You can use rules to configure it.
The behavior of this method differ according to environment:
in all environment it will rely on preloaded require.context (see dependencies) wich is the only way to include dependency in webpack (because of static require resolution), for node, if the dependency it's not registred, it will require the specified file and register it.

di.addRules({
  'A': {
    classDef: A,
    params: [ di.require('path/to/my-file') ],
  },
  'path/to/my-file': {
    /* ... */
  },
)

const a = di.get('A')

4. Rules

The rules define resolutions behaviors of the classes or factories and their dependencies.

const rules = {}

//1st way to define rules
const di = container()
di.addRules(rules)

//2nd way to define rules
const di = container({
  rules,
})

4.1. dependencies

The following rule's keys are about classes or factories dependencies.

//you can use class
class A{
  constructor(b, c, d){
    this.b = b
    this.c = c
    this.d = d
  }
}

//or instance factory
function A(b, c, d){
  this.b = b
  this.c = c
  this.d = d
}

//or factory
function A(b, c, d){
  const anotherValue = {
    b: b,
    c: c,
    d: d,
  }
  return anotherValue
}
4.1.1 params

type: array
containing nested dependencies

The rule's key params define what will be injected to class constructor or factory. The keys can be nested (see Recursive params). The resolutions behavior depends of Types of params.

class A{
  constructor(b, c, d){
    this.b = b
    this.c = c
    this.d = d
  }
}

di.addRule('A', { params: ['B','C','D'] })

You can override params defined in rule on manual call:

di.get('A', ['E','F','G'])

4.1.2 calls

type: array
stack of call array with 1st item for method name or callback and 2nd item an array of params for methods (working same as params).

Stack of methods to call after instance creation.
If some circular dependencies are detected, some items of calls stack will be placed automatically in lazyCalls.

class A{
  method1(dep1){
    this.dep1 = dep1
  }
  method2(dep2){
    this.dep2 = dep2
  }
  method3(dep3){
    this.dep3 = dep3
  }
}
di.addRule('A', {
  classDef: A,
  calls: [
    
    [ 'method1', [ 'dep1' ] ],
    [ 'method2', [ 'dep2' ] ],
    
    [
      function(a, dep3){
        a.method3(dep3)
      },
      [ 'dep3' ]
    ],
    
  ],
})

4.1.3 lazyCalls

type: array

Same as calls, but run after dependency has been distributed to needing instances, this helper offer a simple way to solving circular dependency problem.

4.2. instantiation

The following rule's keys are about instantiations of classes and factories.

4.2.1 classDef

type: function class or factory

The classDef key reference the class that will be used for instantiation.
It's used for use reference to class direcly in rule, you can do without if you configure container dependencies with require.context.

class A{}

di.addRule('A',{ classDef: A ])

assert( di.get('A') instanceof A )
4.2.2 instanceOf

type: string interface name

Refers to the name of another rule containing classDef or instanceOf, this is resolved recursively.

di.addRule('A',{ classDef: A })
di.addRule('B',{ instanceOf: 'A' })

assert( di.get('B') instanceof A )
4.2.3 substitutions

type: object | array

Substitutions, as indicated by it's name, substitutes a dependency defined by params, calls (and lazyCalls). If an object is provided, the substitutions operate by associative key, else, if an array is provided, the substitution will be done only for params and will be index based.
By associative key, all dependencies of the rule's named as the key will be replaced by specified other rule's name.

index based

@di('A', [ 'B' ])
class A{
  constructor(B){
    this.B = B
  }
}

di.addRule('A',{
  substitutions: [ 'C' ],
})

assert( di.get('A').B instanceof C )

associative key

@di('A', [ { config: { subkey: 'B' } } ])
class A{
  constructor(config){
    this.B = config.subkey.B
  }
}
di.addRule('A',{
  substitutions: { 'B': 'C' },
})

assert( di.get('A').B instanceof C )

associative key for calls

class A{
  setDep(config){
    this.dep = config.dep
  }
}
di.addRule('A',{
  calls: [
    [ 'setDep', [ { dep: 'B' } ] ],
  ],
  substitutions: { 'B': 'C' },
})

assert( di.get('A').dep instanceof C )

4.3. single instance

The following rule's keys are about sharing single instances.

4.3.1 shared

type: boolean (default false)

When shared is set to true, the instance of the classe or the factory return defined by the rule will be shared for the whole application.

class A{
  constructor(b){
    this.b = b
  }
}
class B{}

di.addRules({
  'A': {
    params: [ 'B' ],
  },
  'B': {
    shared: true,
  },
})

const a1 = di.get('A')
const a2 = di.get('A')
assert( a1 !== a2 )

assert( a1.b === a2.b )

const b1 = di.get('B')
const b2 = di.get('B')
assert( b1 === b2 )

4.3.2 singleton

type: object | array | scalar

If specified it will be registred as shared instance of the dependency for the whole application.

class A{
  constructor(b){
    this.b = b
  }
}
class B{}

const b = new B()

di.addRules({
  'A': {
    params: [ 'B' ],
  },
  'B': {
    singleton: b,
  },
})

const a1 = di.get('A')
const a2 = di.get('A')
assert( a1 !== a2 )

assert( a1.b === a2.b === b )

const b1 = di.get('B')
const b2 = di.get('B')
assert( b1 === b2 === b )

4.3.3 sharedInTree

In some cases, you may want to share a a single instance of a class between every class in one tree but if another instance of the top level class is created, have a second instance of the tree.

For instance, imagine a MVC triad where the model needs to be shared between the controller and view, but if another instance of the controller and view are created, they need a new instance of their model shared between them.

The best way to explain this is a practical demonstration:

class A {    
  constructor(b, c){
    this.b = b
    this.c = c
  }
}

class B {
  constructor(d){
    this.d = d
  }
}

class C {
  constructor(d){
    this.d = d
  }
}

class D {}

di.addRule('A', {
  'sharedInTree': ['D'],
})

const a = di.get('A')

// Anywhere that asks for an instance D within the tree that existis within A will be given the same instance:
// Both the B and C objects within the tree will share an instance of D
assert( a.b.d === a.c.d )

// However, create another instance of A and everything in this tree will get its own instance of D:
const a2 = di.get('A')
assert( a2.b.d === a2.c.d )

assert( a.b.d !== a2.b.d )
assert( a.c.d !== a2.c.d )

By using sharedInTree it's possible to mark D as shared within each instance of an object tree. The important distinction between this and global shared objects is that this object is only shared within a single instance of the object tree.

4.4. rule inheritance

The following rule's keys are about rule inheritance.
There are three way to herit rule, the priority order of override is inheritInstanceOf, overrided by inheritPrototype, overrided by inheritMixins, and finally the rule itself wich is composed by rule definition and decorator. Priority between rule and decorator depends of calls order, the last take precedence, traditionally it's the rule.
Most of rules options are replaced except some options that will be merged: sharedInTree, substitutions, calls and lazyCalls.

See also rulesDefault in container config section.

4.4.1 inheritInstanceOf

type: boolean (default true)
Enable inheritance of rules from instanceOf parents classes.

class X{
  constructor(x){
    this.x = x
  }
}
    
di.addRules({
  'X':{
    classDef: X,
    params: [ di.value('ok') ],
    shared: true,
  },
  'Y':{
    instanceOf: 'X',
    inheritInstanceOf: true,
  },
})

assert( di.get('Y').x === 'ok' )
assert( di.get('Y') === di.get('Y') )

4.4.2 inheritPrototype

type: boolean (default false)

Enable inheritance of rules from ES6 extended parents classes.
decorator must be enabled to parents rules you want to extend from.

class Z{
  constructor(...params){
    this.params = params
  }
}
class ZX extends Z{}

di.addRules({
  'Z': {
    classDef: Z,
    params: [ di.value(1), di.value(2), di.value(3) ],
    decorator: true, //needed for parent class by extended using inheritPrototype
  },
  'Z2': {
    classDef: ZX,
    inheritPrototype: true,
  },
})

const z   = di.get('Z').getParams()
const z2  = di.get('Z2').getParams()
assert.deepEqual(z2, z)

4.4.3 inheritMixins

type: array

Enable inheritance from a list of specified rules.

class A{
  constructor(...params){
    this.params = params
  }
  getParams(){
    return this.params
  }
}
class B{
  constructor(...params){
    this.params = params
  }
  getParams(){
    return this.params
  }
}

di.addRules({
  'A':{
    classDef: A,
    params: [ di.value('a') ],
  },
  'B':{
    classDef: B,
    inheritMixins: [ 'A' ],
  },
})

const a = di.get('A').getParams()
const b = di.get('B').getParams()
assert.deepEqual(b, a)  
4.4.4 decorator

type: boolean (default false)

When set to true, a Symbol property will be set on class or factory function, allowing to use inheritPrototype. If the decorator injection approach is used, it's not necessary to configure this rule, because the Symbol will be set whatever the decorator key value is.
This is required to enable inheritPrototype feature.

4.5. asynchronous dependencies resolution

The following rule's keys allow you to manage the asynchronous dependencies resolution flow.
When a dependency return a Promise and this promise is waited for resolution by asyncResolve, the outputed object of di.get() method will be a Promise object, wich will be resolved by the expected object.

4.5.1 asyncResolve

type: boolean (default false)

When set to true, if a factory return a Promise, the dependency tree will wait for it's resolution, and then call the requiring dependency with the Promise's resolved value.
Promise is detected with instanceof operator, if you want to use a specific Promise polyfill (eg: bluebird) you can use the promiseFactory and promiseInterfaces container's config options.

function A(b, c){
  this.b = b
  this.c = c
}
async function B(){
  return 'b'
}
async function C(){
  return 'c'
}

di.addRules({
  'A': {
    classDef: A,
    params: ['B','C'],
  },
  'B': {
    classDef: B,
    asyncResolve: true,
  },
  'C': {
    classDef: C,
    asyncResolve: false, //default
  },
})

di.get('A').then(a => {
  assert(a.b === 'b')
  assert(a.c instanceof Promise)
})

4.5.2 asyncCallsSerie

type: boolean (default false)

When set to true, defer calls resolution sequentially when the method or callback require a dependency returning a Promise and for wich asyncResolve rule option setted to true.

class A{
  setB(d){
    this.b = ++d.i
  }
  setC(d){
    this.c = ++d.i
  }
}

function B(d){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve(d)
    }, 200)
  })
}
function C(d){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve(d)
    }, 100)
  })
}

function D(){
  this.i = 0
}

di.addRules({
  'A': {
    classDef: A,
    calls: [
      ['setB', ['B'] ],
      ['setC', ['C'] ],
    ],
    sharedInTree: ['D'],
    asyncCallsSerie: false, //default
  },
  'A2': {
    instanceOf: 'A',
    asyncCallsSerie: true,
  },
  
  'B': {
    classDef: B,
    params: ['D'],
    asyncResolve: true,
  },
  'C': {
    classDef: C,
    params: ['D'],
    asyncResolve: true,
  },
  'D':{
    classDef: D,
  },
  
  
})

di.get('A').then( a => {
  assert.strictEqual(a.b, 2)
  assert.strictEqual(a.c, 1)
} )

di.get('A2').then( a => {
  assert.strictEqual(a.b, 1)
  assert.strictEqual(a.c, 2)
} )
4.5.3 asyncCallsParamsSerie

type: boolean (default false)

When set to true, ensure that the dependencies stacks for all calls of a dependency are resolved sequentially according to order of calls, when the method or callback require a dependency returning a Promise and for wich asyncResolve rule option setted to true. Setted to true, it will implicitly set asyncCallsSerie to true.

class A{
  setB(b){
    this.b = b
  }
  setC(c){
    this.c = c
  }
}

function B(d){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve(++d.i)
    }, 200)
  })
}
function C(d){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve(++d.i)
    }, 100)
  })
}

function D(){
  this.i = 0
}

di.addRules({
  'A': {
    classDef: A,
    calls: [
      ['setB', ['B'] ],
      ['setC', ['C'] ],
    ],
    asyncCallsParamsSerie: true,
    sharedInTree: ['D'],
  },
  'B': {
    classDef: B,
    params: ['D'],
    asyncResolve: true,
  },
  'C': {
    classDef: C,
    params: ['D'],
    asyncResolve: true,
  },
  'D':{
    classDef: D,
  },
})

di.get('A').then( a => {
  assert(a.b === 1)
  assert(a.c === 2)
})

4.6 dependency file location

The following rule's keys are about dependency file location.

4.6.1 autoload

type: boolean (default false)

When set to true, check for allready registred dependency and if not, in node, try to require it, if dependency is not found it can (maybe) throw an Error according to autoloadFailOnMissingFile container config.
The require path resolution is based first on path rule option if defined, then on instanceOf rule option if defined (if instanceOf point to a rule with it's own path it will get it), and finally on the key of the rule. This require path can be post-processed using autoloadPathResolver container config.
The colons character : can be used to get a subkey of exported, and you can use it multiple times in same expression to get nested value. When path is defined it will implicitly set autoload to true.

di.addRules({
  'http:Server':{
    autoload: true,
  },
  '#server': {
    instanceOf: 'http:Server',
    autoload: true,
  },
  '#server2': {
    path: 'http:Server',
  },
  
})

assert( di.get('http:Server') instanceof require('http').Server )
assert( di.get('#server') instanceof require('http').Server )
assert( di.get('#server2') instanceof require('http').Server )
4.6.2 path

type: string

The require path can be post-processed by autoloadPathResolver container config.
When defined it will implicitly set autoload to true.
You can traverse exported and get specific key using colons character :.
You can't use relative path, if you want to include relative path, your application source files for exemple, you have to alias directories (or files) using autoloadPathResolver feature.
See autoload section for more details on the requiring behavior based on implicit path with instanceOf and rule's key.

di.addRules({
  '#server': {
    path: 'http:Server',
  },
})

assert( di.get('#server') instanceof require('http').Server )

5. Container

The container config options manage the container behavior and the way that the rules are resolving.

import container from 'di-ninja'

//set config on container creation
const di = container(config)

//or using config method
const di = container()
di.config(config)
di.config('aConfigKey', aConfigValue)

Order of config calls doesn't matter except for options dependencies and rules wich must be called at end. If key/value config object is provided as config param, options will be treated in the correct order.

5.1 rules

See rules section.

di.config({
  rules
})

//or
di.config('rules',rules)

//or
di.addRules(rules)
di.addRule('#myClassName', rule)

5.2 rulesDefault

Default values for rules, each rule will be extended from this setup, values will be overidded or merged.
See rule inheritance documentation section for more details on extending. See rules section for rules options.

di.config('rulesDefault',rulesDefault)

5.3 dependencies

Dependencies is intendeed to allow you to "inject" require's context directories as preload dependencies. It work using the webpack require.context feature, but a node polyfill called container.context is provided with di-ninja allowing you to build isomorphic architecture.
see also polyfillRequireContext

NodeJS example

import container from 'di-ninja'

const di = container({
  rules:{
    'app/A': {
      
    },
    'app/B': {
      
    },
    'app/B/C': {
      
    },
  },
  
  dependencies: {
    
    'app' : container.context('./src', true, /\.js$/),
    
    'A': container.require('./src/A'),
    
    'B': container.dependency(require('./src/B')),
    
  },
})

assert( di.get('app/A') instanceof require('./src/A').default )

assert( di.get('app/B') instanceof require('./src/B').default )

assert( di.get('app/B/C') instanceof require('./src/B/C').default )

assert( di.get('A') instanceof require('./src/A').default )

assert( di.get('B') instanceof require('./src/B').default )

Isomorphic example
Use the same code for browser compilation (via webpack for example) than on server-side with nodejs.

in webpack.config.js

const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        APP_ENV: JSON.stringify('browser')
      }
    }),
  ],
  /* ... */
}

and in your dependency-injection config file

import container from 'di-ninja'

if(process.env.APP_ENV !== 'browser'){
  //we are not in webpack/browser but in nodejs/server-side
  require.context = container.context
}

const di = container({
  rules:{
    'app/A': {
      
    },
    'app/B': {
      
    },
    'app/B/C': {
      
    },
    
  },
  
  dependencies: {
    'app' : require.context('./src', true, /\.js$/),    
    
    'A': container.dependency( require('./src/A') ),
    
    'B': container.dependency( require('./src/B') ),
  },
})

assert( di.get('app/A') instanceof require('./src/A').default )

assert( di.get('app/B') instanceof require('./src/B').default )

assert( di.get('app/B/C') instanceof require('./src/B/C').default )

assert( di.get('A') instanceof require('./src/A').default )

assert( di.get('B') instanceof require('./src/B').default )

5.4 autoloadPathResolver

Rule's path post-processor callback function or alias map object.

with callback

di.config('autoloadPathResolver', (path)=>{
  switch(path){
    case 'framework':
      path = 'express'
    break
  }
  return path
})

with alias map object

import path from 'path'
const aliasMap = {
  'app': path.resolve(__dirname, './src'),
}

di.config('autoloadPathResolver', aliasMap)


//will implicitly set the following predefined callback, here is the magic under the hood
di.config('autoloadPathResolver', (path)=>{
  Object.keys(aliasMap).forEach(alias=>{
    const realPath = aliasMap[alias]
    if(path == alias){
      path = realPath
    }
    else if(path.substr(0,alias.length+1)==alias+'/'){
      path = realPath+path.substr(alias.length)
    }
  })
  return path
})

5.5 autoloadExtensions

You can use Array or RegExp.

di.config('autoloadExtensions', ['js', 'jsx'])

di.config('autoloadExtensions', new RegExp('\.(js|jsx)

))

5.6 autoloadFailOnMissingFile

Possible values are string path, true or false.
Setted to false, it will never throw error on missing dependency.
Setted to true, it will always throw an error on missing dependency.
Setted to path, it will throw error only if a dependency with a specified rule's option path is specified. The default value is path.

di.config('autoloadFailOnMissingFile', true)

5.7 defaultVar

Value by default for defaultRuleVar, defaultDecoratorVar, and defaultArgsVar.

This is about implicit wrapping of params, calls and lazyCalls.

Possible values are interface or value.
Default is interface.

interface mean that all scalar values will be implicitly wrapped by di.interface(), it will be resolved as class. value mean that all scalar values will be wrapped by di.value(), it will be left as untouched.

interface example

di.config('defaultVar', 'interface') //default

di.addRule('A', {
  params: [
    {
      B :        'B',
      message :  di.value( 'Hello world !' ),
      config :  di.value({
        option1: value1
        /* ... */
      }),
    }
  ]
})

value example

di.config('defaultVar', 'value')

di.addRule('A', {
  params: [
    {
      B :        di.interface('B'),
      message :  'Hello world !',
      config :  di.value({
        option1: value1
        /* ... */
      }),
    }
  ]
})

5.8 defaultRuleVar

Implicit wrapping for scalar values defined from rules.
see defaultVar.

5.9 defaultDecoratorVar

Implicit wrapping for scalar values defined from decorator.
see defaultVar.

5.10 defaultArgsVar

Implicit wrapping for scalar values defined from manual call (see params).
see defaultVar.

5.11 defaultFactory

default wrapper class to instanciate for di.factory() method.

5.12 defaultFunctionWrapper

Implicit wrapping for function or class values.
Possible values are ClassFactory or ValueFactory.
The default value is ClassFactory.

ClassFactory example

import ClassFactory from 'di-ninja/dist/classFactory'
di.config('defaultFunctionWrapper', ClassFactory) //default

class B{}

di.addRule('A', {
  params: [
    B,
    di.valueFactory( () => {
      return 'Hello world !'
    } )
  ],
})

ValueFactory example

import ValueFactory from 'di-ninja/dist/valueFactory'
di.config('defaultFunctionWrapper', ValueFactory)

class B{}

di.addRule('A', {
  params: [
    di.classFactory( B ),
    () => {
      return 'Hello world !'
    }
  ],
})

5.13 promiseFactory

default: Promise (global)
promiseFactory option let you modify the way DiNinja create Promise for handle asyncResolve.
For example, you can use it with bluebird.
The common way is to use it in combination with promiseInterfaces option.

import bluebird from 'bluebird'

di.config('promiseFactory', bluebird)

function A (b) {
  this.b = b
}
function B () {
  return new Promise((resolve, reject) => {
  resolve('b')
  })
}

di.addRules({
  'A': {
  classDef: A,
  params: ['B']
  },
  'B': {
  classDef: B,
  asyncResolve: true
  }
})

assert( di.get('A') instanceof bluebird )

5.14 promiseInterfaces

default: Promise (global)
promiseInterfaces option let you modify the way DiNinja recognize Promise.
For example, you can use it with bluebird.
The common way is to use it in combination with promiseFactory option.
The promiseFactory option will automatically be pushed to promiseInterfaces.

import bluebird from 'bluebird'

di.config('promiseInterfaces', [ bluebird, Promise /* let the standard Promise be identified */ ])

function A(b){
  this.b = b
}
function B(){
  return new bluebird((resolve, reject)=>{
    resolve('b')
  })
}

di.addRules({
  'A': {
    classDef: A,
    params: ['B'],
  },
  'B': {
    classDef: B,
    asyncResolve: true,
  },
})

assert( di.get('A') instanceof Promise )

5.15 interfacePrototype

Enable you to use Symbol based interface reference instead of string as dependency key, allowing you to use runtime interfaceTypeCheck.

import {
  InterfacePrototype,
  instanceOf,
  Interface,
} from 'interface-prototype'

di.config('interfacePrototype', InterfacePrototype)
    
const I = new Interface();

@di('A')
@instanceOf(I)
class A{}

@di('B')
@instanceOf(I)
class B{}

di.addRules({
  [I]: {
    classDef: A,
  }
})

@di('D', [I])
class D {
  constructor(i){
    this.i = i;
  }
}

assert(di.get('A') instanceof I)
assert(di.get('B') instanceof I)

assert(di.get(I) instanceof A)
assert( !( di.get(I) instanceof B ))

assert(di.get('D').i instanceof A)
 

5.16 interfaceTypeCheck

type: boolean (default false)

Enable check for "implemented" interface using interfacePrototype. If a manually provided dependency doesn't "implement" the required "interface", it will throw an error.
In combination with interface-prototype, this enable runtime type-check and custom type-check for all type of variables.

5.17 globalKey

type: string | boolean (default false)
When setted to true it will be transformed to string 'di'.
If provided, global.globalKey (on node) or window.globalKey (on browser) will be set to the instance of container.

container({
  globalKey: 'di',
})

di.get('A')

5.18 ruleCache

type: boolean (default true)
Enable cache for built rules, optimizing the future calls of di.get().
You can set it to false when you have rules dynamically modified after some di.get() calls.

di.config('ruleCache', false)

di.addRule('A', { params: [ 'B' ] })

const a1 = di.get('A')

di.addRule('A', { params: [ 'C' ] })

const a2 = di.get('A')

5.19 polyfillRequireContext

type: boolean (default false)
This is an experimental feature, so be carefull.
It will automatically polyfill the webpack's require.context method to nodejs environment.
It's an helper to enforce easy isomorphism (cross-environment).
See also dependencies for the hack-less technic

6 Examples

Here is a list of examples of complex di-ninja implementations with dependencies wired by directories:

  1. di-ninja-nodejs-example (using di-ninja node polyfill for require.context())
  2. di-ninja-webpack-example (using require.context() native method of webpack)
  3. di-ninja-reactnative-example (using babel-plugin-require-context-polyfill)

About

Built with babel.
Work with NodeJS, Webpack, React-Native and UMD/AMD (requirejs). Inspired by strategy for php, itself based on dice design. Made with ❤️ and 🦊.