From 8af1463c87b89fe5e2f2f241cac2aabf5130a5f1 Mon Sep 17 00:00:00 2001 From: Joshua B Date: Sat, 28 May 2022 10:49:13 -0600 Subject: [PATCH 1/6] add extra test to verify the injector itself can be injected --- test/injector.spec.js | 65 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/test/injector.spec.js b/test/injector.spec.js index a150eea..1d2c895 100644 --- a/test/injector.spec.js +++ b/test/injector.spec.js @@ -159,12 +159,13 @@ describe('injector', function() { it('should resolve dependencies', function() { class Foo { - constructor(bar1, baz1) { + constructor(/* bar */ bar1, /* baz */ baz1) { this.bar = bar1; this.baz = baz1; } } - Foo.$inject = [ 'bar', 'baz' ]; + + // Foo.$inject = [ '...', 'bar', 'baz' ]; function bar(baz, abc) { return { @@ -183,12 +184,72 @@ describe('injector', function() { const injector = new Injector([ module ]); const fooInstance = injector.get('foo'); + const barInstance = injector.get('bar'); expect(fooInstance.bar).to.deep.equal({ baz: 'baz-value', abc: 'abc-value' }); + expect(barInstance).to.deep.equal({ + baz: 'baz-value', + abc: 'abc-value' + }); + + expect(fooInstance.baz).to.equal('baz-value'); + }); + + it('inject injector', function() { + class Foo { + constructor(bar1, baz1) { + this.bar = bar1; + this.baz = baz1; + } + } + Foo.$inject = [ 'bar', 'baz' ]; + + const bar = (baz, injector) => { + return { + baz: baz, + abc: injector.get('abc') + }; + }; + + const barFn = (injector) => (x) => { + return { + abc: injector.get('abc') + x + }; + }; + + // bar.$inject = [ 'baz', 'injector' ]; + + const module = /** @type ModuleDeclaration */ ({ + foo: [ 'type', Foo ], + bar: [ 'factory', bar ], + barFn: [ 'factory', barFn ], + baz: [ 'value', 'baz-value' ], + abc: [ 'value', 'abc-value' ] + }); + + const injector = new Injector([ module ]); + const fooInstance = injector.get('foo'); + const barInstance = injector.get('bar'); + const barFnRef = injector.get('barFn'); + + expect(fooInstance.bar).to.deep.equal({ + baz: 'baz-value', + abc: 'abc-value' + }); + + expect(barInstance).to.deep.equal({ + baz: 'baz-value', + abc: 'abc-value' + }); + + expect(barFnRef('-go')).to.deep.equal({ + abc: 'abc-value-go' + }); + expect(fooInstance.baz).to.equal('baz-value'); }); From 7d4eb7ca7d62631894163e7cf975f97c98bef05c Mon Sep 17 00:00:00 2001 From: Joshua B Date: Sat, 28 May 2022 10:50:22 -0600 Subject: [PATCH 2/6] flush out tests and get basics going for parser, beefed up the README --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++- lib/annotation.js | 6 +++ lib/injector.js | 23 +++++++++-- package.json | 1 + test/annotation.spec.js | 6 +++ 5 files changed, 119 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c61b0d4..8ae0d50 100644 --- a/README.md +++ b/README.md @@ -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 uses 3 flavors to make it happen. #### `type(token, Constructor)` @@ -119,8 +121,74 @@ 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 or Destructured Parameters + +This works with minification(in vite) and does not require babel. + +```js +// 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: @@ -159,6 +227,22 @@ const engineModule = { }; ``` +### 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 diff --git a/lib/annotation.js b/lib/annotation.js index 5428e02..711706e 100644 --- a/lib/annotation.js +++ b/lib/annotation.js @@ -45,6 +45,7 @@ export function annotate() { var CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m; var FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m; var FN_ARG = /\/\*([^*]*)\*\//m; +var FN_CURLY = /{(.*?)}/; /** * @param {unknown} fn @@ -66,6 +67,11 @@ export function parseAnnotations(fn) { var args = match[1] || match[2]; + if (args && args.startsWith('{')) { + let m = args.match(FN_CURLY)[1]; + args = m; + } + return args && args.split(',').map(function(arg) { var argMatch = arg.match(FN_ARG); return (argMatch && argMatch[1] || arg).trim(); diff --git a/lib/injector.js b/lib/injector.js index 2f7fb0c..03ac57b 100644 --- a/lib/injector.js +++ b/lib/injector.js @@ -99,8 +99,16 @@ 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 { @@ -108,10 +116,19 @@ export default function Injector(modules, parent) { } }); - return { + var ret = { fn: fn, dependencies: dependencies }; + + // convert dependencies to object form + if (isObjParam) { + ret.dependencies = injectProps.reduce((accumulator, key, idx) => { + return { ...accumulator, [key]: dependencies[idx] }; + }, {}); + } + + return ret; } function instantiate(Type) { diff --git a/package.json b/package.json index 2e57755..c4a39a4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "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", "integration-test": "(cd test/integration && mocha -r ts-node/register *.spec.{js,ts})", "prepare": "run-s bundle" }, diff --git a/test/annotation.spec.js b/test/annotation.spec.js index 47c343b..5d8f2e7 100644 --- a/test/annotation.spec.js +++ b/test/annotation.spec.js @@ -231,4 +231,10 @@ describe('annotation', function() { }); + it('should parse destructured object args', function() { + const fn = function({ a, b, c }) {}; + + expect(parseAnnotations(fn)).to.eql([ 'a', 'b', 'c' ]); + }); + }); From f8f55c79c6d91a0a2edda40bbecdc5325d22948a Mon Sep 17 00:00:00 2001 From: Joshua B Date: Sat, 28 May 2022 14:29:52 -0600 Subject: [PATCH 3/6] tests passing with object destructuring with renaming and defaults. added example to README --- README.md | 42 ++++++++++++++++++++++++++-- lib/annotation.js | 23 +++++++++++++--- lib/injector.js | 16 +++++++---- lib/util.js | 11 +++++++- package.json | 2 +- test/annotation.spec.js | 17 ++++++++++-- test/injector.spec.js | 61 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8ae0d50..b05f3b3 100644 --- a/README.md +++ b/README.md @@ -157,19 +157,20 @@ const carModule = { }; ``` -### Option 3: Unpacking or Destructured Parameters +### Option 3: Unpacking/Destructured Parameters This works with minification(in vite) and does not require babel. -```js +```javascript // destructured object parameter -function createEngine({power}) { ... } +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 @@ -222,9 +223,44 @@ 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 + +```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 + ``` ### Injecting the injector diff --git a/lib/annotation.js b/lib/annotation.js index 711706e..50c23ab 100644 --- a/lib/annotation.js +++ b/lib/annotation.js @@ -45,7 +45,13 @@ export function annotate() { var CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m; var FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m; var FN_ARG = /\/\*([^*]*)\*\//m; -var FN_CURLY = /{(.*?)}/; +var OBJ_CURLY = /{(.*?)}/m; // matches inside {a,b,c} +var OBJ_DEFAULTS = /([^=]*)[=]?/; // matches the group 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 = /([^:]*)[:]?/m; // matches the prop in foo:bar 'foo':bar 'foo.buz':bar + /** * @param {unknown} fn @@ -67,13 +73,22 @@ export function parseAnnotations(fn) { var args = match[1] || match[2]; + // if its a destructuctured object param in form {a,b,c} will use if (args && args.startsWith('{')) { - let m = args.match(FN_CURLY)[1]; - args = m; + let m = args.match(OBJ_CURLY)[1]; + + // prefix with '...' so a,b will become ...,a,b which is indicator to injector to pass object for destructuring. + args = `...,${m}`; } 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(); }) || []; } \ No newline at end of file diff --git a/lib/injector.js b/lib/injector.js index 03ac57b..68c40af 100644 --- a/lib/injector.js +++ b/lib/injector.js @@ -5,7 +5,8 @@ import { import { isArray, - hasOwnProp + hasOwnProp, + isPlainObject } from './util'; /** @@ -112,7 +113,9 @@ export default function Injector(modules, parent) { 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); } }); @@ -124,7 +127,8 @@ export default function Injector(modules, parent) { // convert dependencies to object form if (isObjParam) { ret.dependencies = injectProps.reduce((accumulator, key, idx) => { - return { ...accumulator, [key]: dependencies[idx] }; + // dont pass nulls through + return dependencies[idx] === null ? accumulator : { ...accumulator, [key]: dependencies[idx] } ; }, {}); } @@ -146,8 +150,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); } diff --git a/lib/util.js b/lib/util.js index c34e526..658b846 100644 --- a/lib/util.js +++ b/lib/util.js @@ -27,4 +27,13 @@ export function isArray(obj) { */ export function hasOwnProp(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); -} \ No newline at end of file +} + +/** + * 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]'; +} diff --git a/package.json b/package.json index c4a39a4..7de6292 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "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:ann": "nyc --reporter=lcov mocha -r esm test/annotation.spec.js", "integration-test": "(cd test/integration && mocha -r ts-node/register *.spec.{js,ts})", "prepare": "run-s bundle" }, diff --git a/test/annotation.spec.js b/test/annotation.spec.js index 5d8f2e7..ab10526 100644 --- a/test/annotation.spec.js +++ b/test/annotation.spec.js @@ -231,10 +231,21 @@ describe('annotation', function() { }); - it('should parse destructured object args', function() { - const fn = function({ a, b, c }) {}; + 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() { + const fn = function({ foo:a, bar: b, 'foo.baz': c = 3, x}) {}; + + expect(parseAnnotations(fn)).to.eql([ '...', 'foo', 'bar', 'foo.baz', 'x' ]); + }); - expect(parseAnnotations(fn)).to.eql([ 'a', 'b', 'c' ]); }); }); diff --git a/test/injector.spec.js b/test/injector.spec.js index 1d2c895..b2dd1fe 100644 --- a/test/injector.spec.js +++ b/test/injector.spec.js @@ -1228,4 +1228,65 @@ describe('injector', function() { }); + describe('destructered object params', function() { + + it('destructered with default', function() { + + function makeEngine({ power, foo = 'bar', block = 'alum' }) { + return { + getPower() { return `${power}hp`; }, + foo, + block + }; + } + + const module = ({ + engine: [ 'factory', makeEngine ], + power: [ 'value', 400 ], + foo: [ 'value', false ] // override default + }); + + const injector = new Injector([ module ]); + const _engine = injector.get('engine'); + const _power = injector.get('power'); + + expect(_power).to.equal(400); + expect(_engine.getPower()).to.equal('400hp'); + expect(_engine.block).to.equal('alum'); + expect(_engine.foo).to.equal(false); // shoudl override the default + }); + + it('with renaming, key and defaults', function() { + + 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 ], + power: [ 'value', 400 ], + kinds: [ 'value', { v8: '8 cylinder', v6: '6 cylinder' } ], + block: [ 'factory', ({power}) => power > 300 ? 'steel' : 'alum' ] + }); + + const injector = new Injector([ module ]); + const {getPower, powerDesc, kind, blockType, fuelType} = injector.get('engine'); + + expect( injector.get('power') ).to.equal(400); + expect( injector.get('kinds.v8') ).to.equal('8 cylinder'); + + expect(getPower()).to.equal(400); + expect(powerDesc).to.equal('400hp'); + expect(kind).to.equal('8 cylinder'); + expect(blockType).to.equal('steel'); + expect(fuelType).to.equal('diesel'); + }); + + }); }); From aea3f66f9ef938397d8ed6e93cb1cc99072b1eef Mon Sep 17 00:00:00 2001 From: Joshua B Date: Sat, 28 May 2022 14:55:13 -0600 Subject: [PATCH 4/6] added invoke tests and note in README --- README.md | 9 ++++++--- test/injector.spec.js | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b05f3b3..5d8bed5 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ injector.invoke(['car', function(car) { ### 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 uses 3 flavors to make it happen. +This project calls them components and there are 3 flavors; `type`, `factory`, `value`. #### `type(token, Constructor)` @@ -235,12 +235,12 @@ function Engine({ 'config.engine.power': power }) { ... } ### Destructured Function Parameters -Kitchen Sink example that will work with minification +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, + getPower: () => p, powerDesc: `${p}hp`, kind, blockType: b, @@ -262,6 +262,9 @@ 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 diff --git a/test/injector.spec.js b/test/injector.spec.js index b2dd1fe..cc33e0d 100644 --- a/test/injector.spec.js +++ b/test/injector.spec.js @@ -1271,8 +1271,8 @@ describe('injector', function() { const module = ({ engine: [ 'factory', makeEngine ], power: [ 'value', 400 ], - kinds: [ 'value', { v8: '8 cylinder', v6: '6 cylinder' } ], - block: [ 'factory', ({power}) => power > 300 ? 'steel' : 'alum' ] + kinds: [ 'value', { v8: '8 cylinder', v6: '6' } ], + block: [ 'factory', ({ power }) => power > 300 ? 'steel' : 'alum' ] }); const injector = new Injector([ module ]); @@ -1286,6 +1286,11 @@ describe('injector', function() { expect(kind).to.equal('8 cylinder'); expect(blockType).to.equal('steel'); expect(fuelType).to.equal('diesel'); + + // make sure invoke works + const fn = ({ power:p, 'kinds.v6': k, foo = 'bar' }) => `${p}:${k}:${foo}`; + expect(injector.invoke(fn)).to.equal('400:6:bar'); + expect(injector.invoke(fn, null, {foo: 'buzz'})).to.equal('400:6:buzz'); }); }); From 990ac568cc744c94f21b072f197c30da02070541 Mon Sep 17 00:00:00 2001 From: Joshua B Date: Sat, 28 May 2022 14:56:25 -0600 Subject: [PATCH 5/6] lint fixes --- lib/annotation.js | 6 +++--- lib/injector.js | 1 + test/annotation.spec.js | 4 ++-- test/injector.spec.js | 14 +++++++------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/annotation.js b/lib/annotation.js index 50c23ab..d8c3133 100644 --- a/lib/annotation.js +++ b/lib/annotation.js @@ -48,7 +48,7 @@ var FN_ARG = /\/\*([^*]*)\*\//m; var OBJ_CURLY = /{(.*?)}/m; // matches inside {a,b,c} var OBJ_DEFAULTS = /([^=]*)[=]?/; // matches the group in x=123 or x: b = 123 -//TODO using replace below as it seems to be caputuring the quotes. come back to this. +// 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 = /([^:]*)[:]?/m; // matches the prop in foo:bar 'foo':bar 'foo.buz':bar @@ -73,11 +73,11 @@ export function parseAnnotations(fn) { var args = match[1] || match[2]; - // if its a destructuctured object param in form {a,b,c} will use + // if its a destructuctured object param in form {a,b,c} will use if (args && args.startsWith('{')) { let m = args.match(OBJ_CURLY)[1]; - // prefix with '...' so a,b will become ...,a,b which is indicator to injector to pass object for destructuring. + // prefix with '...' so a,b will become ...,a,b which is indicator to injector to pass object for destructuring. args = `...,${m}`; } diff --git a/lib/injector.js b/lib/injector.js index 68c40af..39bdcf2 100644 --- a/lib/injector.js +++ b/lib/injector.js @@ -127,6 +127,7 @@ export default function Injector(modules, parent) { // 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] } ; }, {}); diff --git a/test/annotation.spec.js b/test/annotation.spec.js index ab10526..7bac31e 100644 --- a/test/annotation.spec.js +++ b/test/annotation.spec.js @@ -234,14 +234,14 @@ describe('annotation', function() { describe('object destructure', function() { it('should parse simple destructured object args', function() { - const fn = function({ a, b = 1, c=2 }) {}; + 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() { - const fn = function({ foo:a, bar: b, 'foo.baz': c = 3, x}) {}; + const fn = function({ foo:a, bar: b, 'foo.baz': c = 3, x }) {}; expect(parseAnnotations(fn)).to.eql([ '...', 'foo', 'bar', 'foo.baz', 'x' ]); }); diff --git a/test/injector.spec.js b/test/injector.spec.js index cc33e0d..9eedd90 100644 --- a/test/injector.spec.js +++ b/test/injector.spec.js @@ -1233,13 +1233,13 @@ describe('injector', function() { it('destructered with default', function() { function makeEngine({ power, foo = 'bar', block = 'alum' }) { - return { + return { getPower() { return `${power}hp`; }, foo, block }; } - + const module = ({ engine: [ 'factory', makeEngine ], power: [ 'value', 400 ], @@ -1259,7 +1259,7 @@ describe('injector', function() { it('with renaming, key and defaults', function() { function makeEngine({ power: p, 'kinds.v8': kind, block: b = 'alum', fuel: f = 'diesel' }) { - return { + return { getPower: ()=> p, powerDesc: `${p}hp`, kind, @@ -1276,10 +1276,10 @@ describe('injector', function() { }); const injector = new Injector([ module ]); - const {getPower, powerDesc, kind, blockType, fuelType} = injector.get('engine'); + const { getPower, powerDesc, kind, blockType, fuelType } = injector.get('engine'); - expect( injector.get('power') ).to.equal(400); - expect( injector.get('kinds.v8') ).to.equal('8 cylinder'); + expect(injector.get('power')).to.equal(400); + expect(injector.get('kinds.v8')).to.equal('8 cylinder'); expect(getPower()).to.equal(400); expect(powerDesc).to.equal('400hp'); @@ -1290,7 +1290,7 @@ describe('injector', function() { // make sure invoke works const fn = ({ power:p, 'kinds.v6': k, foo = 'bar' }) => `${p}:${k}:${foo}`; expect(injector.invoke(fn)).to.equal('400:6:bar'); - expect(injector.invoke(fn, null, {foo: 'buzz'})).to.equal('400:6:buzz'); + expect(injector.invoke(fn, null, { foo: 'buzz' })).to.equal('400:6:buzz'); }); }); From 07e37d6b483c07e885323e23ab20a619e62fa170 Mon Sep 17 00:00:00 2001 From: Joshua B Date: Sat, 28 May 2022 17:23:15 -0600 Subject: [PATCH 6/6] support multiline with comments --- lib/annotation.js | 20 ++++++++++++-------- package.json | 2 ++ test/annotation.spec.js | 8 +++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/annotation.js b/lib/annotation.js index d8c3133..2c79cda 100644 --- a/lib/annotation.js +++ b/lib/annotation.js @@ -41,16 +41,22 @@ 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_CURLY = /{(.*?)}/m; // matches inside {a,b,c} -var OBJ_DEFAULTS = /([^=]*)[=]?/; // matches the group in x=123 or x: b = 123 +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 = /([^:]*)[:]?/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 /** @@ -73,12 +79,10 @@ export function parseAnnotations(fn) { var args = match[1] || match[2]; - // if its a destructuctured object param in form {a,b,c} will use + // 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('{')) { - let m = args.match(OBJ_CURLY)[1]; - - // prefix with '...' so a,b will become ...,a,b which is indicator to injector to pass object for destructuring. - args = `...,${m}`; + args = `...,${ args.slice(1, -1).trim() }`; } return args && args.split(',').map(function(arg) { diff --git a/package.json b/package.json index 7de6292..ac6e8c0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "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" }, diff --git a/test/annotation.spec.js b/test/annotation.spec.js index 7bac31e..a4b1d31 100644 --- a/test/annotation.spec.js +++ b/test/annotation.spec.js @@ -241,7 +241,13 @@ describe('annotation', function() { //minification will normally rename the unpacked object param it('should parse assignments destructured object args', function() { - const fn = function({ foo:a, bar: b, 'foo.baz': c = 3, x }) {}; + // 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' ]); });