Giter Site home page Giter Site logo

jeffijoe / awilix Goto Github PK

View Code? Open in Web Editor NEW
3.3K 27.0 130.0 3.08 MB

Extremely powerful Inversion of Control (IoC) container for Node.JS

License: MIT License

JavaScript 4.49% TypeScript 95.46% Shell 0.06%
dependency-injection awilix ioc lifetime-management nodejs javascript

awilix's Introduction

Awilix

npm CI Coveralls npm npm node JavaScript Style Guide

Extremely powerful, performant, & battle-tested Dependency Injection (DI) container for JavaScript/Node, written in TypeScript.

Awilix enables you to write composable, testable software using dependency injection without special annotations, which in turn decouples your core application code from the intricacies of the DI mechanism.

💡 Check out this intro to Dependency Injection with Awilix

Table of Contents

Installation

Install with npm

npm install awilix

Or yarn

yarn add awilix

You can also use the UMD build from unpkg

<script src="https://unpkg.com/awilix/lib/awilix.umd.js" />
<script>
  const container = Awilix.createContainer()
</script>

Usage

Awilix has a pretty simple API (but with many possible ways to invoke it). At minimum, you need to do 3 things:

  • Create a container
  • Register some modules in it
  • Resolve and use!

index.js

const awilix = require('awilix')

// Create the container and set the injectionMode to PROXY (which is also the default).
// Enable strict mode for extra correctness checks (highly recommended).
const container = awilix.createContainer({
  injectionMode: awilix.InjectionMode.PROXY,
  strict: true,
})

// This is our app code... We can use
// factory functions, constructor functions
// and classes freely.
class UserController {
  // We are using constructor injection.
  constructor(opts) {
    // Save a reference to our dependency.
    this.userService = opts.userService
  }

  // imagine ctx is our HTTP request context...
  getUser(ctx) {
    return this.userService.getUser(ctx.params.id)
  }
}

container.register({
  // Here we are telling Awilix how to resolve a
  // userController: by instantiating a class.
  userController: awilix.asClass(UserController),
})

// Let's try with a factory function.
const makeUserService = ({ db }) => {
  // Notice how we can use destructuring
  // to access dependencies
  return {
    getUser: (id) => {
      return db.query(`select * from users where id=${id}`)
    },
  }
}

container.register({
  // the `userService` is resolved by
  // invoking the function.
  userService: awilix.asFunction(makeUserService),
})

// Alright, now we need a database.
// Let's make that a constructor function.
// Notice how the dependency is referenced by name
// directly instead of destructuring an object.
// This is because we register it in "CLASSIC"
// injection mode below.
function Database(connectionString, timeout) {
  // We can inject plain values as well!
  this.conn = connectToYourDatabaseSomehow(connectionString, timeout)
}

Database.prototype.query = function (sql) {
  // blah....
  return this.conn.rawSql(sql)
}

// We use register coupled with asClass to tell Awilix to
// use `new Database(...)` instead of just `Database(...)`.
// We also want to use `CLASSIC` injection mode for this
// registration. Read more about injection modes below.
container.register({
  db: awilix.asClass(Database).classic(),
})

// Lastly we register the connection string and timeout values
// as we need them in the Database constructor.
container.register({
  // We can register things as-is - this is not just
  // limited to strings and numbers, it can be anything,
  // really - they will be passed through directly.
  connectionString: awilix.asValue(process.env.CONN_STR),
  timeout: awilix.asValue(1000),
})

// We have now wired everything up!
// Let's use it! (use your imagination with the router thing..)
router.get('/api/users/:id', container.resolve('userController').getUser)

// Alternatively, using the `cradle` proxy..
router.get('/api/users/:id', container.cradle.userController.getUser)

// Using  `container.cradle.userController` is actually the same as calling
// `container.resolve('userController')` - the cradle is our proxy!

That example is rather lengthy, but if you extract things to their proper files it becomes more manageable.

Check out a working Koa example!

Lifetime management

Awilix supports managing the lifetime of instances. This means that you can control whether objects are resolved and used once, cached within a certain scope, or cached for the lifetime of the process.

There are 3 lifetime types available.

  • Lifetime.TRANSIENT: This is the default. The registration is resolved every time it is needed. This means if you resolve a class more than once, you will get back a new instance every time.
  • Lifetime.SCOPED: The registration is scoped to the container - that means that the resolved value will be reused when resolved from the same scope (or a child scope).
  • Lifetime.SINGLETON: The registration is always reused no matter what - that means that the resolved value is cached in the root container.

They are exposed on the awilix.Lifetime object.

const Lifetime = awilix.Lifetime

To register a module with a specific lifetime:

const { asClass, asFunction, asValue } = awilix

class MailService {}

container.register({
  mailService: asClass(MailService, { lifetime: Lifetime.SINGLETON }),
})

// or using the chaining configuration API..
container.register({
  mailService: asClass(MailService).setLifetime(Lifetime.SINGLETON),
})

// or..
container.register({
  mailService: asClass(MailService).singleton(),
})

// or.......
container.register('mailService', asClass(MailService, { lifetime: SINGLETON }))

Scoped lifetime

In web applications, managing state without depending too much on the web framework can get difficult. Having to pass tons of information into every function just to make the right choices based on the authenticated user.

Scoped lifetime in Awilix makes this simple - and fun!

const { createContainer, asClass, asValue } = awilix
const container = createContainer()

class MessageService {
  constructor({ currentUser }) {
    this.user = currentUser
  }

  getMessages() {
    const id = this.user.id
    // wee!
  }
}

container.register({
  messageService: asClass(MessageService).scoped(),
})

// imagine middleware in some web framework..
app.use((req, res, next) => {
  // create a scoped container
  req.scope = container.createScope()

  // register some request-specific data..
  req.scope.register({
    currentUser: asValue(req.user),
  })

  next()
})

app.get('/messages', (req, res) => {
  // for each request we get a new message service!
  const messageService = req.scope.resolve('messageService')
  messageService.getMessages().then((messages) => {
    res.send(200, messages)
  })
})

// The message service can now be tested
// without depending on any request data!

IMPORTANT! If a singleton is resolved, and it depends on a scoped or transient registration, those will remain in the singleton for its lifetime! Similarly, if a scoped module is resolved, and it depends on a transient registration, that remains in the scoped module for its lifetime. In the example above, if messageService was a singleton, it would be cached in the root container, and would always have the currentUser from the first request. Modules should generally not have a longer lifetime than their dependencies, as this can cause issues of stale data.

const makePrintTime =
  ({ time }) =>
  () => {
    console.log('Time:', time)
  }

const getTime = () => new Date().toString()

container.register({
  printTime: asFunction(makePrintTime).singleton(),
  time: asFunction(getTime).transient(),
})

// Resolving `time` 2 times will
// invoke `getTime` 2 times.
container.resolve('time')
container.resolve('time')

// These will print the same timestamp at all times,
// because `printTime` is singleton and
// `getTime` was invoked when making the singleton.
container.resolve('printTime')()
container.resolve('printTime')()

If you want a mismatched configuration like this to error, set strict in the container options. This will trigger the following error at runtime when the singleton printTime is resolved: AwilixResolutionError: Could not resolve 'time'. Dependency 'time' has a shorter lifetime than its ancestor: 'printTime'

In addition, registering a singleton on a scope other than the root container results in unpredictable behavior. In particular, if two different singletons are registered on two different scopes, they will share a cache entry and collide with each other. To throw a runtime error when a singleton is registered on a scope other than the root container, enable strict mode.

Read the documentation for container.createScope() for more examples.

Strict mode

Strict mode is a new feature in Awilix 10. It enables additional correctness checks that can help you catch bugs early.

In particular, strict mode enables the following checks:

  • When a singleton or scoped registration depends on a transient non-value registration, an error is thrown. This detects and prevents the issue where a shorter lifetime dependency can leak outside its intended lifetime due to its preservation in a longer lifetime module.
  • Singleton registrations on any scopes are disabled. This prevents the issue where a singleton is registered on a scope other than the root container, which results in unpredictable behavior.
  • Singleton resolution is performed using registrations from the root container only, which prevents potential leaks in which scoped registrations are preserved in singletons.

Injection modes

The injection mode determines how a function/constructor receives its dependencies. Pre-2.3.0, only one mode was supported - PROXY - which remains the default mode.

Awilix v2.3.0 introduced an alternative injection mode: CLASSIC. The injection modes are available on awilix.InjectionMode

  • InjectionMode.PROXY (default): Injects a proxy to functions/constructors which looks like a regular object.

    class UserService {
      constructor(opts) {
        this.emailService = opts.emailService
        this.logger = opts.logger
      }
    }

    or with destructuring:

    class UserService {
      constructor({ emailService, logger }) {
        this.emailService = emailService
        this.logger = logger
      }
    }
  • InjectionMode.CLASSIC: Parses the function/constructor parameters, and matches them with registrations in the container. CLASSIC mode has a slightly higher initialization cost as it has to parse the function/class to figure out the dependencies at the time of registration, however resolving them will be much faster than when using PROXY. Don't use CLASSIC if you minify your code! We recommend using CLASSIC in Node and PROXY in environments where minification is needed.

    class UserService {
      constructor(emailService, logger) {
        this.emailService = emailService
        this.logger = logger
      }
    }

    Additionally, if the class has a base class but does not declare a constructor of its own, Awilix simply invokes the base constructor with whatever dependencies it requires.

    class Car {
      constructor(engine) {
        this.engine = engine
      }
    }
    
    class Porsche extends Car {
      vroom() {
        console.log(this.engine) // whatever "engine" is
      }
    }

Injection modes can be set per-container and per-resolver. The most specific one wins.

Note: I personally don't see why you would want to have different injection modes in a project, but if the need arises, Awilix supports it.

Container-wide:

const { createContainer, InjectionMode } = require('awilix')

const container = createContainer({ injectionMode: InjectionMode.CLASSIC })

Per resolver:

const container = createContainer()

container.register({
  logger: asClass(Logger).classic(),
  // or..
  emailService: asFunction(makeEmailService).proxy()
  // or..
  notificationService: asClass(NotificationService).setInjectionMode(InjectionMode.CLASSIC)
})

// or..
container.register({
  logger: asClass(Logger, { injectionMode: InjectionMode.CLASSIC })
})

For auto-loading modules:

const container = createContainer()
container.loadModules(['services/**/*.js', 'repositories/**/*.js'], {
  resolverOptions: {
    injectionMode: InjectionMode.CLASSIC,
  },
})

Choose whichever fits your style.

  • PROXY technically allows you to defer pulling dependencies (for circular dependency support), but this isn't recommended.
  • CLASSIC feels more like the DI you're used to in other languages.
  • PROXY is more descriptive, and makes for more readable tests; when unit testing your classes/functions without using Awilix, you don't have to worry about parameter ordering like you would with CLASSIC.
  • Performance-wise, CLASSIC is slightly faster because it only reads the dependencies from the constructor/function once (when asClass/asFunction is called), whereas accessing dependencies on the Proxy may incur slight overhead for each resolve.
  • CLASSIC will not work when your code is minified! It reads the function signature to determine what dependencies to inject. Minifiers will usually mangle these names.

Here's an example outlining the testability points raised.

// CLASSIC
function database(connectionString, timeout, logger) {
  // ...
}

// Shorter, but less readable, order-sensitive
const db = database('localhost:1337;user=123...', 4000, new LoggerMock())

// PROXY
function database({ connectionString, timeout, logger }) {
  // ...
}

// Longer, more readable, order does not matter
const db = database({
  logger: new LoggerMock(),
  timeout: 4000,
  connectionString: 'localhost:1337;user=123...',
})

Auto-loading modules

When you have created your container, registering 100's of classes can get boring. You can automate this by using loadModules.

Important: auto-loading looks at a file's default export, which can be:

  • module.exports = ...
  • module.exports.default = ...
  • export default ...

To load a non-default export, set the [RESOLVER] property on it:

const { RESOLVER } = require('awilix')
export class ServiceClass {}
ServiceClass[RESOLVER] = {}

Or even more concise using TypeScript:

// TypeScript
import { RESOLVER } from 'awilix'
export class ServiceClass {
  static [RESOLVER] = {}
}

Note that multiple services can be registered per file, i.e. it is possible to have a file with a default export and named exports and for all of them to be loaded. The named exports do require the RESOLVER token to be recognized.

Imagine this app structure:

  • app
    • services
      • UserService.js - exports an ES6 class UserService {}
      • emailService.js - exports a factory function function makeEmailService() {}
    • repositories
      • UserRepository.js - exports an ES6 class UserRepository {}
    • index.js - our main script

In our main script we would do the following:

const awilix = require('awilix')

const container = awilix.createContainer()

// Load our modules!
container.loadModules(
  [
    // Globs!
    [
      // To have different resolverOptions for specific modules.
      'models/**/*.js',
      {
        register: awilix.asValue,
        lifetime: Lifetime.SINGLETON,
      },
    ],
    'services/**/*.js',
    'repositories/**/*.js',
  ],
  {
    // We want to register `UserService` as `userService` -
    // by default loaded modules are registered with the
    // name of the file (minus the extension)
    formatName: 'camelCase',
    // Apply resolver options to all modules.
    resolverOptions: {
      // We can give these auto-loaded modules
      // the deal of a lifetime! (see what I did there?)
      // By default it's `TRANSIENT`.
      lifetime: Lifetime.SINGLETON,
      // We can tell Awilix what to register everything as,
      // instead of guessing. If omitted, will inspect the
      // module to determine what to register as.
      register: awilix.asClass,
    },
  },
)

// We are now ready! We now have a userService, userRepository and emailService!
container.resolve('userService').getUser(1)

Important: Auto-loading relies on glob and therefore does not work with bundlers like Webpack, Rollup and Browserify.

Per-module local injections

Some modules might need some additional configuration values than just dependencies.

For example, our userRepository wants a db module which is registered with the container, but it also wants a timeout value. timeout is a very generic name and we don't want to register that as a value that can be accessed by all modules in the container (maybe other modules have a different timeout?)

export default function userRepository({ db, timeout }) {
  return {
    find() {
      return Promise.race([
        db.query('select * from users'),
        Promise.delay(timeout).then(() =>
          Promise.reject(new Error('Timed out')),
        ),
      ])
    },
  }
}

Awilix 2.5 added per-module local injections. The following snippet contains all the possible ways to set this up.

import { createContainer, Lifetime, asFunction } from 'awilix'
import createUserRepository from './repositories/userRepository'

const container = createContainer()
  // Using the fluid variant:
  .register({
    userRepository: asFunction(createUserRepository)
      // Provide an injection function that returns an object with locals.
      // The function is called once per resolve of the registration
      // it is attached to.
      .inject(() => ({ timeout: 2000 })),
  })

  // Shorthand variants
  .register({
    userRepository: asFunction(createUserRepository, {
      injector: () => ({ timeout: 2000 }),
    }),
  })

  // Stringly-typed shorthand
  .register(
    'userRepository',
    asFunction(createUserRepository, {
      injector: () => ({ timeout: 2000 }),
    }),
  )

  // with `loadModules`
  .loadModules([['repositories/*.js', { injector: () => ({ timeout: 2000 }) }]])

Now timeout is only available to the modules it was configured for.

IMPORTANT: the way this works is by wrapping the cradle in another proxy that provides the returned values from the inject function. This means if you pass along the injected cradle object, anything with access to it can access the local injections.

Inlining resolver options

Awilix 2.8 added support for inline resolver options. This is best explained with an example.

services/awesome-service.js:

import { RESOLVER, Lifetime, InjectionMode } from 'awilix'

export default class AwesomeService {
  constructor(awesomeRepository) {
    this.awesomeRepository = awesomeRepository
  }
}

// `RESOLVER` is a Symbol.
AwesomeService[RESOLVER] = {
  lifetime: Lifetime.SCOPED,
  injectionMode: InjectionMode.CLASSIC,
}

index.js:

import { createContainer, asClass } from 'awilix'
import AwesomeService from './services/awesome-service.js'

const container = createContainer().register({
  awesomeService: asClass(AwesomeService),
})

console.log(container.registrations.awesomeService.lifetime) // 'SCOPED'
console.log(container.registrations.awesomeService.injectionMode) // 'CLASSIC'

Additionally, if we add a name field and use loadModules, the name is used for registration (ignoring formatName if provided).

// `RESOLVER` is a Symbol.
AwesomeService[RESOLVER] = {
+ name: 'superService',
  lifetime: Lifetime.SCOPED,
  injectionMode: InjectionMode.CLASSIC
}
const container = createContainer().loadModules(['services/*.js'])
console.log(container.registrations.superService.lifetime) // 'SCOPED'
console.log(container.registrations.superService.injectionMode) // 'CLASSIC'

Important: the name field is only used by loadModules.

Disposing

As of Awilix v3.0, you can call container.dispose() to clear the resolver cache and call any registered disposers. This is very useful to properly dispose resources like connection pools, and especially when using watch-mode in your integration tests.

For example, database connection libraries usually have some sort of destroy or end function to close the connection. You can tell Awilix to call these for you when calling container.dispose().

Important: the container being disposed will not dispose its' scopes. It only disposes values in it's own cache.

import { createContainer, asClass } from 'awilix'
import pg from 'pg'

class TodoStore {
  constructor({ pool }) {
    this.pool = pool
  }

  async getTodos() {
    const result = await this.pool.query('SELECT * FROM todos')
    return result.rows
  }
}

function configureContainer() {
  return container.register({
    todoStore: asClass(TodoStore),
    pool: asFunction(() => new pg.Pool())
      // Disposables must be either `scoped` or `singleton`.
      .singleton()
      // This is called when the pool is going to be disposed.
      // If it returns a Promise, it will be awaited by `dispose`.
      .disposer((pool) => pool.end()),
  })
}

const container = configureContainer()
const todoStore = container.resolve('todoStore')

// Later...
container.dispose().then(() => {
  console.log('Container has been disposed!')
})

A perfect use case for this would be when using Awilix with an HTTP server.

import express from 'express'
import http from 'http'

function createServer() {
  const app = express()
  const container = configureContainer()
  app.get('/todos', async (req, res) => {
    const store = container.resolve('todoStore')
    const todos = await store.getTodos()
    res.status(200).json(todos)
  })

  const server = http.createServer(app)
  // Dispose container when the server closes.
  server.on('close', () => container.dispose())
  return server
}

test('server does server things', async () => {
  const server = createServer()
  server.listen(3000)

  /// .. run your tests..

  // Disposes everything, and your process no
  // longer hangs on to zombie connections!
  server.close()
})

API

The awilix object

When importing awilix, you get the following top-level API:

  • createContainer
  • listModules
  • AwilixResolutionError
  • asValue
  • asFunction
  • asClass
  • aliasTo
  • Lifetime - documented above.
  • InjectionMode - documented above.

These are documented below.

Resolver options

Whenever you see a place where you can pass in resolver options, you can pass in an object with the following props:

  • lifetime: An awilix.Lifetime.* string, such as awilix.Lifetime.SCOPED
  • injectionMode: An awilix.InjectionMode.* string, such as awilix.InjectionMode.CLASSIC
  • injector: An injector function - see Per-module local injections
  • register: Only used in loadModules, determines how to register a loaded module explicitly
  • isLeakSafe: true if this resolver should be excluded from lifetime-leak checking performed in strict mode. Defaults to false.

Examples of usage:

container.register({
  stuff: asClass(MyClass, { injectionMode: InjectionMode.CLASSIC }),
})

container.loadModules([['some/path/to/*.js', { register: asClass }]], {
  resolverOptions: {
    lifetime: Lifetime.SCOPED,
  },
})

createContainer()

Creates a new Awilix container. The container stuff is documented further down.

Args:

  • options: Options object. Optional.
    • options.require: The function to use when requiring modules. Defaults to require. Useful when using something like require-stack. Optional.
    • options.injectionMode: Determines the method for resolving dependencies. Valid modes are:
      • PROXY: Uses the awilix default dependency resolution mechanism (I.E. injects the cradle into the function or class). This is the default injection mode.
      • CLASSIC: Uses the named dependency resolution mechanism. Dependencies must be named exactly like they are in the registration. For example, a dependency registered as repository cannot be referenced in a class constructor as repo.
    • options.strict: Enables strict mode. Defaults to false.

asFunction()

Used with container.register({ userService: asFunction(makeUserService) }). Tells Awilix to invoke the function without any context.

The returned resolver has the following chainable (fluid) API:

  • asFunction(fn).setLifetime(lifetime: string): sets the lifetime of the registration to the given value.
  • asFunction(fn).transient(): same as asFunction(fn).setLifetime(Lifetime.TRANSIENT).
  • asFunction(fn).scoped(): same as asFunction(fn).setLifetime(Lifetime.SCOPED).
  • asFunction(fn).singleton(): same as asFunction(fn).setLifetime(Lifetime.SINGLETON).
  • asFunction(fn).inject(injector: Function): Let's you provide local dependencies only available to this module. The injector gets the container passed as the first and only argument and should return an object.

asClass()

Used with container.register({ userService: asClass(UserService) }). Tells Awilix to instantiate the given function as a class using new.

The returned resolver has the same chainable API as asFunction.

asValue()

Used with container.register({ dbHost: asValue('localhost') }). Tells Awilix to provide the given value as-is.

aliasTo()

Resolves the dependency specified.

container.register({
  val: asValue(123),
  aliasVal: aliasTo('val'),
})

container.resolve('aliasVal') === container.resolve('val')

listModules()

Returns an array of {name, path} pairs, where the name is the module name, and path is the actual full path to the module.

This is used internally, but is useful for other things as well, e.g. dynamically loading an api folder.

Args:

  • globPatterns: a glob pattern string, or an array of them.
  • opts.cwd: The current working directory passed to glob. Defaults to process.cwd().
  • returns: an array of objects with:
    • name: The module name - e.g. db
    • path: The path to the module relative to options.cwd - e.g. lib/db.js

Example:

const listModules = require('awilix').listModules

const result = listModules(['services/*.js'])

console.log(result)
// << [{ name: 'someService', path: 'path/to/services/someService.js' }]

Important: listModules relies on glob and therefore is not supported with bundlers like Webpack, Rollup and Browserify.

AwilixResolutionError

This is a special error thrown when Awilix is unable to resolve all dependencies (due to missing or cyclic dependencies). You can catch this error and use err instanceof AwilixResolutionError if you wish. It will tell you what dependencies it could not find or which ones caused a cycle.

AwilixRegistrationError

This is a special error thrown when Awilix is unable to register a dependency due to a strict mode violation. You can catch this error and use err instanceof AwilixRegistrationError if you wish.

The AwilixContainer object

The container returned from createContainer has some methods and properties.

container.cradle

Behold! This is where the magic happens! The cradle is a proxy, and all getters will trigger a container.resolve. The cradle is actually being passed to the constructor/factory function, which is how everything gets wired up.

container.registrations

A read-only getter that returns the internal registrations. When invoked on a scope, will show registrations for it's parent, and it's parent's parent, and so on.

Not really useful for public use.

container.cache

A Map<string, CacheEntry> used internally for caching resolutions. Not meant for public use but if you find it useful, go ahead but tread carefully.

Each scope has it's own cache, and checks the cache of it's ancestors.

let counter = 1
container.register({
  count: asFunction(() => counter++).singleton(),
})

container.cradle.count === 1
container.cradle.count === 1

container.cache.delete('count')
container.cradle.count === 2

container.options

Options passed to createContainer are stored here.

const container = createContainer({
  injectionMode: InjectionMode.CLASSIC,
})

console.log(container.options.injectionMode) // 'CLASSIC'

container.resolve()

Resolves the registration with the given name. Used by the cradle.

Signature

  • resolve<T>(name: string, [resolveOpts: ResolveOptions]): T
container.register({
  leet: asFunction(() => 1337),
})

container.resolve('leet') === 1337
container.cradle.leet === 1337

The optional resolveOpts has the following fields:

  • allowUnregistered: if true, returns undefined when the dependency does not exist, instead of throwing an error.

container.register()

Signatures

  • register(name: string, resolver: Resolver): AwilixContainer
  • register(nameAndResolverPair: NameAndResolverPair): AwilixContainer

Awilix needs to know how to resolve the modules, so let's pull out the resolver functions:

const awilix = require('awilix')
const { asValue, asFunction, asClass } = awilix
  • asValue: Resolves the given value as-is.
  • asFunction: Resolve by invoking the function with the container cradle as the first and only argument.
  • asClass: Like asFunction but uses new.

Now we need to use them. There are multiple syntaxes for the register function, pick the one you like the most - or use all of them, I don't really care! 😎

Both styles supports chaining! register returns the container!

// name-resolver
container.register('connectionString', asValue('localhost:1433;user=...'))
container.register('mailService', asFunction(makeMailService))
container.register('context', asClass(SessionContext))

// object
container.register({
  connectionString: asValue('localhost:1433;user=...'),
  mailService: asFunction(makeMailService, { lifetime: Lifetime.SINGLETON }),
  context: asClass(SessionContext, { lifetime: Lifetime.SCOPED }),
})

// `asClass` and `asFunction` also supports a fluid syntax.
// This...
container.register(
  'mailService',
  asFunction(makeMailService).setLifetime(Lifetime.SINGLETON),
)
// .. is the same as this:
container.register('context', asClass(SessionContext).singleton())

// .. and here are the other `Lifetime` variants as fluid functions.
container.register('context', asClass(SessionContext).transient())
container.register('context', asClass(SessionContext).scoped())

The object syntax, key-value syntax and chaining are valid for all register calls!

container.hasRegistration()

  • container.hasRegistration(name: string | symbol): boolean

Determines if the container has a registration with the given name. Also checks ancestor containers.

container.loadModules()

Given an array of globs, registers the modules and returns the container.

💡 When using opts.esModules, a Promise is returned due to using the asynchronous import().

Awilix will use require on the loaded modules, and register the default-exported function or class as the name of the file.

This uses a heuristic to determine if it's a constructor function (function Database() {...}); if the function name starts with a capital letter, it will be newed!

Args:

  • globPatterns: Array of glob patterns that match JS files to load.
  • opts.cwd: The cwd being passed to glob. Defaults to process.cwd().
  • opts.formatName: Can be either 'camelCase', or a function that takes the current name as the first parameter and returns the new name. Default is to pass the name through as-is. The 2nd parameter is a full module descriptor.
  • opts.resolverOptions: An object passed to the resolvers. Used to configure the lifetime, injection mode and more of the loaded modules.
  • opts.esModules: Loads modules using Node's native ES modules. This makes container.loadModules asynchronous, and will therefore return a Promise! This is only supported on Node 14.0+ and should only be used if you're using the Native Node ES modules

Example:

// index.js
container.loadModules(['services/*.js', 'repositories/*.js', 'db/db.js'])

container.cradle.userService.getUser(123)

// to configure lifetime for all modules loaded..
container.loadModules([
  'services/*.js',
  'repositories/*.js',
  'db/db.js'
], {
  resolverOptions: {
    lifetime: Lifetime.SINGLETON
  }
})

container.cradle.userService.getUser(123)

// to configure lifetime for specific globs..
container.loadModules([
  ['services/*.js', Lifetime.SCOPED], // all services will have scoped lifetime
  'repositories/*.js',
  'db/db.js'
], {
  resolverOptions: {
    lifetime: Lifetime.SINGLETON // db and repositories will be singleton
  }
)

container.cradle.userService.getUser(123)

// to use camelCase for modules where filenames are not camelCase
container.loadModules(['repositories/account-repository.js', 'db/db.js'], {
  formatName: 'camelCase'
})

container.cradle.accountRepository.getUser(123)

// to customize how modules are named in the container (and for injection)
container.loadModules(['repository/account.js', 'service/email.js'], {
  // This formats the module name so `repository/account.js` becomes `accountRepository`
  formatName: (name, descriptor) => {
    const splat = descriptor.path.split('/')
    const namespace = splat[splat.length - 2] // `repository` or `service`
    const upperNamespace =
      namespace.charAt(0).toUpperCase() + namespace.substring(1)
    return name + upperNamespace
  }
})

container.cradle.accountRepository.getUser(123)
container.cradle.emailService.sendEmail('[email protected]', 'waddup')

The ['glob', Lifetime.SCOPED] syntax is a shorthand for passing in resolver options like so: ['glob', { lifetime: Lifetime.SCOPED }]

Important: loadModules depends on fast-glob and is therefore not supported in module bundlers like Webpack, Rollup, esbuild and Browserify.

container.createScope()

Creates a new scope. All registrations with a Lifetime.SCOPED will be cached inside a scope. A scope is basically a "child" container.

  • returns AwilixContainer
// Increments the counter every time it is resolved.
let counter = 1
container.register({
  counterValue: asFunction(() => counter++).scoped(),
})
const scope1 = container.createScope()
const scope2 = container.createScope()

const scope1Child = scope1.createScope()

scope1.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope2.cradle.counterValue === 2
scope2.cradle.counterValue === 2

scope1Child.cradle.counterValue === 3

A Scope maintains it's own cache of Lifetime.SCOPED registrations, meaning it does not use the parent's cache for scoped registrations.

let counter = 1
container.register({
  counterValue: asFunction(() => counter++).scoped(),
})
const scope1 = container.createScope()
const scope2 = container.createScope()

// The root container is also a scope.
container.cradle.counterValue === 1
container.cradle.counterValue === 1

// This scope resolves and caches it's own.
scope1.cradle.counterValue === 2
scope1.cradle.counterValue === 2

// This scope resolves and caches it's own.
scope2.cradle.counterValue === 3
scope2.cradle.counterValue === 3

A scope may also register additional stuff - they will only be available within that scope and it's children.

// Register a transient function
// that returns the value of the scope-provided dependency.
// For this example we could also use scoped lifetime.
container.register({
  scopedValue: asFunction((cradle) => 'Hello ' + cradle.someValue),
})

// Create a scope and register a value.
const scope = container.createScope()
scope.register({
  someValue: asValue('scope'),
})

scope.cradle.scopedValue === 'Hello scope'
container.cradle.someValue
// throws AwilixResolutionException
// because the root container does not know
// of the resolver.

Things registered in the scope take precedence over registrations in the parent scope(s). This applies to both the registration directly requested from the scope container, and any dependencies that the registration uses.

// It does not matter when the scope is created,
// it will still have anything that is registered
// in its parent.
const scope = container.createScope()

container.register({
  value: asValue('root'),
  usedValue: asFunction((cradle) => `hello from ${cradle.value}`),
})

scope.register({
  value: asValue('scope'),
})

container.cradle.value === 'root'
scope.cradle.value === 'scope'
container.cradle.usedValue === 'hello from root'
scope.cradle.usedValue === 'hello from scope'

Registering singletons in a scope results in unpredictable behavior and should be avoided. Having more than one singleton with the same name in different scopes will result in them sharing a cache entry and colliding with each other. To disallow such registrations, enable strict mode in the container options.

container.build()

Builds an instance of a class (or a function) by injecting dependencies, but without registering it in the container.

It's basically a shortcut for asClass(MyClass).resolve(container).

Args:

  • targetOrResolver: A class, function or resolver (example: asClass(..), asFunction(..))
  • opts: Resolver options.

Returns an instance of whatever is passed in, or the result of calling the resolver.

Important: if you are doing this often for the same class/function, consider using the explicit approach and save the resolver, especially if you are using classic resolution because it scans the class constructor/function when calling asClass(Class) / asFunction(func).

// The following are equivelant..
class MyClass {
  constructor({ ping }) {
    this.ping = ping
  }

  pong() {
    return this.ping
  }
}

const createMyFunc = ({ ping }) => ({
  pong: () => ping,
})

container.register({
  ping: asValue('pong'),
})

// Shorthand
// This uses `utils.isClass()` to determine whether to
// use `asClass` or `asFunction`. This is fine for
// one-time resolutions.
const myClass = container.build(MyClass)
const myFunc = container.build(createMyFunc)

// Explicit
// Save the resolver if you are planning on invoking often.
// **Especially** if you're using classic resolution.
const myClassResolver = asClass(MyClass)
const myFuncResolver = asFunction(MyFunc)

const myClass = container.build(myClassResolver)
const myFunc = container.build(myFuncResolver)

container.dispose()

Returns a Promise that resolves when all disposers of cached resolutions have resolved. Only cached values will be disposed, meaning they must have a Lifetime of SCOPED or SINGLETON, or else they are not cached by the container and therefore can't be disposed by it.

This also clears the container's cache.

const pg = require('pg')

container.register({
  pool: asFunction(() => new pg.Pool())
    .disposer((pool) => pool.end())
    // IMPORTANT! Must be either singleton or scoped!
    .singleton(),
})

const pool = container.resolve('pool')
pool.query('...')

// Later..
container.dispose().then(() => {
  console.log('All dependencies disposed, you can exit now. :)')
})

Universal Module (Browser Support)

As of v3, Awilix ships with official support for browser environments!

The package includes 4 flavors.

  • CommonJS, the good ol' Node format - lib/awilix.js
  • ES Modules, for use with module bundlers in Node - lib/awilix.module.mjs
  • ES Modules, for use with module bundlers in the browser - lib/awilix.browser.js
  • UMD, for dropping it into a script tag - lib/awilix.umd.js

The package.json includes the proper fields for bundlers like Webpack, Rollup and Browserify to pick the correct version, so you should not have to configure anything. 😎

Important: the browser builds do not support loadModules or listModules, because they depend on Node-specific packages.

Also important: due to using Proxy + various Reflect methods, Awilix is only supposed to work in:

  • Chrome >= 49
  • Firefox >= 18
  • Edge >= 12
  • Opera >= 36
  • Safari >= 10
  • Internet Explorer is not supported

Ecosystem

Contributing

Please see our contributing.md

What's in a name?

Awilix is the mayan goddess of the moon, and also my favorite character in the game SMITE.

Author

Jeff Hansen - @Jeffijoe

awilix's People

Contributors

ackos95 avatar andersonleite avatar andyfleming avatar astephens25 avatar berndartmueller avatar blove avatar cjhoward92 avatar dboune avatar ddproxy avatar dependabot[bot] avatar eithermonad avatar farhantahir avatar faustbrian avatar fnimick avatar jeffijoe avatar khoahuynhdev avatar kibertoad avatar leitea-google avatar moltar avatar mrzli avatar mvayngrib avatar neerfri avatar piotrblachnio avatar razor-x avatar richardsimko avatar seldszar avatar wellloy1 avatar ycros avatar zb-sj avatar zer0tonin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

awilix's Issues

Official browser build

With Rollup we can use some tricks to strip out the Node-specific stuff, as well as provide a format for bundlers like Webpack, as well as a UMD build.

awilix-koa 1.0.3 doesn't work with awilix 3.0.0-rc.3 - typescript issue

I'm getting below error trying to compile code. There are some typings missing or I'm using incompatible versions?

node_modules/awilix-koa/lib/invokers.d.ts(1,10): error TS2305: Module '"node_modules/awilix/lib/awilix"' has no exported member 'Registration'.
node_modules/awilix-koa/lib/invokers.d.ts(1,24): error TS2305: Module '"node_modules/awilix/lib/awilix"' has no exported member 'RegistrationOptions'.

package.json

    "awilix": "^3.0.0-rc.3",
    "awilix-koa": "^1.0.3",
    "typescript": "2.6.1"

Container configuration

import { asFunction, asValue, createContainer } from 'awilix'
import { config } from './config'
import { run } from './some/someController'

export const configureContainer = () => {
    const container = createContainer()

    container.register({
        config: asValue(config),
        someController: asFunction(run),
    })

    return container
}

Using container in koa app

import { scopePerRequest } from 'awilix-koa'

const container = configureContainer()

app
    .use(scopePerRequest(container))

As as side note I had to manually add "@types/glob": "^5.0.33", into dev dependencies - it seems awilix is not pulling it as dependency an TS is whining about it.

Please let me know if you need any additional information.

Instantiate two objects of the same class

From within a component that is getting a dependency injected, how can I instantiate multiple objects of that dep?
Basically, what should I write into Other.prototype.createTwoUsers body?

Example files:
File: user.js

'use strict';
module.exports = User;

function User({ log }) {
  this.id = Math.random();  
  this.logger = log;
}

User.prototype.testStuff = function() {
  this.logger.info(`testStuff(): ${this.id}`);
}

File: other.js

'use strict';

module.exports = Other

function Other({ user }) {
  this.user = user;
};

Other.prototype.createTwoUsers = function() {
  ----> what should I do here? <-----
}

File: log.js

'use strict';

module.exports = Log

function Log({}) {};

Log.prototype.info = function(message) {
  console.log(message);
}

File: container.js

'use strict';

const { createContainer, Lifetime } = require('awilix');
const container = createContainer();

// Load modules
const log = require('./log');
const user = require('./user');
const other = require('./other');
container.registerClass({
  log: log,
  user: user,
  other: other
});

module.exports = container;

File: entrypoint.js

'use strict';
const container = require('./library/container');
// Require modules

const other = container.resolve('other');
other.createTwoUsers();

[Feature] Inject local options per registration

The problem

The use case here is that some service may require additional options besides dependencies. For example:

export default function userRepository ({ db, timeout }) {
  return {
    find () {
      return Promise.race([
        db.query('select * from users'),
        Promise.delay(timeout).then(() => Promise.reject(new Error('Timed out')))
      ])
    }
  }
}
  • db is a registered module that can be injected anywhere. That's good.
  • timeout would also need to be registered, making it available to all registered modules. But what if different modules have different timeouts? Or if you just want to keep the timeout local to one specific module?

The solution

Providing a registration-time API for injecting locals to specific services/groups of services (using loadModules).

API proposal

import { createContainer, Lifetime, asFunction } from 'awilix'
import createUserRepository from './repositories/userRepository'

const container = createContainer()
  // Using the fluid variant:
  .register({
    userRepository: asFunction(createUserRepository)
      // provide an injection function that returns an object with locals.
      .inject(() => ({ timeout: 2000 }))
  })
  
  // Shorthand variants
  .registerFunction({
    userRepository: [createUserRepository, { injector: () => ({ timeout: 2000 }) }]
  })
  
  // Stringly-typed shorthand
  .registerFunction(
    'userRepository', 
    createUserRepository, 
    { injector: () => ({ timeout: 2000 }) }
  )
  
  // with `loadModules`
  .loadModules([
    ['repositories/*.js', { injector: () => ({ timeout: 2000 }) }]
  ])

Dumb Question: Awilix perspective on error handling?

Apologies in advance for the dumb weird question.

It seems like awilix's preferred method of catching errors is from the scope(?) of the container, correct? Or is better to create it first then catch errors whenever creating services attached to it? Or is catching errors on a (general) api level, with a generic handler be the best? I understand it's kind of depends on the project, but I'm curious what would be the best practice in the context of awilix.

Let me know if this question is unclear. Not sure if you've seen silicon valley, but I feel like Bernice asking these questions and using your amazing framework!

loadModules function doesn't work with relative paths like '../src/lib'

This is my project's structure:

project
│
└───bin
│   │   dependencies.js
│   │   server.js
│   
└───src
│   │
│   └───services
│   │   │   user.js
│   │   │   ...
│   │
│   └───lib
│       │   response-handlers.js
│       │   ...

inside bin/dependencies.js I'm trying to load my js files from services and lib:

container.loadModules([
    '../src/lib/*.js',
    '../src/services/*.js'
  ])

When I try to resolve the functions from those modules I get the following error:

AwilixResolutionError: Could not resolve 'sendValidResponse'

`alias` resolver

This will allow aliasing a registration to another.

Example:

container.register({
  permissionChecker: asClass(PermissionChecker).singleton(),
  perms: aliasTo('permissionChecker')
})

container.resolve('perms') === container.resolve('permissionChecker')

Cannot convert a Symbol value to a string

hey guys I'm getting this error when trying to create a container:

import { createContainer } from 'awilix'
container = createContainer()

Error:

/usr/src/app/node_modules/awilix/lib/createContainer.js:306
backend  │       throw err
backend  │       ^
backend  │
backend  │ TypeError: Cannot convert a Symbol value to a string
backend  │     at Array.join (native)
backend  │     at createErrorMessage (/usr/src/app/node_modules/awilix/lib/AwilixResolutionError.js:19:48)
backend  │     at AwilixResolutionError (/usr/src/app/node_modules/awilix/lib/AwilixResolutionError.js:46:11)
backend  │     at resolve (/usr/src/app/node_modules/awilix/lib/createContainer.js:252:15)
backend  │     at Object.get (/usr/src/app/node_modules/awilix/lib/createContainer.js:97:28)
backend  │     at formatValue (util.js:345:37)
backend  │     at formatProperty (util.js:794:15)
backend  │     at util.js:654:12
backend  │     at Array.map (native)
backend  │     at formatObject (util.js:653:15)
backend  │     at formatValue (util.js:592:16)
backend  │     at inspect (util.js:186:10)
backend  │     at exports.format (util.js:130:20)

I'm using node v6.9.2

resolve dependencies

Hey, good library

i want to know if there is an option or how can i resolve multiple dependencies in one call ?

example:
const {deps1, deps2, deps3} = container.resolve('deps1', 'deps2', 'deps3')

Why "Awilix", not "DI" or similar?

OK, stupid question.

This is a great framework! As coming from Java / Spring world to Node.JS - I was looking for something similar and I'm so glad I finally found this!

I bet that the reason I wasn't able to find this lib/fw in Search Engines is because of the name...I'd never follow this name to find DI framework. I know Spring is not named as "DI" or anything similar so maybe there's really no reason for it; but it's already a 10.000x times more popular than this, but to help this project grow and be used more often - why not name it something related to "DI"? Well at least "Jeff's DI" or "Awilix DI" or smth? :)

I mean I know I could rename it myself in my local code...

const di = require('awilix')

const container = di.createContainer({
  resolutionMode: di.ResolutionMode.PROXY
})

or

const ioc = require('awilix')

const container = ioc.createContainer({
  resolutionMode: ioc.ResolutionMode.PROXY
})

I mean I'd suggest to rename this to one of:

  • jeffjoe/di
  • jeffjoe/node-di ( or js-di? )
  • jeffjoe/awilix-di

Or similar. WDYT?

Immutable resolver chaining API

Currently, doing asClass().singleton().injector() modifies the same instance. I'd like to make these immutable. It shouldn't break anything but it's going in v3 anyway.

improve readme for container.listModules()

I might be an idiot, but I bet I'm not the only one. 😄 I spent hours trying to figure out why container.listModules() just refuses to work in my serverless/aws-lambda app, until it hit me – I'm bundling my app using webpack! So there's simply no files to look for and require at all!

I don't know, maybe if you added a short disclaimer that one shouldn't expect it to work like this, it would save somebody else a little time or more…

Build an instance with dependencies without registering

Proposal

Support calling a function with dependencies / instantiate a class with dependencies without having to register it.

Motivation

This would make it easier to write factories that have to return concrete implementations without being forced to inject the cradle (requiring PROXY resolution mode).

Implementation

There are 2 ways we can go about this:

  1. Add a container.build(ClassOrFunction, opts) function, detecting the type of resolver to use.
  2. Add a container.build(asClass(Class)) function, explicitly stating the resolver to use, including the ability to fluidly configure it.

Perhaps we can support both with a simple heuristic like checking if ClassOrFunction.resolve exists, in which case it's a resolver.

Electron BrowserWindow - no arg Constructor?

Awilix is pretty slick! Thank you for hard work.

I think I might be missing how injector is meant to function.

Say I have:

registry.register('electronBrowserWindow', asClass(BrowserWindow, {
        injector: () => ({ height: 600, width: 1200, backgroundColor: '#312450' }),
        lifetime: Lifetime.TRANSIENT,
        injectionMode: awilix.InjectionMode.PROXY
}));

I would expect when electronBrowserWindow is resolved it to be created with the arguments provided in injector. It would seem this is not the case.

I think I understand why - as the constructor signature to BrowserWindow has no arguments when in reality it optionally takes quite a few: https://electronjs.org/docs/api/browser-window#new-browserwindowoptions

console.log(BrowserWindow.toString());
function BrowserWindow() { [native code] }

What would be the approach to having awilix really, pretty please, cherry on top inject - the arguments provided via the injector. Maybe there already is a pattern to accomplish this use case?

loadModules error when using sequelize-cli

i have structure like this
-apps
---user
-----model.js
---container.js

code at container.js
container.loadModules([ './**/controller.js', './**/service.js', './**/model.js', ], { formatName: (name, descriptor) => { const splat = descriptor.path.split('/'); const namespace = splat[splat.length - 2]; const fileName = _.upperFirst(name); return namespace + fileName; }, registrationOptions: { lifetime: awilix.Lifetime.SINGLETON, }, });

error like this
node_modules/sequelize-cli/lib/assets/models/model.js:4 var <%= names %> = sequelize.define('<%= names %>', { ^ SyntaxError: Unexpected token <

When I rename the model name to another name it's not error.
How to solve this issue?

Proposal to enhance the DI Container with dependency injection based on name

The current DI Container works, but it is using a common anti-pattern called the Service Locator.

I was wondering your thoughts on adding a configuration option to enhance the container to read functions and classes for their dependencies based on explicit name. Something like so:

container.register({
  dep1: asFunction(makeDep1)
  dep2: asFunction(makeDep2)
})

function makeDep1() {
  function do() { }
  return { do }
}

function makeDep2(dep1) {
  function useDep1() {
    dep1.do();
  }
}

Where makeDep2 would automatically have an instance of dep1 injected into it because the container recognizes that dep1 is a registered dependency.

We could easily inspect function and constructor calls for strictly named dependencies and save the required dependencies in the registration object by name.

The advantage to this is that I will know exactly what is being used by a class or function based on the arguments. This is nice for testing, and it makes the dependencies more explicit, which is good.

The issues come into play with minification. If someone wants to minify to take advantage of V8 function inlining or something then the naming conventions would break. We could either tell the users, "Hey, minification will break your app in this configuration" or offer something like Aurelia or Angular where they have special functions and/or string based dependency names to avoid minification issues.

If you like the idea I can try and draft a legitimate proposal and potential write some prototype code to start with. I'd have no problem contributing to this container, I think it has promise!

Feature request

Hi! First of all, you did a really good job with Awilix. I like IOC and your lib is really good.
I’m currently working on a node framework, modular and I’d like to use Awilix (I don't want ti reinvent the wheel) as one of the main ingredient, using the power of container to have a robust plugins system (plugins able to declare services).
The idea would be to be able to “describe” services creation like the PHP Symfony Framework do (https://symfony.com/doc/current/service_container/definitions.html).
For example, imagine if I have a mail builder service sending emails thank to a “mailer” service, I would like to be able to describe the service like that (the “@“ prefix means it’s a reference to a service).

// services.js
services: {
    “my-mail-builder”: {
        class: “service/my-mail-builder”, 
        args: [“@mailer”, “[email protected]”]
    }
}

And why not with other features, like calls to setters, services tagging, etc…
Currently, correct me if I’m wrong, Awilix parses the final class to detect the services that need to be injected, right ? The “resolution modes”: classic or proxy.
So, the service itself is responsible for declaring every others services it depends on. It could be great to be able to declare the dependencies (the constructor args) outside the service itself. An "explicit" way. So, for the example above, we would end with something like that:

container.register({
	“my-amazing-service”: asClass(require(“service/my-amazing-service”)).withArgs(“@mailer”, “[email protected]”)
});

This way, we would be able to change the way the services are wired really easily, with config.
What do you think ?

Per-glob registration options for loadModules

Current syntax:

container.loadModules([
  'services/*.js'
], {
  registrationOptions: {
    lifetime: Lifetime.SCOPED
  }
})
container.loadModules([
  'repositories/*.js'
], {
  registrationOptions: {
    lifetime: Lifetime.SINGLETON
  }
})

Wanted syntax:

container.loadModules([
  'repositories/*.js', // will get the default
  ['services/*.js', { lifetime: Lifetime.SCOPED }],
  // or maybe even..
  ['services/*.js', Lifetime.SCOPED]
])

This is akin to Babel's plugin syntax.

Using loadModules(), when are modules actually resolved?

Hi,
I've one question about this wonderful lib:
I load most of my modules using loadModules(). All these modules are functions returning an object.
In some of these modules, I call a function to init some stuff, like this:

module.exports = ({ someDeps }) => {
    
    initStuff();

    return { /* Modules properties... */ };
}

When all modules are loaded, I'd like to call a function which needs all the initStuff() calls to be done.

container.loadModules([
    'server/services/**/!(*.spec).js'
], {
    formatName: 'camelCase',
    registrationOptions: {
        lifetime: awilix.Lifetime.SINGLETON
    }
});

doSomethingUsingStuff(); // This one needs all modules to be loaded

From my tests, doSomethingUsingStuff() is called before the modules' functions are actually called.
So, my question is: when are the modules resolved? Is it when loadModules() is called or later, when it's injected somewhere?

Hope my question makes sense :)

Thanks!

Awilix v3 Release Candidate 🚀

I've pushed [email protected] to npm which has a few breaking changes for JS users and a few more for TS users (new, better typings as v3 is now written in TypeScript).

Please try it out so I can ship it as stable ASAP! :shipit:

✨ New Features

With v3 comes a few new cool features.

Disposer support (#48)

This has been a very requested feature. The idea is you can tell Awilix how to dispose
of a dependency—for example, to close a database connection—when calling container.dispose().

const pg = require('pg')
const { createContainer, asFunction } = require('awilix')
const container = createContainer()
  .register({
    pool: (
      asFunction(() => new pg.Pool({ ... }))
        .singleton()
        .disposer((pool) => pool.end())
    )
  })

// .. later, but only if a `pool` was ever created
container.dispose().then(() => {
  console.log('One disposable connection.. disposed! Huehehehe')
})

alias resolver (#55)

This new resolver lets you alias a registration. This is best illustrated with an example:

const { alias, asValue, createContainer } = require('awilix')

const container = createContainer()

container.register({
  laughingOutLoud: asValue('hahahahaha'),
  lol: alias('laughingOutLoud')
})

container.resolve('lol') // 'hahahahaha'

It's essentially the exact same as calling container.resolve('laughingOutLoad'), but lol might be easier to type out in your constructors. 😎

Default values in constructors/functions (#46)

This is a pretty small feature but was the most difficult to land, mainly because I had to write a smarter parser and tokenizer, not to mention they are now way better at skipping over code. Check out the tests, it's pretty wild.

class MyClass {
  constructor(db, timeout = 1000) { /*...*/ }
}

container.register({
  db: asFunction(..)
})

// Look! No errors!! :D
container.build(MyClass) instanceof MyClass // true

Official support for running in the browser (#69)

Awilix now ships with 4 module flavors: CommonJS (same old), ES Modules for Node, ES Modules for the Browser and UMD.

Please see the Universal Module section in the readme for details.

🚨 Known breaking changes

The following is a list of known breaking changes. If there's any I've missed feel free to let me know.

The entire library is now written in TypeScript! (#49)

This means a bunch of interfaces have been renamed and made more correct.
If you're a TypeScript user, this is great news for you. 😄

ResolutionMode is now InjectionMode (#57)

  • ResolutionMode.js renamed to injection-mode.ts
  • ResolutionMode renamed to InjectionMode

"Registrations" are now "Resolvers" (#51)

The terminology is now "you register a resolver to a name".

  • TypeScript interfaces renamed
  • REGISTRATION symbol renamed to RESOLVER
  • registrations.js renamed to resolvers.ts
  • registrationOptions in loadModules renamed to resolverOptions

registerClass, registerFunction and registerValue removed (#60)

This was done to simplify the API surface, and also simplifies the implementation greatly (less overloads). You should be using container.register with asClass, asFunction and asValue instead.

Resolver configuration chaining API is now immutable (#62)

This simplifies the TypeScript types and is also considered a good practice. All configuration functions rely on this, meaning you should not do:

// I don't know why you would, but DONT DO THIS!
const singleton = asClass(MyClass).singleton
singleton()

However, this also means you can now "split" a resolver to configure it differently. For example:

class GenericSender {
  constructor(transport) {
    this.transport = transport
  }

  send() {
    if (this.transport === 'email') {
      // ... etc
    }
  }

  dispose() { /*...*/ }
}

const base = asClass(GenericSender).scoped().disposer((g) => g.dispose())
const emailSender = base.inject(() => ({ transport: 'email' })
const pushSender = base.inject(() => ({ transport: 'push' })

container.register({
  emailSender,
  pushSender
})

Removed AwilixNotAFunctionError in favor of a generic AwilixTypeError (#52)

This should not have an impact on userland code but I thought I'd mention it.

There are a bunch of internal uses of this error, so I thought it made sense to consolidate them into one error type.

👀 Other cool changes

  • Code is now formatted with Prettier
  • Awilix is now using husky + lint-staged to lint, format and test every commit to ensure top code quality.
  • Switched to Jest from Mocha
  • Switched from eslint to tslint
  • Rewrote the function parameter parser, it is now much better at correctly skipping over default value expressions to reach the next parameter.
  • Most (if not all) of the code is now documented and should be readable.

Please give it whirl and let me know how it goes! I'll update the changelog once stable ships.

/cc @ddproxy @cjhoward92 @blove @cikasfm @Seldszar

Per-module resolve config using loadModules

Hey there,

i love your library. Great work! :)

I don't know is something like this is possible (but it would be great, maybe i just missed it in docs) to do when using loadModules so you can setup lifetime, resolutionMode or register per imported module.

For example, something like this:

class FooService {
  // Either this solution
  static resolveConfig() {
    return {
      lifetime: awilix.Lifetime.SINGLETON,
      resolutionMode: awilix.ResolutionMode.CLASSIC
    }
  }

  barMethod() {}
}

// ... or this solution
// (probably this one because it can be applied on both functions and classes)
FooService.lifetime = awilix.Lifetime.SINGLETON;
FooService.resolutionMode: awilix.ResolutionMode.CLASSIC;

This way you would have total flexibility to setup imports when using automatic module loading.

Using Awilix autoloading together with a bundler like webpack?

Hey,

This is more of a question than an issue, but when you are using a bundler like webpack, is there still a way to do autoloading? I understand that it's not going to work, because the paths you pass to it are only valid in the source folder. Any idea how this could be resolved?

Rename `registrations` to `resolvers`

The API will still be container.register, but asClass, asFunction and asValue will be referred to as resolvers after this change.

This is a breaking change:

  • TypeScript interfaces renamed
  • REGISTRATION symbol renamed to RESOLVER
  • registrations.ts renamed to resolvers.ts
  • registrationOptions in loadModules renamed to resolverOptions

Disposer support

Proposal

Support disposing dependencies.

Each resolver (registration) can specify a disposer function to call when container.dispose() is called.

Motivation

This is useful for managing dependencies that have resources to dispose—connection pools for example.

In test runners like Jest, watch-mode will recycle worker processes which means connections may or may not have been closed between each run. In testing HTTP servers, It is a good practice to call server.close() after the tests are done.

We want to be able to run cleanup on dependencies in the same way. For example:

server.on('close', () => container.dispose())

Proposed API

  • Introduce container.dispose()
  • Introduce disposer option for asClass and asFunction resolvers
  • Disposing a container also disposes it's child (scoped) containers
  • Containers are disposed bottom-first; this means scoped containers are disposed before their parent.
  • Only SCOPED and SINGLETON registrations can be disposed as they are the only ones the container caches.

Example

const pg = require('pg')
const { createContainer, asFunction } = require('awilix')
const container = createContainer()
  .register({
    pool: (
      asFunction(() => new pg.Pool({ ... }))
        .singleton()
        .disposer((pool) => pool.end())
    )
  })

// .. later, but only if a `pool` was ever created
container.dispose().then(() => {
  console.log('One disposable connection.. disposed! Huehehehe')
})

Registering a class

Firstly, thanks for creating a great library!

I would like to know how one goes about registering a class to a container. At the moment I am getting a TypeError: Cannot read property 'constructor' of undefined error.

Here is the class I want to register:

module.exports = ({ mongoose }) => {
  const { Schema } = mongoose;

  const bugSchema = new Schema({
    id: {
      type: String,
      required: true,
      index: true,
      unique: true,
    },
    url: {
      type: String,
      required: true,
      unique: true,
    },
  });

  const Bug = mongoose.model('Bug', bugSchema);

  return Bug;
};

And here is how I'm registering it:

const Bug = require('./schemas/bug');

container.register({
  Bug: asClass(Bug),
});

Thanks

Add support for multiple registrations

I am looking to implement a collection of domain event handlers managed by a mediator object. The mediator will need to identify all event handlers for a given event and pass the event to an instance of each handler. I am already using Awilix as my IoC container, so it would be helpful if I could register all of my event handlers there. The mediator would have a reference to the container, and would resolve the event handler at runtime. For example, I suggest the following syntax.

container.register({
    mediator: asClass(Mediator)
    createdEventHandlers: [
        asClass(DatabaseEventHandler),
        asClass(MessageBusEventHandler)
    ],
    deletedEventHandler: [
        asClass(CleanUpEventHandler)
    ]
});

The resolve method would return all event handlers as an array.

const handlers = container.resolve('createdEventHandlers');
// or
const handlers = container.cradle.createdEventHandlers;

for (const handler of handlers) {
    // Send the event to the handler
}

How to inject the constructor of another module / class?

First of all, thanks for developing awilix. Apart from this issue it has been super simple to get set up and running.

Say I have the following code, prior to using awilix:

// Bar.js
const Foo = require('./Foo');

class Bar {
  buildResults(data) {
    return data.map(elem => new Foo(elem));
  }
}

module.exports = Bar;
// Foo.js
module.exports = class Foo {};

If I want to convert the project to use awilix, I would set up my container as follows:

const Foo = require('./Foo');
const Bar = require('./Bar');

const { createContainer } = require('awilix');

const container = createContainer();

container.registerClass({
  foo: Foo,
  bar: Bar,
});

module.exports = container;

But then injecting Foo into Bar as below obviously won't work:

// Bar.js
class Bar {
  constructor({ foo }) {
    this.foo = foo;
  }

  buildResults(data) {
    return data.map(elem => new this.foo(elem));
  }
}

module.exports = Bar;

Is there a way to configure awilix to do this, or is this an anti pattern that I should not be following anyway?

Different behaviour on node and babel+webpack

I made a simple test project: https://github.com/smikheiev/awilix-test

There are two classes: AppModel and AppController, AppController has dependency of AppModel. Both classes are registered in container. And when I run it in browser (built with babel and webpack) it doesn't work like it should, but with node it works like a charm.

When I run it with node AppController has correct dependency injection. Constructor argument opts is Proxy (opts [AwilixContainer.cradle]) and AppModel instance can be simply get from it (using object property or destructuring).
But when I run it with browser (babel+webpack) argument opts of AppControler constructor is arguments object (opts Arguments(1)), where Proxy is the first element and it's needed to do something like opts[0] to get AppModel instance from it.

I found that the problem in browser is with passing arguments to constructor, because if to get the model with container.cradle it's ok.

loadModules - return container?

Currently loadModules returns an array of objects that tell you what paths were loaded. However given that all other container methods (apart from resolve for obvious reasons) are chainable (returns the container itself). Should loadModules do that as well? Is the result from loadModules more useful than chaining?

@donjae @jonahx @ddproxy what do you think? 😄

[Question] How to resolve with event emiter

Hi @jeffijoe,
I'm using Hydra for my project with Awilix. Everything work fine with Awilix and Awilix-express for REST API. However, if the app is fired via hydra.getHydra().on(...), the umfHandler cannot be resolved.

module.exports = class Hydra {
  constructor (opts) {
    this.config = require('./config').HYDRA;
    this.umfHandler = umfHandler; // Resolve handler successfully
  }

  initialize () {
    return new Promise(function (resolve, reject) {
      hydra
        .init(config, function () {
          hydra.registerRoutes(routerConfig);
          hydra.getHydra().on('message', (message) => {
            this.umfHandler(message);  // Undefined
          });
        })
        .then(function () {
          resolve(hydra);
        })
        .catch(function (err) {
          reject(err);
        });
    });
  }
};

Do we have any way to resolve the function like that.

// container.js
container.loadModules(
  [
    ['../src/repository/*.js', Lifetime.SINGLETON],
    ['../src/service/*.js', Lifetime.SCOPED]
  ],
  opts
).register({
  mongoose: asFunction(
    () => require('mongoose').createConnection(config.database.mongodb)
  ).singleton().disposer((conn) => conn.close()),
  umfHandler: asFunction(
    () => require('./umf')
  ).singleton(),
  containerMiddleware: asValue(scopePerRequest(container))
});

Tracking child containers may lead to memory issue

In order for a parent container to dispose it's children, the parent needs to track the children.

This is a problem because children are usually created per request, meaning each request means an entry in the child array for the parent. With enough requests, that array will be very large.

I don't think there's a good way to deal with this, other than simply not disposing child containers which means we can avoid the child array.

Related to #48

Rewrite in TypeScript

TypeScript has gotten really good, and we already have typings (thanks to @blove for those).

This will be for v3.

Accessing container for building dependencies from modules

First of all, thanks for creating this nice and simple library. Looking at this issue (#50), I understand that in order to access the registered instances, one needs access to container. How do you recommend accessing the container from separate modules? I'd not like to pass the container instance out of band.

As an example
//index.js

 const container = createContainer();
  container.register({
    myRepository: asClass(require('./lib/sample/myRepository')).singleton(),
   logger: asClass(require('./lib/logger)).singleton()
  });

//myRepository.js

class MyRepository {
  createSample() {
     return container.build(SampleInstance); **//How do I get access to container?**

}
}

//SampleInstance.js

class SampleInstancer{
     constructor({logger}){  //**Expected to be injected by container, but it's not registered**
          this.logger = logger;
    }

}

Rename `ResolutionMode` to `InjectionMode`

I think InjectionMode makes more sense, given it decides whether to use CLASSIC injection mode or PROXY injection mode.

This is a breaking change.

  • resolution-mode.ts renamed to injection-mode.ts
  • ResolutionMode renamed to InjectionMode

[Question] Testing support

I'm using this library for developing a module which doesn't (yet) need REST APIs. I'd like to inject few of the mocked dependencies and mix it up with real dependencies (may change based on test case) to do perform integration test. The (anti?) pattern I'm using right now is to have the module responsible for creating Container to accept mocked dependencies. You have any better ideas to do this?

module.exports = ({ dependency } = {}) => {
  const container = createContainer();
  container.register({
          dependency: asValue(dependency || require('./realDependency'),
         ...........  //Few more which don't need mocking yet
        ............
  });
  return container;
};

Then, from test case only, I pass the dependency ever, but not from production code. Let me know if there is any better way to achieve this.

Add support for Asynchronous Factories

From your README.md example:

function Database({ connectionString }) {
  // We can inject plain values as well!
  this.conn = connectToYourDatabaseSomehow(connectionString);
}

This does not pass for me... 😢.
Dependency innitiations are often an asynchronous operation, especially when working with fail-fast policy. If a Peer cannot be found - I want the service to fail as soon as possible - preferably before it joins the load balancer.
For this I need my dependencies to be more eager-loaded and less lazy loaded.

Would you please consider support for

container.registerAsyncFunction( function(opts, next) )
container.regisgterPromiseFunction( function(opts) )

?

Thanks

Babel breaks loadModules when using ES2015 presets (is-class always false)

When running Babel with ES2015/stage-X presets, the container.loadModules method does not properly register ES6 classes due to how Babel transpiles them to ES5 (is-class returns false).

My workaround was to ditch ES2015 preset and go with ES2016/ES2017 and selectively choose our own plugins, as we have upgraded to Node 6.9.4. However, I can understand that some people may not have that luxury.

If this issue cannot be fixed directly (detection of transpiled classes), then it would be ideal if loadModules could take asClass/asFunction/asValue methods as hints in addition to the Lifetime.

Example:

    container.loadModules([
        ['services/**/*.js', Lifetime.SCOPED, asClass], // these are classes
        ['models/**/*.js', Lifetime.SCOPED, asValue], // these are exported as schemas
        ['routes/**/*.js', Lifetime.SINGLETON], // these are exported as factory functions
        ...
    ])

[DISCUSSION NEEDED|: simplify `register` API

At the moment, there's shortcuts registerClass, registerFunction and registerValue which map to using register with asClass, asFunction and asValue respectively.

I'm beginning to think it might be beneficial to remove them to reduce the API surface, making it harder for newcomers to use the wrong signature and getting confused by the rather large README.

Are anyone using these overloads?

If we all agree on scrapping the shortcuts, I'll try to get this into v3.

cc @ddproxy @cjhoward92 @blove @cikasfm @Seldszar

Extract registration name from function and class

It would prevent typing the names twice in several occasions:

class MyClass { /* ... */}
function myFunction(MyClass){ /* ... */ }

// AS IS
container.registerClass('MyClass', MyClass);
container.registerFunction('myFunction', myFunction);

// SUGGESTION
container.registerClass(MyClass);
container.registerFunction(myFunction);

container.register(MyClass);
container.register(myFunction);

[DISCUSSION] Change default injection mode to `CLASSIC`?

Since v2.0, Awilix has injected an ES6 Proxy which was used to resolve dependencies so we wouldn't have to use a shoddy regex to parse function signatures.

@cjhoward92 submitted PR #21 that added support for what is now referred to as CLASSIC injection mode, where—using a decent regex—the function signature was parsed to extract parameter names.

It wasn't without its issues though, for example arrow functions were not that well supported (#30), and so I wrote a basic parser to replace the regex which has worked very well.

I have personally started using CLASSIC injection more, especially in TypeScript projects, and #59 added support for default parameters, making the parser even more tolerant of real-world code.

The parser runs once per registration, so runtime performance should actually be better than using the Proxy.

What do you guys think? Which injection mode do you use in your projects?

Should we change the default to CLASSIC?

Note: this does not mean we're getting rid of PROXY, this is just about changing the default.


/cc @ddproxy @cjhoward92 @blove @cikasfm @Seldszar @niyoko @thisdotrob @mikhas-rock @leorossi @ith @Vincz @neerfri @ewrogers @dboune

Classic resolution mode fails to resolve single parameter arrow function

Awilix dependencies parser fails to detect factory function that is declared using arrow function with single parameter and no bracket enclosing the parameter.

How to reproduce:
Using CLASSIC resolution mode, declare following service. It will fail to parse the dependencies.

module.exports = config => {
    return {
        a: (b, c) => b
    };
}

But service below will work (notice brackets that enclose the parameter):

module.exports = (config) => {
    return {
        a: (b, c) => b
    };
}

I suspect this line causes the problem.

const DEPS_EXPR = /(\(\s*([^)]+?)\s*\)|\(\))/

Awilix version: 2.6.0
Node version: 8.2.1

Inject mocks for test suite

I migrated from another DI library to awilix. I migrated the codebase but now I am struggling with test suite, since I use mocks for many of the tests I am wondering how I can inject my custom modules after I register them in the container.
Is there a way to do that?

Inject dependency in koa route file

First of all congratulations for this module, really simple and powerful!
Maybe a stupid question but, how can I inject dependencies into my koa route files?
For example, I would like to inject my controller in my route, like this :

import Router from 'koa-router';
const productsRoute = ({ productController }) => {
  const router = new Router({
    prefix: '/products',
  });
  router.get('/', productController.getProducts());
  return router;
};

It's possibile with makeInvoker from awilix-koa package ?
I'm using koa 2.3.0 and koa-router 7.2.1.

Many thanks,

Damien

Class returning undefined when attempting to register

I have multiple classes registered on my container, and all has been working flawlessly so far. However, I have one class that returns the following error when attempting to register it as a class:

AwilixTypeError: asClass: expected Type to be class, but got undefined.

I cannot for the life of me figure out why this class is returning this error -- I've read through the docs multiple times and didnt spot anything -- perhaps I am missing something?

Here's the class:

export default class ServerConfig {
  _app;

  constructor({ logger, uploadsController }) {
    this._app = express();
    this.logger = logger;
    this.uploadsController = uploadsController;
    this.initGraphql();
    this.initGraphiql();
    this.initUploads();
  }

  get app() {
    return this._app;
  }

  initGraphql() {
    // implementation
  }

  initGraphiql() {
    // implementation
  }

  initUploads() {
    // implementation
  }
}

Here's my container setup:

const container = createContainer();

container.register({
  logger: asValue(logger),
  salesforceClient: asValue(salesforceGeneral),

  s3client: asClass(S3, { lifetime: Lifetime.SINGLETON }),
  uploadsController: asClass(UploadsController, { lifetime: Lifetime.SINGLETON }),

  caseResolver: asClass(CaseResolver, { lifetime: Lifetime.SINGLETON }),
  commentResolver: asClass(CommentResolver, { lifetime: Lifetime.SINGLETON }),
  uploadResolver: asClass(UploadResolver, { lifetime: Lifetime.SINGLETON }),

  salesforceService: asClass(SalesforceService, { lifetime: Lifetime.SINGLETON }),
  s3Service: asClass(S3Service, { lifetime: Lifetime.SINGLETON }),

  serverConfig: asClass(ServerConfig, { lifetime: Lifetime.SINGLETON }),
});

Again, all of the other classes and values are resolved fine, it is strictly an issue with ServerConfig. Please let me know if I can provide any additional information.

[Typescript] no exported member 'listModules'

Hi!
I would like to use Awilix in a Typescript project.
I'm trying to import the listModules function:

import {listModules} from 'awilix';

But getting an error:

src/DepsManager.ts(2,43): error TS2305: Module '"[....]/node_modules/awilix/lib/awilix"' has no exported member 'listModules'.

Is it a problem with my import or something missing in types declaration?

Thanks for your help! :)

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.