Giter Site home page Giter Site logo

ivandotv / pumpit Goto Github PK

View Code? Open in Web Editor NEW
23.0 2.0 2.0 777 KB

PumpIt is a small (~2KB) dependency injection container without the decorators, suitable for the browser.

License: MIT License

JavaScript 0.25% TypeScript 99.75%
dependency-injection dependency-injection-container dependency-inversion ioc ioc-container

pumpit's Introduction

PumpIt

Test Codecov GitHub license

PumpIt is a small (~2KB) dependency injection container without the decorators and zero dependencies, suitable for the browser. It supports different injection scopes, child containers, hooks etc...

Motivation

Dependency injection is a powerful concept, and there are some excellent solutions like tsyringe, awilix, and inversify, however, they all use decorators (which are great, but not a standard), and their file size is not suitable for front-end development. So I've decided to create an implementation of a dependency injection container that is small and doesn't use decorators. I also believe that I've covered all the functionality of the above-mentioned libraries.

Getting Started

Installation:

npm i pumpit

Since PumpIt does not rely on the decorators the injection is done via the injection property. When used with classes, inject will be a static property on the class, and it will hold an array of registered injection tokens that will be injected into the constructor in the same order when the class instance is created (in case of factory functions it will be a property on the function itself, more on that later).

Registering classes

import { PumpIt } from 'pumpit'

const container = new PumpIt()
const bindKeyB = 'b'

class TestA {
  static inject = [bindKeyB]

  constructor(b: B) {}
}

class TestB {}

// bind (register)  classes to the injection container.
container.bindClass(TestA, TestA).bindClass(bindKeyB, TestB)

//resolve values
const instanceA = container.resolve<TestA>(TestA)
const instanceB = container.resolve<TestA>(bindKeyB)

instanceA.b // injected B instance

There is also alternative syntax that you can use when you don't want to use the static inject property, or you are importing a class from third-party packages.

import { PumpIt } from 'pumpit'

const container = new PumpIt()
class TestA {
  constructor(b: TestB) {}
}

class TestB {}

//`bind`(register)  classe to the injection container.
container.bindClass(TestA, { value: TestA, inject: [TestB] })

Class injection inheritance

Class injection inheritance is supported out of the box, which means that the child class will get dependencies that are set to be injected to the parent.

const pumpIt = new PumpIt()

class TestB {}

class TestA {
  static inject = [TestB]
}

class TestC extends TestA {
  //TestB will be injected by reading `inject` array from the parent (TestA)
  constructor(public b: TestB) {
    super()
  }
}

pumpIt.bindClass(TestA, TestA)
pumpIt.bindClass(TestB, TestB)
pumpIt.bindClass(TestC, TestC)

const instance = pumpIt.resolve<TestC>(TestC)

expect(instance.b).toBeInstanceOf(TestB)
Combining injection dependencies

Child class can define their own dependencies and combine them with the parent dependencies.

class TestB {}
class TestD {}

class TestA {
  static inject = [TestB]

  constructor(public b: TestB) {}
}

class TestC extends TestA {
  // use dependencies from the parent and add your own (TestD class)
  static inject = [...TestA.inject, TestD]

  constructor(
    public b: TestB,
    public d: TestD
  ) {
    super()
  }
}

pumpIt.bindClass(TestA, TestA)
pumpIt.bindClass(TestB, TestB)
pumpIt.bindClass(TestC, TestC)
pumpIt.bindClass(TestD, TestD)

const instance = pumpIt.resolve<TestC>(TestC)

expect(instance.b).toBeInstanceOf(TestB)
expect(instance.d).toBeInstanceOf(TestD)

Registering factories

When registering function factories, function needs to be provided as the value, and when that value is requested, the function will be executed and returned result will be the value that will be injected where it is needed.

const container = new PumpIt()

const myFactory = () => 'hello world'

container.bindFactory(myFactory, myFactory)

const value: string = container.resolve(myFactory)

value === 'hello world'

Factories can also have dependencies injected. They will be passed as the arguments to the factory function when it is executed.

const container = new PumpIt()
class A {
  hello() {
    return 'hello from A'
  }
}

const myFactory = (a: A) => {
  return a.hello()
}
myFactory.inject = [A]

container.bindClass(A, A)
container.bindFactory(myFactory, myFactory)

const value: string = container.resolve(myFactory) //hello from A

Or alternative syntax (same as class alternative syntax):

const container = new PumpIt()

class A {
  hello() {
    return 'hello from A'
  }
}

const myFactory = (a: A) => {
  return a.hello()
}

container.bindClass(A, A)
container.bindFactory(myFactory, { value: myFactory, inject: [A] })

const value: string = container.resolve(myFactory)
value === 'hello from A'

I encourage you to experiment with factories because they enable you to return anything you want.

Registering values

Values should be used when you just want to get back the same thing that is passed in to be registered.

const container = new PumpIt()

const myConfig = { foo: 'bar' }

container.bindValue('app_config', myConfig)

const resolvedConfig = container.resolve('app_config')

resolvedConfig === myConfig

Resolving container data

When the container data is resolved, if the key that is requested to be resolved is not found, the container will throw an error.

const container = new PumpIt()

container.resolve('key_does_not_exist') // will throw

Resolve context

You can also pass in additional data that will be used in various callbacks that will be called when resolving the key.

const container = new PumpIt()
const resolveCtx = { foo: 'bar' }
container.resolve('some_key', resolveCtx)

Read about transforming dependencies to see how context is passed to the callbacks.

Injection tokens

Injection tokens are the values by which the injection container knows how to resolve registered data. They can be string, Symbol, or any object.

const container = new PumpIt()
const symbolToken = Symbol('my symbol')

class A {}

//bind to container
container.bindClass('my_token', A)
container.bindClass(symbolToken, A)
container.bindClass(A, A)

//resolve
container.resolve<A>('my_token')
container.resolve<A>(symbolToken)
container.resolve<A>(A)

//inject tokens
class B {
  static inject = [symbolToken, 'my_token', A]
  constructor(aOne: A, aTwo: A, aThree: A) {}
}

Injection scopes

There are four types of injection scopes:

Singleton

Once the value is resolved the value will not be changed as long as the same container is used.

In the next example, both A and B instances have the same instance of C

import { PumpIt, SCOPE } from 'pumpit'

container = new PumpIt()

class A {
  static inject = [C, B]
  constructor(
    public c: C,
    public b: B
  ) {}
}
class B {
  static inject = [C]
  constructor(public c: C) {}
}

class C {}

container.bindClass(A, A)
container.bindClass(B, B)
container.bindClass(C, C, { scope: SCOPE.SINGLETON })

// A -> B,C,
// B -> C
const instanceA = container.resolve(A)

//A and B share the same instance C
instanceA.c === instanceA.b.c

Transient

This is the default scope. Every time the value is requested, a new value will be returned (resolved). In the case of classes, it will be a new instance every time, in the case of factories, the factory function will be executed every time.

In the next example, both A and B instances will have a different C instance.

import { PumpIt, SCOPE } from 'pumpit'
container = new PumpIt()

class A {
  static inject = [C, B]
  constructor(
    public c: C,
    public b: B
  ) {}
}
class B {
  static inject = [C]
  constructor(public c: C) {}
}

class C {}

container.bindClass(A, A)
container.bindClass(B, B)
container.bindClass(C, C, { scope: SCOPE.TRANSIENT })

// A -> B,C,
// B -> C
const instanceA = container.resolve(A)

//C instance is created two times
//A and B have different instances of C
instanceA.c !== instanceA.b.c //C

Request

This is similar to the singleton scope except the value is resolved once per resolve request chain. Every new call to container.resolve() will create a new value.

import { PumpIt, SCOPE } from 'pumpit'

container = new PumpIt()

class A {
  static inject = [C, B]
  constructor(
    public c: C,
    public b: B
  ) {}
}
class B {
  static inject = [C]
  constructor(public c: C) {}
}

class C {}

container.bindClass(A, A)
container.bindClass(B, B)
container.bindClass(C, C, { scope: SCOPE.REQUEST })

const firstA = container.resolve(A)
const secondA = container.resolve(A)
firstA.c === firstA.b.c // A and B share C

secondA.c === secondA.b.c // A and B share C

secondA.c !== firstA.c //C from first request is different to the C from the second request

Container singleton

This scope is similar to the regular singleton scope, but in the case of child containers, the child container will create its version of the singleton instance.

In the next example, the child container will create its own version of the singleton instance.

import { PumpIt, SCOPE } from 'pumpit'

container = new PumpIt()
const childContainer = container.child()

class A {
  static count = 0
  constructor() {
    A.count++
  }
}

container.bindClass(A, A, { scope: SCOPE.CONTAINER_SINGLETON })

const parentOneA = container.resolve(A)
const parentTWoA = container.resolve(A)

parentOneA === parentTWoA
A.count === 1

const childOneA = childContainer.resolve(A)
const childTwoA = childContainer.resolve(A)

childOneA === childTwoA
A.count === 2

// parent and child have different instances
childOneA !== parentOneA

Injection scopes do not apply to bound values (bindValue)

Optional injections

Whenever the injection container can't resolve the requested dependency anywhere in the chain, it will immediately throw.

But you can make the dependency optional, and if it cant be resolved, the container will not throw, and undefined will be injected in place of the requested dependency. For this, you need to use the get() helper function.

import { PumpIt, get } from 'pumpit'

const container = new PumpIt()

class A {
  //make B optional dependency
  static inject = [get(B, { optional: true })]
  constructor(public b: B) {}
}
class B {}

//NOTE: B is NOT registered with the container
container.bindClass(A, A)

const instanceA = container.resolve(A)

instanceA.b // undefined

Circular dependencies

NOTE: Circular dependency functionality has been removed in version 6. If you want to use circular dependency you can use version 5

Injecting arrays

NOTE: Injecting array as a dependency has been removed in version 6. If you want to use this feature you can use version 5

Transforming dependencies (hooks)

Dependencies can be transformed before being resolved.

They can be manipulated just before they are created, or after they are created.

  • "beforeResolve" callback is called before the registered value is created. In the case of the class just before the class instance is created. In the case of the factory just before the factory is executed.

  • "afterResolve" - callback is called after the class instance is created, or factory function is executed. The value in the callback represents whatever is returned from the beforeResolve callback. This callback is the perfect place to do any post creation setup.

const container = new PumpIt()
const valueB = { name: 'Ivan' }
const resolveCtx = { foo: 'bar' }

class TestA {
  static inject = [keyB]

  constructor(public keyB: typeof valueB) {}
  hello(){
    return  'hello world'
  }
}

container.bindValue(keyB, valueB)

container.bindClass(keyA, TestA, {
  beforeResolve: ({ container, value, ctx }, ...deps) => {

    container === pumpIt // instance of PumpIt
    value === TestA // class constructor
    ctx === resolveCtx//context data if any
    deps ===[valueB]// resolved dependency of class TestA

    // internally this is the default behavior
    return new value(...deps)

    // in case of factory function
    // return value(...deps)
  },
  afterResolve:({container,value,ctx}=>{

    container === pumpIt // instance of PumpIt
    value // whatever is returned from the "beforeResolve" callback
    //^ in this case it is an instance of TestA
    ctx === resolveCallbackData //context data if any

    // you can do custom setup here
    value.hello() // hello world
  })
})

const instance = pumpIt.resolve(TestA, resolveCtx)

The number of times these callbacks will be executed directly depends on the scope with which the value was registered. In the case of a singleton scope callbacks will be executed only once, since the values are resolved only once.

Transforming injected dependencies

Injected dependencies can also be manipulated just before they are injected. For this, we use the transform() helper function.

transform function wraps the injected dependencies, and accepts a callback which will receive all the resolved dependencies that need to be injected, and it should return an array of dependencies. whatever is returned from the callback, will be injected.

import { transform, PumpIt } from 'pumpit'

const container = new PumpIt()

const keyA = Symbol()
const keyB = Symbol()
const keyC = Symbol()

const valueA = { name: 'a' }
const valueB = { name: 'b' }
const valueC = { name: 'c' }

const resolveCtx = { hello: 'world' }

class TestA {
  static inject = transform(
    [keyA, keyB, keyC],
    (
      { container, ctx },
      a: typeof valueA,
      b: typeof valueB,
      c: typeof valueC
    ) => {
      container === pumpIt // instance of PumpIt
      ctx === resolveCtx // context data

      a === valueA
      b === valueB
      c === valueC

      //default implementation, return the same dependencies in the same order
      return [a, b, c]
    }
  )

  constructor(a: typeof valueA, b: typeof valueB, c: typeof valueC) {}
}

Post construct method

If the class that is being constructed (resolved) has a "postConstruct" method defined it will be called automatically when the class instance is created, in the case of singleton instances it will be called only once. One more important thing about postConstruct method is that it will be called in the reverse order of the resolution chain. Please refer to this test for a concrete example

Removing values from the container

Registered values can be removed from the container. When the value is removed, trying to resolve the value will throw an error.

const container = new PumpIt()

container.bindValue('name', 'Mario')

container.unbind('name')

container.resolve('name') // throws error

Calling the dispose method

If the class has a method dispose() it will automatically be called on the disposed of value, but only if the value is a singleton.

Internally, the container will remove the value from its internal pool, and if the value was registered with the scope: singleton and the value has been resolved before (class has been instantiated or factory function executed). That means that the container holds an instance of the value, and it will try to call the dispose of method on that instance, or in the case of the factory, on whatever was returned from the factory.

const container = new PumpIt()

class TestA {
  static count = 0
  dispose() {
    TestA.count++
  }
}

pumpIt.bindClass(TestA, TestA, { scope: 'SINGLETON' })
pumpIt.unbind(TestA)

pumpIt.has(TestA) // false

TestA.count === 1

If you don't want to call the dispose method, pass false as the second parameter container.unbind(TestA, false)

Dispose callback

When registering the class or factory, you can provide an unbind callback that will be called when the value is about to be removed from the container.

unbind callback will be called regardless of whether the value to be removed is singleton or not.

const container = new PumpIt()

class TestA {}

container.bindClass(TestA, TestA, {
  scope: 'SINGLETON',
  unbind: (container, dispose, value) => {
    container === pumpIt
    value // TestA instance is scope: singleton otherwise TestA constructor
    dispose // true if `dispose` method should be called
  }
})

Please note that in the preceding example value property in the callback can be a TestA constructor or an instance of TestA depending on if the value was registered with the scope of singleton and it was resolved before (container holds the instance singleton).

Removing all the values from the container

You can remove all the values from the container by calling container.unbindAll(). This method will remove all the keys from the container, so the container will be empty. All the same, rules apply as for the container.unbind() method.

const container = new PumpIt()
const callDispose = true
container.unbindAll(callDispose)

Clearing container values

You can clear all the singleton values that are present in the container. When the values are cleared, resolving those values again will create new singletons. Also, the dispose method will be called (if present on the instance).

const container = new PumpIt()

class TestA {}

container.bindClass(TestA, TestA, { scope: 'SINGLETON' }) // or SCOPE.CONTAINER_SINGLETON

const instanceOne = pumpIt.resolve(TestA)

container.clearAllInstances()

const instanceTwo = container.resolve(TestA) // new instance

instanceOne !== instanceTwo

A particular singleton instance can also be cleared by using the key:

const container = new PumpIt()

class TestA {}

container.bindClass(TestA, TestA, { scope: SCOPE.SINGLETON }) // or SCOPE.CONTAINER_SINGLETON

const instanceOne = pumpIt.resolve(TestA)

container.clearInstance(TestA)

Clearing a single singleton will return true if the singleton key was found, false otherwise.

Child containers

Every container instance can create a child container.

The child container is a new PumpIt instance that is connected to the parent container instance and it inherits all the values that are registered with the parent.

The great thing about the child container is that it can shadow the parent value by registering a value with the same key.

Shadowing values

The child container can have the same key as the parent, in that case when the value is resolved, the child container value will be returned.

const parent = new PumpIt()
const child = parent.child()

const key = 'some_key'

class ParentClass {}
class ChildClass {}

parent.bindClass(key, ParentClass)

child.bindClass(key, ChildClass)

const instance = child.resolve(key) // ChildClass

Parent -> child chains can be as long as you like grand parent -> parent -> child ...

Checking for values

When you check if the value exists on the child, the parent instance is also searched. You can optionally disable searching on the parent.

const parent = new PumpIt()
const child = parent.child()

class TestA {}

parent.bindClass(TestA, TestA)

child.has(TestA) //true

// disable search on the parent
child.has(TestA, false) // false

Child singletons

If the parent container has registered a value with a scope SINGLETON all child containers will share the same instance however, if the parent has registered the value with the scope CONTAINER_SINGLETON then child containers will create their versions of singleton instances.

const parent = new PumpIt()
const child = parent.child()

class TestA {
  static count = 0

  constructor() {
    TestA.count++
  }
}

parent.bindClass(TestA, TestA, { scope: SCOPE.CONTAINER_SINGLETON })

const parentInstance = parent.resolve<TestA>(TestA)
const childInstance = child.resolve<TestA>(TestA)

parentInstance !== childInstance
TestA.count === 2

API docs

PumpIt is written in TypeScript, auto generated API documentation is available.

License

This project is licensed under the MIT License - see the LICENSE file for details

pumpit's People

Contributors

github-actions[bot] avatar intellild avatar ivandotv avatar renovate-bot avatar renovate[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pumpit's Issues

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

github-actions
.github/workflows/CI.yml
  • actions/checkout v4
  • actions/setup-node v4
  • pnpm/action-setup v3.0.0
  • actions/cache v3
  • changesets/action v1
npm
package.json
  • @biomejs/biome 1.5.3
  • @changesets/cli ^2.27.1
  • @vitest/coverage-v8 ^1.3.1
  • lefthook ^1.6.5
  • microbundle ^0.15.1
  • shx ^0.3.4
  • typedoc ^0.25.10
  • typedoc-plugin-markdown ^3.17.1
  • typescript ^5.3.3
  • vitest ^1.3.1

  • Check this box to trigger a request for Renovate to run again on this repository

Implement validate dependencies method

Implement a method that will validate all the dependencies in the container (confirm that all dependencies can be resolved and instantiated)
This will effectively prevent runtime resolve errors.

Support --moduleResolution node16 in typescript

Hi! I'm really liking this library! Thank you for your work.
I use moduleResolution of node16 in my project, and can't import it without errors.
image

Then I edit package.json in node_modules

...

  "umd:main": "./dist/prod/index.umd.js",
  "exports": {
    "require": "./dist/prod/index.cjs",
    "default": "./dist/prod/index.modern.js",
+   "types": "./src/index.ts"
  },
  "types": "./dist/types/index.d.ts",
...

But the source files is containing imports without .js extension, so there's other error

image

And last thing I tried is providing url to .d.ts file in exports:

...

  "umd:main": "./dist/prod/index.umd.js",
  "exports": {
    "require": "./dist/prod/index.cjs",
    "default": "./dist/prod/index.modern.js",
-   "types": "./src/index.ts"
+   "types": "./dist/types/index.d.ts"
  },
  "types": "./dist/types/index.d.ts",
...

But it also didn't work.
image

Without extensions it just doesn't reexport types in index.d.ts

image

I think the best solution for this problem is providing extensions for import paths and providing type's path to index.d.ts file in exports. What do you think?

Dependency inversion principle in bindClass

Hi, I am not very good with english, but i will try to express me. I am using this library with qwik.js because It does not support with decorators (for example, qwik.js with inversify.js). It is a very good library for this use case. the problem is that i use the SOLID principe "Dependency inversion principle" https://en.wikipedia.org/wiki/Dependency_inversion_principle and i need to use the bindClass's generic with interfaces, but it is not working. I am using my solution for this problem:

import type { BindKey, ClassOptions } from "pumpit";
import { PumpIt } from "pumpit";
import { UserRepository } from "./domain"
import { UserMongolRepository } from "./repository"

type Instanciable<T> =
  | { new (...arg: any[]): T }
  | { new (...arg: any[]): T; inject?: Inject };

type BindOptions<T> = Omit<
  Partial<
    ClassOptions<
      Instanciable<T>,
      "SINGLETON" | "TRANSIENT" | "REQUEST" | "CONTAINER_SINGLETON"
    >
  >,
  "type"
>;

class IoC extends PumpIt {
  bind = <T>(
    key: BindKey,
    value: Instanciable<T>,
    options?: BindOptions<T>
  ) => {
    return super.bindClass(key, value, options);
  };
}

export const container = new IoC();
container.bind<UserRepository>("TOKEN_REPOSITORY", UserMongoRepository);

can you add this features in your library? thanks for reading this

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.