1.0.1 • Published 4 years ago

diminish v1.0.1

Weekly downloads
3
License
ISC
Repository
github
Last release
4 years ago

NPM Build Status Coverage Status

Diminish Minimal Dependency Injection

Getting Started

Install the package with npm as follows

npm install --save diminish

Now create a container by importing the provided Container class.

import { Container } from 'diminish'

const container = new Container()

Providers

Diminish works by defining providers using the register method. Once defined, the resolve method can be used to execute the provider.

container.register('itemOne', () => {
  return 1
})

const itemOne = container.resolve('itemOne')
//--> itemOne === 1

Dependencies can be injected into a provider by passing them as arguments. This works using standard function arguments or destructured arguments. When the provider is resolved, each of the dependencies will be resolved first.

// Destructured Arguments
container.register('itemTwo', ({ itemOne }) => {
  return 1 + itemOne
})

// Standard Arguments
container.register('itemTwo', (itemOne) => {
  return 1 + itemOne
})

const itemTwo = container.resolve('itemTwo')
//--> itemTwo === 2

Async Providers

Asynchronous providers are transparently supported using the same interface. When a provider is asynchronous, the resolve method will return a promise which can be awaited.

container.register('itemOne', async () => {
  return 1
})

const promise = container.resolve('itemOne')
//--> promise = Promise(<Pending>)

Even if a provider is not asynchronous, if any of its dependencies are provided asynchronously the resolve method will still return a promise. For this reasons, unless you are certain that resolution doesn't involve any asynchronous operations, it is good practice to await calls to resolve.

// itemOne is an asynchronous provider
container.register('itemTwo', ({ itemOne }) => {
  return 1
})

const promise = container.resolve('itemTwo')
//--> promise = Promise(<Pending>)

Class Providers

It is also possible to provide a class constructor as the provider. When resolved, the class will be called with the new keyword and the returned instance will become the resolved value. Dependencies are injected according to the properties on the class constructor.

container.register('thing', class Thing {
  constructor ({ itemOne }) {
    this.value = itemOne
  }

  action () {
    console.log('It Works!')
  }
})

const thing = container.resolve('thing')
//--> thing - Thing { value: 1 }

thing.action()
//---> Logs "It Works!""

Literal Providers

Injecting a provider isn't always what you need. For example, you may want to register a function in a container. You could do this but wrapping the function up in a lightweight do-nothing provider as follows:

function someFunction () {
  console.log('Hey!')
}

container.register('someFunction', () => someFunction)

To make this easier Diminish includes another method on a container that does the above for you called literal.

// Adds the function as a trivial provider
container.literal('someFunction', someFunction)

Bulk Registration

Often many providers need to be registered at the same time. To make this easier both the register and literal methods will accept an object dictionary of providers or values respectively.

container.register({
  itemOne () {
    return 10
  },
  itemTwo ({ itemOne }) {
    return itemOne + 10
  }
})

container.literal({
  someFunction () {
    console.log('Hey')
  },
  someValue: 15
})

Injection Typing

In all of the above examples, injected dependencies will have type any inside of the provider definition. This works for many simple situations but ideally the types for your dependencies should be available inside the provider. In order to do this a custom interface can be given as a generic type to the Container constructor.

interface Types {
  itemOne: number
}

const container = new Container<Types>()

The register method will only allow registration of names and providers which correspond to a property defined on the generic interface.

container.register('itemOne', () => 10) // OK
container.register('itemTwo', () => 20) // ERROR

The same is true for the resolve method. Only keys from the given interface can be requested from the container.

const itemOne = container.resolve('itemOne') // OK
const itemTwo = container.resolve('itemTwo') // ERROR

To use this inside of your providers, use the interface as the parameter type in the function or constructor definition.

// Function
const provider = function ({ itemOne } : Types) {}

// Class
const provider = class {
  constructor ({ itemOne } : Types) {}
}

Support for Standard Parameters

So far we've only used examples where dependency injection is done using the destructured parameter style. This style is preferred in most cases. However support for standard parameters is included with diminish without any fuss.

// This works just fine
const provider = function itemTwo (itemOne: any) {}

Properly resolving types in your providers will require an additional step. In addition to creating an interface as above, you must create a namespace with the same name and declare the dependency types inside it.

interface Types {
  itemOne: number
}

namespace Types {
  type itemOne = number
}

Now the type can be accessed and used in the provider definitions

// Now this works too
const provider = function itemTwo (itemOne: Types.itemOne) {}

Global Type

In the above examples the interface used must be defined before the container is created. This means all of the dependencies and their types must be declared or imported prior to that point in the file. This can create issues when working with providers defined in different files. Trying to properly manage your imports in these situations can be complicated, which defeats the purpose of this package.

To get around this it is possible to declare your generic interface as part of the global scope. While in a general sense using the global scope is a touchy practice, I have found this to be a reasonable place to use it. Once created, in each of your files that global interface can be merged with a local interface of the same name and a new property (See Merging Interfaces) effectively allowing you to decentralize your type declarations.

// index.ts
import { Container } from 'diminish'
import { itemOneProvider } from './itemOne.ts'
import { itemTwoProvider } from './itemTwo.ts'

declare global {
  interface Types {}
  namespace Types {}
}

const container = new Container<Types>()
container.register('itemOne', itemOneProvider) // OK
container.register('itemTwo', itemTwoProvider) // OK

const itemTwo = container.resolve('itemTwo') // OK
// here itemTwo has type number | Promise<number>
// itemOne.ts

declare global {
  interface Types { itemOne: number }
  namespace Types { type itemOne = number }
}

export function itemOneProvider() : number {
  return 10
}
// itemTwo.ts

declare global {
  interface Types { itemTwo: number }
  namespace Types { type itemTwo = number }
}

export function itemTwoProvider({ itemOne } : Types) : number {
  // here itemOne will correctly have type
  return itemOne + 10
}

// OR

export function itemTwoProvider(itemOne : Types.itemOne) : number {
  // here itemOne will correctly have type
  return itemOne + 10
}

Invoked Providers

It is possible to inject dependencies into a provider without registering it by calling the invoke method. This method takes any valid provider and immediately resolves it without ever registering it with the container.

container.literal('itemOne', 10)

const result = container.invoke(({ itemOne }) => {
  return 10 + itemOne
})

// result === 20

It is possible to provide a custom context for executing the provider.

const context = { prop: 10 }
const result = container.invoke(context, function () {
  return this.prop
})
// result === 10

This will ONLY work with standard functions. Trying to set a custom context for either a class constructor or an arrow function will throw an error.

const context = { prop: 10 }
const result = container.invoke(context, () => {}) // ERROR
const result = container.invoke(context, class {}) // ERROR

Dynamic imports

When a project contains many complex providers it becomes convenient to dynamically load them at run-time. To make this easier Diminish includes an import method on the container. This method will grab files based on a glob or a list of globs.

// load modules from src or lib
await container.import('(src|lib)/**.js')

// OR

await container.import([
  'src/**.js',
  'lib/**.js'
])

By default this will call container.register on the exported object from each file. To set this up in each file, just export the desired provider with the desired name.

declare global {
  interface Types { itemOne: itemOneProvider }
  namespace Types { type itemOne = itemOneProvider }
}

function itemOneProvider () {
  return 10
}

// The exports object will be { itemOne: itemOneProvider }
export let itemOne = itemOneProvider

You may customize the loading behavior by giving a custom loader function to the import method. This function will called for each found file with the container and imported module as arguments.

// Change loader to add module exports as literals
await container.import('src/**.js', {
  loader: (container: Container, module: any) => {
    container.literal(module)
  }
})

A glob or and array of globs to ignore can also be given in the options object.

await container.import('src/**.js', { exclude: 'src/**.spec.js' })

Finally, the relative directory to use for resolving module paths can be set using the cwd option.

await container.import('src/**.js', { cwd: __dirname })
1.0.1

4 years ago

1.0.0

4 years ago

0.6.0

5 years ago

0.5.0

5 years ago

0.4.3

5 years ago

0.4.2

5 years ago

0.4.1

5 years ago

0.4.0

5 years ago

0.3.0

5 years ago

0.2.1

5 years ago

0.2.0

5 years ago

0.1.0

5 years ago