7

I'd like to inject lodash by name, something like this:

let val = function(lodash){
   // lodash will be injected, simply by using require('lodash');
};

but say I want to rename the import, I want do something like this:

let val = function({lodash:_}){

};

or

let val = function(lodash as _){

};

is there a way to do this with either ES6/ES7/ES8 or TypeScript?

Note that this DI framework does more work than just require('x')...it will try to inject other values first, if nothing else exists, then it will attempt to require the value.

Note also that the requirements here are that when you call val.toString() then "lodash" would be seen as the argument name. But _ instead of lodash would be seen at runtime inside the function body. This is because in order to inject lodash, we call fn.toString() to get the argument names.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • you want to create a DI library where you can have a function with variables set and those will be required? – Tzook Bar Noy Jun 21 '17 at 16:34
  • I want to rename the variable after it is injected by name. Lodash matches the dependency name, but I want to allow them to automatically rename it from inside the function scope. – Alexander Mills Jun 21 '17 at 17:18
  • Ok and is that used with webpack, or you want it to be generic? I mean that all packages are already loaded and you just want them rename inside the function scoop? – Tzook Bar Noy Jun 21 '17 at 17:27
  • Generic if possible, but I will take a webpack solution if nothing else – Alexander Mills Jun 21 '17 at 17:28
  • ES7 is deprecated name for ES.next, and there's no such thing as ES8. – Estus Flask Jun 24 '17 at 15:01
  • version 1.0.1 ? :) confident :) fyi you can start NPM versions at 0.0.10001 last time I checked. I don't think you go backwards in versions once you publish, but I guess you could ask NPM. – Alexander Mills Jun 25 '17 at 06:54
  • 1
    @AlexanderMills `npm init` defaults to `1.0.0`. I'm not sure why, but I always just do that. – Patrick Roberts Jun 25 '17 at 07:12
  • http://semver.org/, it's not a big deal at all, but usually 1.0.0 means already tested by a few actual users, is production ready etc. I think it's dumb NPM defaults to 1.0.0...I should open a ticket about that with them. – Alexander Mills Jun 25 '17 at 07:48
  • 1
    Well I'd say it's production ready considering I test all the advertised features with mocha and it has 100% code coverage. – Patrick Roberts Jun 25 '17 at 15:25
  • Yeah I asked this DI question because I need some advanced DI features for this library - https://github.com/sumanjs/suman - I made the 1.0.0 mistake with suman 2 years ago, back when I was more of an NPM newb and it irritated me that they had me start off at 1.0.0. I need some help with Suman if you are interested in state of the art software testing. it's written with Node/TypeScript, but it can run tests in any language which is really cool. – Alexander Mills Jun 25 '17 at 19:07
  • So you want something to replace [this](https://github.com/sumanjs/suman/blob/master/lib/injection/create-injector.js)? – Patrick Roberts Jun 27 '17 at 02:09
  • more or less, there are 3 or 4 different injection routines, let me find the one that's highest value – Alexander Mills Jun 27 '17 at 02:23
  • yeah, pretty much any of the files in https://github.com/sumanjs/suman/tree/master/lib/injection could be replaced by something more sophisticated. Right now, they use require('function-arguments') to read the arguments from the function, and then inject by matching name. One nice thing, of course, is that order of arguments doesn't matter with DI. – Alexander Mills Jun 27 '17 at 02:25

4 Answers4

5

Update

Here's a link to the npm package di-proxy (inspired by this answer) with 100% code coverage, and support for memoization to increase performance, compatible with Node.js >=6.0.0.

Old answer

Here's an awesome solution I figured out while tinkering around with object destructuring and Proxy:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      /* this is just a demo, swap these two lines for actual injection */
      // return require(name);
      return { name };
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({ lodash: _, 'socket.io': sio, jquery: $, express, fs }, other, args) {
  // already have access to lodash, no need to even require() here
  console.log(_);
  console.log(sio);
  console.log($);
  console.log(express);
  console.log(fs);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');
.as-console-wrapper {
  max-height: 100% !important;
}

How it works

Passing parameters to a function via object destructuring invokes the getter methods for each property on the object literal in order to determine the values when the function is executed.

If the object being destructured is initialized as a Proxy, you can intercept each getter invocation with a reference to the property name attempting to be resolved, and return a value you choose to resolve it with. In this case, the resolution should be the require(name), which injects the module just by specifying it as a property name in the function object parameter.

Below is a link to a demo where you can actually see it working in Node.js.

Try it online!

Here's the code in that demo just for reference, because it demonstrates object destructuring to a larger degree:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      return require(name);
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({
  fs: { readFile: fsRead, writeFile: fsWrite },
  child_process: { fork: cpF, spawn: cpS, exec: cpE },
  events: { EventEmitter }
}, other, args) {
  // already have access to modules, no need to require() here
  console.log('fs:', { fsRead, fsWrite });
  console.log('child_process:', { fork: cpF, spawn: cpS, exec: cpE });
  console.log('EventEmitter:', EventEmitter);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');

As stated above, I have published a full npm package implementing this concept. I recommend you check it out if you like this syntax and want something a little more performant and tested than this very basic example.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • yeah I think a proxy function is the only way to do this! – Alexander Mills Jun 23 '17 at 17:19
  • yeah that will be awesome - if you could create a simple readme file with examples then I think people would use it. A couple tests would help too to prove it. – Alexander Mills Jun 23 '17 at 17:23
  • 1
    @AlexanderMills and to be fair, there are other solutions to this, but they're dirty hacks at best, since they require the function being injected to be non-native in order to access its declaration as a string, pass it to some sort of AST generator and figure out what properties to inject. That is all necessary if `Proxy` is not supported, so I may consider having a fallback to do all that for backwards compatibility. – Patrick Roberts Jun 23 '17 at 17:28
  • 1
    What's the oldest version of JS and Node.js that supports Proxy? I was wondering that. For my purposes, I think using Proxy is fine since I am only supporting Node.js versions > 4.0.0. – Alexander Mills Jun 23 '17 at 17:58
  • @AlexanderMills See the row for `Proxy` under Built-ins [here](http://kangax.github.io/compat-table/es6/). Support in Node.js starts at 6.0, and all modern browsers support it. IE, including 11, have no support, but starting at Edge 12 has full support. – Patrick Roberts Jun 23 '17 at 18:01
  • 1
    huh, I wonder if TypeScript/Babel can transpile Proxy to ES5...? – Alexander Mills Jun 23 '17 at 18:21
  • 1
    @AlexanderMills no, a non-native implementation is not possible. – Patrick Roberts Jun 23 '17 at 18:22
  • Got it, that makes sense – Alexander Mills Jun 23 '17 at 18:22
3

There's no syntax in JavaScript that supports such mapping. Even if custom function signature parser were written to to provide desired behaviour for destructured params like function({lodash:_}) ..., it would fail for transpiled functions, which is a major flaw. The most straightforward way to handle this is

function foo(lodash){
  const _ = lodash;
  ...
}

And it obviously won't work for invalid variable names like lodash.pick.

A common practice for DI recipes to do this is to provide annotations. All of described annotations can be combined together. They are particularly implemented in Angular DI. Angular injector is available for standalone use (including Node) as injection-js library.

Annotation property

This way function signature and the list of dependencies don't have to match. This recipe can be seen in action in AngularJS.

The property contains a list of DI tokens. They can be names of dependencies that will be loaded with require or something else.

// may be more convenient when it's a string
const ANNOTATION = Symbol();

...

foo[ANNOTATION] = ['lodash'];
function foo(_) {
  ...
}

bar[ANNOTATION] = ['lodash'];
function bar() {
  // doesn't need a param in signature
  const _ = arguments[0];
  ...
}

And DI is performed like

const fnArgs = require('fn-args');
const annotation = foo[ANNOTATION] || fnArgs(foo);
foo(...annotation.map(depName => require(depName));

This style of annotations disposes to make use of function definitions, because hoisting allows to place annotation above function signature for convenience.

Array annotation

Function signature and the list of dependencies don't have to match. This recipe can be seen in AngularJS, too.

When function is represented as an array, this means that it is annotated function, and its parameters should be treated as annotations, and the last one is function itself.

const foo = [
  'lodash',
  function foo(_) {
  ...
  }
];

...

const fn = foo[foo.length - 1];
const annotation = foo.slice(0, foo.length - 1);
foo(...annotation.map(depName => require(depName));

TypeScript type annotation

This recipe can be seen in Angular (2 and higher) and relies on TypeScript types. Types can be extracted from constructor signature and used for DI. Things that make it possible are Reflect metadata proposal and TypeScript's own emitDecoratorMetadata feature.

Emitted constructor types are stored as metadata for respective classes and can be retrieved with Reflect API to resolve dependencies. This is class-based DI, since decorators are supported only on classes, it works best with DI containers:

import 'core-js/es7/reflect';

abstract class Dep {}

function di(target) { /* can be noop to emit metadata */ }

@di
class Foo {
  constructor(dep: Dep) {
    ...
  }
}

...

const diContainer = { Dep: require('lodash') };
const annotations = Reflect.getMetadata('design:paramtypes', Foo);
new (Foo.bind(Foo, ...annotations.map(dep => diContainer [dep]))();

This will produce workable JS code but will create type issues, because Lodash object isn't an instance of Dep token class. This method is primarily effective for class dependencies that are injected into classes.

For non-class DI a fallback to other annotations is required.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • did you read the other answer? I think Proxy has some promise here, as well. – Alexander Mills Jun 24 '17 at 18:45
  • 1
    @AlexanderMills I did. It looks as a neat idea but impractical. The performance of proxies seriously limits their potential use in real-world apps. The trade-off is unfair, considering that other DI patterns have nearly zero performance penalty. – Estus Flask Jun 24 '17 at 18:51
  • IIRC the benchmarks I've done before the penalty is anywhere from x100 to x1000... not exactly a thing that is welcome on frequently called function. – Estus Flask Jun 24 '17 at 19:03
  • yeah in this case it might be called frequently, so yeah – Alexander Mills Jun 24 '17 at 19:07
  • I think Proxy is most useful for testing, where performance is not as critical – Alexander Mills Jun 24 '17 at 19:07
  • @estus I think I can mitigate that performance penalty by writing something that inherits from `Proxy` and then only the first call to each property will invoke the penalty, after which the getter method of the `Proxy` will be shadowed by a true property which contains the memoized result. – Patrick Roberts Jun 24 '17 at 19:22
  • In addition, while other DI patterns have nearly zero performance penalty, it _is_ at the trade-off of having to redundantly force the consumer to specify an annotation or metadata, which is admittedly a little cumbersome. I'd also like to clarify that the claim about zero performance penalty does _not_ apply to [`require('fn-args')`](https://github.com/sindresorhus/fn-args/blob/master/index.js)... look at that mess. – Patrick Roberts Jun 24 '17 at 19:24
  • @AlexanderMills This also depends on your use case for DI. If it's just for testing, it could be easier to mock `require` instead and don't look any further; there are several existing libs that do that. – Estus Flask Jun 24 '17 at 19:40
  • I know what you mean, but my DI library is doing more than just injecting dependencies from files, it also injects deps from functions, etc. – Alexander Mills Jun 24 '17 at 19:45
  • @PatrickRoberts A Proxy subclass would probably work... not sure what would be pitfalls when caching. Well, DI is *always* cumbersome and requires some extra input from a dev. It really depends on particular case if it pays off or not. Of course, `fn-args` is complex, it tries to cover all function flavours after all (it's there for example, the one can always come up with his/her own superfast solution with zero regexps). The resulting annotation should obviously be cached after first pass (like it's done in Angular 1). – Estus Flask Jun 24 '17 at 19:49
  • So then we agree that if after the first call, results are memoized, then whatever is accomplished in the first call is worth the performance hit. I'll update my answer in a few hours with my npm package, which will do exactly this. – Patrick Roberts Jun 24 '17 at 19:52
  • Well I just came across an interesting issue... did you know that `Proxy.prototype === undefined`? I have to do one of those dumb ES5 design patterns to inherit from it, e.g. `function Injector(){};Injector.prototype = new Proxy({}, handler);` – Patrick Roberts Jun 24 '17 at 20:31
  • @PatrickRoberts I don't remember that, good to know. Makes sense since [the doc](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) is pretty clear on that that there's no prototype. Can't it just be something like `class Injector { constructor(target) { return new Proxy(target, this) } }`? It depends on what Injector actually does. – Estus Flask Jun 24 '17 at 20:46
  • @estus I can't do that, because the `receiver` property on the get trap needs to be the instance of the inherited class. – Patrick Roberts Jun 24 '17 at 20:51
0

I have did something that may work for you, but you can always change it and use the general idea.

It is written with ES6 features, but you can easily remove them.

let di = function() {
    const argumentsLength = arguments.length;

    //you must call this func with at least a callback
    if (argumentsLength === 0) return;
    //this will be called with odd amount of variables,
    //pairs of key and assignment, and the callback
    //means: 1,3,5,7.... amount of args
    if (argumentsLength%2 === 0) throw "mismatch of args";

    //here we will assing the variables to "this"
    for (key in arguments) {
        //skip the callback
        if(key===argumentsLength-1) continue;
        //skip the "key", it will be used in the next round
        if(key%2===0) continue;
        const keyToSet = arguments[key-1];
        const valToSet = arguments[key];
        this[keyToSet] = valToSet;
    }

    arguments[argumentsLength-1].apply(this);
}

di("name", {a:"IwillBeName"}, "whatever", "IwillBeWhatever", () => {
    console.log(whatever);
    console.log(name);
});

in the bottom line, you call the func "di" pass in these args:

di("_", lodash, callback);

now inside you callback code, you could reference "lodash" with "_"

Tzook Bar Noy
  • 11,337
  • 14
  • 51
  • 82
0

Given the answers, I still think what Angular 1.x (and RequireJS) does is the most performant, although perhaps not easiest to use:

let  = createSomething('id', ['lodash', function(_){


}]);
Alexander Mills
  • 90,741
  • 139
  • 482
  • 817