Wondering what’s next for npm?Check out our public roadmap! »

    jpex
    TypeScript icon, indicating that this package has built-in type declarations

    4.3.3 • Public • Published

    Jpex

    Easy Dependency Injection

    Build Status npm version Code Climate Test Coverage

    Jpex is an Inversion of Control framework. Register dependencies on a container, then resolve them anywhere in your application. The real magic of jpex is its ability to infer dependencies using the magic of babel and typescript...

    Contents

    Getting Started

    Install

    npm install jpex
    

    Plugin

    Jpex uses babel to infer type interfaces at build time. You can do this with one of several methods:
    @jpex-js/babel-plugin
    @jpex-js/rollup-plugin
    @jpex-js/webpack-plugin

    Jpex comes bundled with the @jpex-js/babel-plugin so you can easily get started with a .babelrc like this:

    // .bablerc
    {
      presets: [ '@babel/preset-typescript' ],
      plugins: [ 'jpex/babel-plugin' ]
    }

    Usage

    import jpex from 'jpex';
    import { Foo, Bah } from './types';
    
    jpex.factory<Foo>((bah: Bah) => bah.baz);
    
    const foo = jpex.resolve<Foo>();

    Registering Dependencies

    Services and factories are small modules or functions that provide a common piece of functionality.

    factories

    type MyFactory = {};
    
    jpex.factory<MyFactory>(() => {
      return {};
    });

    services

    class MyService = {
      method: (): any {
        // ...
      }
    };
    
    jpex.service(MyService);

    constants

    type MyConstant = string;
    jpex.constant<MyConstant>('foo');

    Consuming Dependencies

    resolve

    You can then resolve a dependency anywhere in your app:

    const value = jpex.resolve<MyFactory>();

    dependent factories

    A factory can request another dependency and jpex will resolve it on the fly:

    jpex.constant<MyConstant>('foo');
    
    jpex.factory<MyFactory>((myConstant: MyConstant) => {
      return `my constant is ${myConstant}`;
    });
    
    jpex.resolve<MyFactory>(); // "my constant is foo"

    encase

    Or you can encase a regular function so that dependencies are injected into it when called:

    const fn = jpex.encase((value: MyFactory) => (arg1, arg2) => {
      return value + arg1 + arg2;
    });
    
    fn(1, 2);

    API

    jpex

    jpex.constant

    <T>(obj: T): void

    Registers a constant value.

    jpex.factory

    <T>(fn: (...deps: any[] => T), opts?: object): void

    Registers a factory function against the given type. Jpex works out the types of deps and injects them at resolution time, then returns the resulting value T.

    type GetStuff = () => Promise<string>;
    
    jpex.factory<GetStuff>((window: Window) => () => window.fetch('/stuff));

    By default jpex will automatically resolve global types like Window or Document. In a node environment it will also be able to resolve node_modules.

    The following options can be provided for both factories and services:

    lifecycle
    'application' | 'class' | 'instance' | 'none';

    Determines how long the factory is cached for once resolved.

    • application is resolved forever across all containers
    • class is resolved for the current jpex container, if you .extend() the new container will resolve it again
    • instance if you request the same dependency multiple times in the same resolve call, this will use the same value, but the next time you call resolve it will start again
    • none never caches anything

    The default lifecycle is class

    precedence
    'active' | 'passive';

    Determines the behavior when the same factory is registered multiple times.

    • active overwrites the existing factory
    • passive prefers the existing factory

    Defaults to active

    bindToInstance
    boolean;

    Specifically for services, automatically binds all of the dependencies to the service instance.

    alias
    string | string[]

    Creates aliases for the factory. This is essentially just shorthand for writing jpex.factory(...); jpex.alias(...);

    jpex.service

    <T>(class: ClassWithConstructor, opts?: object): void

    Registers a service. A service is like a factory but instantiates a class instead.

    class Foo {
      constructor(window: Window) {
        // ...
      }
    }
    
    jpex.service(Foo);

    If a class implements an interface, you can actually use it to resolve the class:

    interface IFoo {}
    
    class Foo implements IFoo {}
    
    jpex.service(Foo);
    
    const foo = jpex.resolve<IFoo>();

    jpex.alias

    <T>(alias: string): void

    Creates an alias to another factory

    jpex.resolve

    <T>(opts?: object): T

    Locates and resolves the desired factory.

    const foo = jpex.resolve<Foo>();

    The following options can be provided for both resolve and resolveWith:

    optional
    boolean;

    When true if the dependency cannot be found or resolved, it will just return undefined rather than throwing an error.

    jpex.resolveWith

    <T, ...Rest[]>(values: Rest, opts?: object): T

    Resolves a factory while substituting dependencies for the given values

    const foo = jpex.resolveWith<Foo, Bah, Baz>(['bah', 'baz']);

    jpex.encase

    (...deps: any[]): (...args: any[]) => any

    Wraps a function and injects values into it, it then returns the inner function for use.

    const getStuff = jpex.encase((http: Http) => (thing: string) => {
      return http(`api/app/${thing}`);
    });
    
    await getStuff('my-thing');

    To help with testing, the returned function also has an encased property containng the outer function

    getStuff.encased(fakeHttp)('my-thing');

    jpex.extend

    (config?: object): Jpex

    creates a new container, using the current one as a base.

    This is useful for creating isolated contexts or not poluting the global container.

    The default behavior is to pass down all config options and factories to the new container.

    inherit

    boolean

    Whether or not to inherit config and factories from its parent

    lifecycle

    'application' | 'class' | 'instance' | 'none'

    The default lifecycle for factories. class by default

    precedence

    'active' | 'passive'

    The default precedence for factories. active by default

    optional

    boolean

    Whether factories should be optional by default

    nodeModules

    boolean

    When trying to resolve a dependency, should it attempt to import the it from node modules?

    globals

    boolean

    When trying to resolve a dependency, should it check for it on the global object?

    jpex.raw

    <T>() => (...deps: any[]) => T;

    Returns the raw factory function, useful for testing.

    jpex.clearCache

    () => void
    <T>() => void

    Clears the cache of resolved factories. If you provide a type, that specific factory will be cleared, otherwise it will clear all factories.

    jpex.infer

    <T>() => string;

    Under the hood jpex converts types into strings for runtime resolution. If you want to get that calculated string for whatever reason, you can use jpex.infer

    Types

    Jpex

    This is the type definition for the jpex container

    NodeModule

    This is a special type that lets you automatically inject a node module with type inference.

    For example:

    import jpex, { NodeModule } from 'jpex';
    
    // this will resolve to the fs module without you having to explicitly register it as a dependency
    const fs = jpex.resolve<NodeModule<'fs'>>();

    The default return type will be any but you can specify one explicitly with the second type parameter:

    import type fstype from 'fs';
    import jpex, { NodeModule } from 'jpex';
    
    const fs = jpex.resolve<NodeModule<'fs', typeof fstype>>();

    Global

    This is another special type that lets you automatically inject a global property with type inference.

    For built-in types you can do this without any helpers:

    import jpex from 'jpex';
    
    const navigator = jpex.resolve<Navigator>();

    But for custom globals, or properties that don't have built-in types, you can use the Global type:

    import jpex, { Global } from 'jpex';
    
    const analytics = jpex.resolve<Global<'ga', Function>>();

    caveats

    There are a few caveats to be aware of:

    • Only named types/interfaces are supported so you can't do jpex.factory<{}>()
    • There is not yet a concept of extending types, so if you do interface Bah extends Foo {} you can't then try to resolve Foo and expect to be given Bah, they are treated as 2 separate things
    • The check for a jpex instance is based on the variable name, so you can't do const jpex2 = jpex; jpex2.constant<Foo>(foo); without explicitly adding jpex2 to the plugin config
    • Similiarly you can't do const { factory } = jpex

    react

    Jpex is a really good fit with React as it offers a good way to inject impure effects into pure components. There is a react-jpex library that exposes a few hooks.

    import React from 'react';
    import { useResolve } from 'react-jpex';
    import { SaveData } from '../types';
    
    const MyComponent = (props) => {
      const saveData = useResolve<SaveData>();
    
      const onSubmit = () => saveData(props.values);
    
      return (
        <div>
          <MyForm />
          <button onClick={onSubmit}>Submit</button>
        </div>
      );
    };

    And this pattern also makes it really easy to isolate a component from its side effects when writing tests:

    import { Provider } from 'react-jpex';
    // create a stub for the SaveData dependency
    const saveData = stub();
    
    render(
      <Provider
        inherit={false}
        // register our stub dependency on an isolated container
        onMount={(jpex) => jpex.constant<SaveData>(saveData)}
      >
        {/* when we render MyComponent, it will be given our stubbed dependency */}
        <MyComponent />
      </Provider>,
    );
    
    // trigger the compnent's onClick
    doOnClick();
    
    expect(saveData.called).to.be.true;

    Vanilla JS mode

    Perhaps you hate typescript, or babel, or both. Or perhaps you don't have the luxury of a build pipeline in your application. That's fine because jpex supports vanilla js as well, you just have to explicitly state your dependencies up front:

    const { jpex } = require('jpex');
    
    jpex.constant('foo', 'foo');
    jpex.factory('bah', ['foo'], (foo) => foo + 'bah');
    
    const value = jpex.resolve('bah');

    Jpex uses language features supported by the latest browsers, but if you need to support IE11 et al. you can import from 'jpex/dist/es5` (or create an alias in your build process)

    Keywords

    none

    Install

    npm i jpex

    DownloadsWeekly Downloads

    837

    Version

    4.3.3

    License

    ISC

    Unpacked Size

    68.8 kB

    Total Files

    26

    Last publish

    Collaborators

    • avatar