Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

14 destructured objects #15

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 125 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ injector.invoke(['car', function(car) {
```


### Registering Stuff
### Registering Stuff in the Module
Services, providers, value objects, config objects, etc... There are many names used in the world of DI and IOC.
This project calls them components and there are 3 flavors; `type`, `factory`, `value`.

#### `type(token, Constructor)`

Expand Down Expand Up @@ -119,8 +121,75 @@ const module = {
};
```

## Function Annotations

### Annotation
The following are all valid ways of annotating function with injection arguments and are equivalent.

### Option 1: Inferred

```js
// inferred (only works if code not minified/obfuscated) unless its specified in line,
//will inject the power config
function createEngine(power){
...use power.horses
}

//then in module config would specify the inline args in most cases because of minification
const carModule = {
engine: ['factory', [ 'power', createEngine ]],
power: ['value', {horses: 400}]
};

```

### Option 2: $inject annotated

```js
// annotated
function createEngine(power) { ... }

createEngine.$inject = ['power'];

//then in module config array notation is not needed
const carModule = {
engine: ['factory', createEngine ],
power: ['value', { horses: 400 }]
};
```

### Option 3: Unpacking/Destructured Parameters

This works with minification(in vite) and does not require babel.

```javascript
// destructured object parameter
function createEngine({ power }) { ... }

//then in module config can take the simple route as well since function params are parsed and $inject is automatically added
const carModule = {
engine: ['factory', createEngine ],
power: ['value', { horses: 400 }]
};

```

### Option 4: Babel Annotations/Comments

```js
// @inject
function createEngine({powerService}){
...use powerService
}

...module

```

### Annotations With Comments

In order for these to work with minification the `#__PURE__` will need to be configured.
There are various options that may work using these [Babel annotations](https://babeljs.io/docs/en/babel-helper-annotate-as-pure)
or plugins such as [babel-plugin-inject-args](https://github.com/hypothesis/babel-plugin-inject-args), depending on choice of usage. Its left to the user to investigate (but please do submit PR with successful options that can be outlined here)

The injector looks up tokens based on argument names:

Expand Down Expand Up @@ -154,11 +223,65 @@ function Engine(/* config.engine.power */ power) {
// assuming there is no direct binding for 'config.engine.power' token
}


const engineModule = {
'config': ['value', {engine: {power: 1184}, other : {}}]
};

//with object destructureing it can be done like this
function Engine({ 'config.engine.power': power }) { ... }

```

### Destructured Function Parameters

Kitchen Sink example that will work with minification (tested with vite's esbuild minifier)

```javascript
function makeEngine({ power: p, 'kinds.v8': kind, block: b = 'alum', fuel: f = 'diesel' }) {
return {
getPower: () => p,
powerDesc: `${p}hp`,
kind,
blockType: b,
fuelType: f
};
}

const module = ({
engine: [ 'factory', makeEngine ],
block: [ 'factory', ({ power }) => power > 300 ? 'steel' : 'alum' ]
power: [ 'value', 400 ],
kinds: [ 'value', { v8: '8 cylinder', v6: '6 cylinder' } ],
});

const injector = new Injector([ module ]);
const {getPower, powerDesc, kind, blockType, fuelType} = injector.get('engine');

console.log(`${getPower()} , ${powerDesc} , ${kind} , ${blockType} , ${fuelType})
// output: 400 , 400hp , 8 cylinder , steel , diesel

```
> 📝 **Note:**
> The [injector tests]( test/injector.spec.js ) are a great place to look for examples.
> You will find one that uses the 'type' and a Class with destructured object injection

### Injecting the injector

In cases where you need the injector it can also be injected

```javascript

//can use a function or lambda
const getEnginePower = ({injector}) => injector.get('engine').power

const carModule = {
engine: ['factory', createEngine ],
enginePower: ['factory', getEnginePower ]
};

let power = injector.get('enginePower')
```

### Component Initialization

Expand Down
27 changes: 26 additions & 1 deletion lib/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,23 @@ export function annotate() {
// - can't reliably auto-annotate constructor; we'll match the
// first constructor(...) pattern found which may be the one
// of a nested class, too.
//
// Destructured Object limitations
// - comments in a multi-line will fail with a comma and child objects can hae more than one prop
// {
// a: 1, // this will work as long as it doesnt have a comma
// b: { c: 2, d: 3 } // this will fail, remove the d and its fine
// }

var CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m;
var FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m;
var FN_ARG = /\/\*([^*]*)\*\//m;
var OBJ_DEFAULTS = /([^=]*)[=]?/; // matches the prop in x=123 or x: b = 123

// TODO using replace below as it seems to be caputuring the quotes. come back to this.
// var OBJ_COLON = /['"]?(.+?)['"]?\s?:/m; // matches the prop in foo:bar 'foo':bar 'foo.buz':bar
var OBJ_COLON = /(?:\/\/.*\n)?([^:]*)[:]?/m; // matches the prop in foo:bar 'foo':bar 'foo.buz':bar


/**
* @param {unknown} fn
Expand All @@ -66,8 +79,20 @@ export function parseAnnotations(fn) {

var args = match[1] || match[2];

// if it starts with { then assume its a destructuctured object param in form {a,b,c} and remove the start and end curlys
// will prefix with '...' so a,b will become ...,a,b which is the indicator to injector to pass object for destructuring.
if (args && args.startsWith('{')) {
args = `...,${ args.slice(1, -1).trim() }`;
}

return args && args.split(',').map(function(arg) {
var argMatch = arg.match(FN_ARG);
return (argMatch && argMatch[1] || arg).trim();
var argDefMatch = arg.match(OBJ_DEFAULTS); // looks for defaults like a= or a:b=
var argColonMatch = argDefMatch && argDefMatch[1].trim().match(OBJ_COLON); // looks for destructured rename a:b
return (
(argMatch && argMatch[1])
|| (argColonMatch && argColonMatch[1].replaceAll('"','').replaceAll("'",''))
|| arg
).trim();
}) || [];
}
38 changes: 31 additions & 7 deletions lib/injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {

import {
isArray,
hasOwnProp
hasOwnProp,
isPlainObject
} from './util';

/**
Expand Down Expand Up @@ -99,19 +100,40 @@ export default function Injector(modules, parent) {
}
}

var inject = fn.$inject || parseAnnotations(fn);
var dependencies = inject.map(function(dep) {
var injectProps = fn.$inject || parseAnnotations(fn);
var isObjParam = false;

// if first item is elipses then its a destructuring object for the argument
if (injectProps[0] === '...') {
isObjParam = true;
injectProps.shift();
}

var dependencies = injectProps.map(function(dep) {
if (hasOwnProp(locals, dep)) {
return locals[dep];
} else {
return get(dep);

// if its object destructure then will have defaults and js will already error if not passed in, so do just do safe get
return isObjParam ? get(dep, false) : get(dep);
}
});

return {
var ret = {
fn: fn,
dependencies: dependencies
};

// convert dependencies to object form
if (isObjParam) {
ret.dependencies = injectProps.reduce((accumulator, key, idx) => {

// dont pass nulls through
return dependencies[idx] === null ? accumulator : { ...accumulator, [key]: dependencies[idx] } ;
}, {});
}

return ret;
}

function instantiate(Type) {
Expand All @@ -129,8 +151,10 @@ export default function Injector(modules, parent) {
function invoke(func, context, locals) {
var def = fnDef(func, locals);

var fn = def.fn,
dependencies = def.dependencies;
var fn = def.fn;

// if its object then wrap in array and pass in single arg
var dependencies = isPlainObject(def.dependencies) ? [ def.dependencies ] : def.dependencies;

return fn.apply(context, dependencies);
}
Expand Down
11 changes: 10 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ export function isArray(obj) {
*/
export function hasOwnProp(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
}

/**
* Check if object passes in is a plain object.
* @param {any} obj
* @return {boolean}
*/
export function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"check-types:test": "tsc --project test --pretty --noEmit",
"check-types:integration": "tsc --project test/integration --pretty --noEmit",
"test": "nyc --reporter=lcov mocha -r esm test/*.spec.js",
"test:ann": "nyc --reporter=lcov mocha -r esm test/annotation.spec.js",
"test:objs": "nyc --reporter=lcov mocha -r esm test/*.spec.js -g 'destructered object params'",
"test:obj-ann": "nyc --reporter=lcov mocha -r esm test/annotation.spec.js -g 'object destructure'",
"integration-test": "(cd test/integration && mocha -r ts-node/register *.spec.{js,ts})",
"prepare": "run-s bundle"
},
Expand Down
23 changes: 23 additions & 0 deletions test/annotation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,27 @@ describe('annotation', function() {

});

describe('object destructure', function() {

it('should parse simple destructured object args', function() {
const fn = function({ a, b = 1, c = 2 }) {};

expect(parseAnnotations(fn)).to.eql([ '...', 'a', 'b', 'c' ]);
});

//minification will normally rename the unpacked object param
it('should parse assignments destructured object args', function() {
// TODO changing below to bar:{a:1, b:2} will fail during splitting beacuase it doesnt look up the comma
const fn = function({
foo:a,
bar: { a: b }, // comments will work as long as it doesnt have a comma
'foo.baz': c = 3,
x
}) {};

expect(parseAnnotations(fn)).to.eql([ '...', 'foo', 'bar', 'foo.baz', 'x' ]);
});

});

});
Loading