From 99b38f52cb5a8df8cfcb5c29d2dc02c2fa6da105 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 29 Jun 2017 22:14:45 +0100 Subject: [PATCH] build(aio): fix matchUpDirectiveDecorators processor --- .../processors/matchUpDirectiveDecorators.js | 98 ++++++++++++------- .../matchUpDirectiveDecorators.spec.js | 93 ++++++++++++++++++ 2 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.spec.js diff --git a/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.js b/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.js index ae093a9653..262569fee8 100644 --- a/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.js +++ b/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.js @@ -1,61 +1,85 @@ -var _ = require('lodash'); - /** * @dgProcessor * @description + * Directives in Angular are specified by various decorators. In particular the `@Directive()` + * decorator on the class and various other property decorators, such as `@Input`. * + * This processor will extract this decorator information and attach it as properties to the + * directive document. + * + * Notably, the `input` and `output` binding information can be specified + * either via property decorators (`@Input()`/`@Output()`) or by properties on the metadata + * passed to the `@Directive` decorator. This processor will collect up info from both and + * merge them. */ -module.exports = function matchUpDirectiveDecoratorsProcessor() { - +module.exports = function matchUpDirectiveDecorators() { return { $runAfter: ['ids-computed', 'paths-computed'], $runBefore: ['rendering-docs'], - decoratorMappings: {'Inputs': 'inputs', 'Outputs': 'outputs'}, $process: function(docs) { - var decoratorMappings = this.decoratorMappings; - _.forEach(docs, function(doc) { + docs.forEach(function(doc) { if (doc.docType === 'directive') { - doc.selector = doc.directiveOptions.selector; - for (let decoratorName in decoratorMappings) { - var propertyName = decoratorMappings[decoratorName]; - doc[propertyName] = - getDecoratorValues(doc.directiveOptions[propertyName], decoratorName, doc.members); - } + doc.selector = stripQuotes(doc.directiveOptions.selector); + doc.exportAs = stripQuotes(doc.directiveOptions.exportAs); + + doc.inputs = getBindingInfo(doc.directiveOptions.inputs, doc.members, 'Input'); + doc.outputs = getBindingInfo(doc.directiveOptions.outputs, doc.members, 'Output'); } }); } }; }; -function getDecoratorValues(classDecoratorValues, memberDecoratorName, members) { - var decoratorValues = {}; +function getBindingInfo(directiveBindings, members, bindingType) { + const bindings = {}; - // Parse the class decorator - _.forEach(classDecoratorValues, function(option) { - // Options are of the form: "propName : bindingName" (bindingName is optional) - var optionPair = option.split(':'); - var propertyName = optionPair.shift().trim(); - var bindingName = (optionPair.shift() || '').trim() || propertyName; + // Parse the bindings from the directive decorator + if (directiveBindings) { + directiveBindings.forEach(function(binding) { + const bindingInfo = parseBinding(binding); + bindings[bindingInfo.propertyName] = bindingInfo; + }); + } - decoratorValues[propertyName] = {propertyName: propertyName, bindingName: bindingName}; - }); + if (members) { + members.forEach(function(member) { + if (member.decorators) { + // Search for members with binding decorators + member.decorators.forEach(function(decorator) { + if (decorator.name === bindingType) { + bindings[member.name] = createBindingInfo(member.name, decorator.arguments[0] || member.name); + } + }); + } - _.forEach(members, function(member) { - _.forEach(member.decorators, function(decorator) { - if (decorator.name === memberDecoratorName) { - decoratorValues[member.name] = { - propertyName: member.name, - bindingName: decorator.arguments[0] || member.name - }; + // Now ensure that any bindings have the associated member attached + // Note that this binding could have come from the directive decorator + if (bindings[member.name]) { + bindings[member.name].memberDoc = member; } }); - if (decoratorValues[member.name]) { - decoratorValues[member.name].memberDoc = member; - } - }); - - if (Object.keys(decoratorValues).length) { - return decoratorValues; } + + // Convert the map back to an array + return Object.keys(bindings).map(function(key) { return bindings[key]; }); } + +function stripQuotes(value) { + return (typeof(value) === 'string') ? value.replace(/^(['"])(.*)\1/, '$2') : value; +} + +function parseBinding(option) { + // Directive decorator bindings are of the form: "propName : bindingName" (bindingName is optional) + const optionPair = option.split(':'); + const propertyName = optionPair[0].trim(); + const bindingName = (optionPair[1] || '').trim() || propertyName; + return createBindingInfo(propertyName, bindingName); +} + +function createBindingInfo(propertyName, bindingName) { + return { + propertyName: stripQuotes(propertyName), + bindingName: stripQuotes(bindingName) + }; +} \ No newline at end of file diff --git a/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.spec.js b/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.spec.js new file mode 100644 index 0000000000..f377841958 --- /dev/null +++ b/aio/tools/transforms/angular-api-package/processors/matchUpDirectiveDecorators.spec.js @@ -0,0 +1,93 @@ +const testPackage = require('../../helpers/test-package'); +const processorFactory = require('./matchUpDirectiveDecorators'); +const Dgeni = require('dgeni'); + +describe('matchUpDirectiveDecorators processor', () => { + + it('should be available on the injector', () => { + const dgeni = new Dgeni([testPackage('angular-api-package')]); + const injector = dgeni.configureInjector(); + const processor = injector.get('matchUpDirectiveDecorators'); + expect(processor.$process).toBeDefined(); + expect(processor.$runAfter).toContain('ids-computed'); + expect(processor.$runAfter).toContain('paths-computed'); + expect(processor.$runBefore).toContain('rendering-docs'); + }); + + it('should extract selector and exportAs from the directive decorator on directive docs', () => { + const docs = [{ + docType: 'directive', + directiveOptions: { selector: 'a,b,c', exportAs: 'someExport' } + }]; + processorFactory().$process(docs); + expect(docs[0].selector).toEqual('a,b,c'); + expect(docs[0].exportAs).toEqual('someExport'); + }); + + it('should ignore properties from the directive decorator on non-directive docs', () => { + const docs = [{ + docType: 'class', + directiveOptions: { selector: 'a,b,c', exportAs: 'someExport' } + }]; + processorFactory().$process(docs); + expect(docs[0].selector).toBeUndefined(); + expect(docs[0].exportAs).toBeUndefined(); + }); + + it('should strip quotes off directive properties', () => { + const docs = [{ + docType: 'directive', + directiveOptions: { selector: '"a,b,c"', exportAs: '\'someExport\'' } + }]; + processorFactory().$process(docs); + expect(docs[0].selector).toEqual('a,b,c'); + expect(docs[0].exportAs).toEqual('someExport'); + }); + + it('should extract inputs and outputs from the directive decorator', () => { + const docs = [{ + docType: 'directive', + directiveOptions: { + inputs: ['a:b', 'x'], + outputs: ['foo:foo'] + }, + members: [ + { name: 'a' }, + { name: 'x' }, + { name: 'foo' } + ] + }]; + processorFactory().$process(docs); + expect(docs[0].inputs).toEqual([ + { propertyName: 'a', bindingName: 'b', memberDoc: docs[0].members[0] }, + { propertyName: 'x', bindingName: 'x', memberDoc: docs[0].members[1] } + ]); + + expect(docs[0].outputs).toEqual([ + { propertyName: 'foo', bindingName: 'foo', memberDoc: docs[0].members[2] } + ]); + }); + + it('should extract inputs and outputs from decorated properties', () => { + const docs = [{ + docType: 'directive', + directiveOptions: {}, + members: [ + { name: 'a1', decorators: [{ name: 'Input', arguments: ['a2'] }] }, + { name: 'b1', decorators: [{ name: 'Output', arguments: ['b2'] }] }, + { name: 'c1', decorators: [{ name: 'Input', arguments: [] }] }, + { name: 'd1', decorators: [{ name: 'Output', arguments: [] }] }, + ] + }]; + processorFactory().$process(docs); + expect(docs[0].inputs).toEqual([ + { propertyName: 'a1', bindingName: 'a2', memberDoc: docs[0].members[0] }, + { propertyName: 'c1', bindingName: 'c1', memberDoc: docs[0].members[2] } + ]); + + expect(docs[0].outputs).toEqual([ + { propertyName: 'b1', bindingName: 'b2', memberDoc: docs[0].members[1] }, + { propertyName: 'd1', bindingName: 'd1', memberDoc: docs[0].members[3] } + ]); + }); +});