build(aio): fix rendering of Decorator API docs

The templates were a bit out and we were not merging the
correct properties.

Added some docs and tests for the processor.

Related to #16208
This commit is contained in:
Peter Bacon Darwin 2017-04-24 15:04:52 +01:00 committed by Pete Bacon Darwin
parent f5aaa55f21
commit 9a4c8d543d
4 changed files with 109 additions and 25 deletions

View File

@ -1,3 +1,48 @@
/**
* Decorators in the Angular code base are made up from three code items:
*
* 1) An interface that represents the call signature of the decorator. E.g.
*
* ```
* export interface ComponentDecorator {
* (obj: Component): TypeDecorator;
* new (obj: Component): Component;
* }
* ```
*
* 2) An interface that represents the members of the object that should be passed
* into the decorator. E.g.
*
* ```
* export interface Component extends Directive {
* changeDetection?: ChangeDetectionStrategy;
* viewProviders?: Provider[];
* templateUrl?: string;
* ...
* }
* ```
*
* 3) A constant that is created by a call to a generic function, whose type parameter is
* the call signature interface of the decorator. E.g.
*
* ```
* export const Component: ComponentDecorator =
* <ComponentDecorator>makeDecorator('Component', { ... }, Directive)
* ```
*
* This processor searches for these constants (3) by looking for a call to
* `make...Decorator(...)`. (There are variations to the call for property and param
* decorators). From this call we identify the `decoratorType` (e.g. `ComponentDecorator`).
*
* Calls to `make...Decorator<X>` will return an object of type X. This type is the document
* referred to in (2). This is the primary doc that we care about for documenting the decorator.
* It holds all of the members of the metadata that is passed to the decorator call.
*
* Finally we want to capture the documentation attached to the call signature interface of the
* associated decorator (1). We copy across the properties that we care about from this call
* signature (e.g. description, whatItDoes and howToUse).
*/
module.exports = function mergeDecoratorDocs(log) { module.exports = function mergeDecoratorDocs(log) {
return { return {
$runAfter: ['processing-docs'], $runAfter: ['processing-docs'],
@ -15,18 +60,19 @@ module.exports = function mergeDecoratorDocs(log) {
docs.forEach(function(doc) { docs.forEach(function(doc) {
makeDecoratorCalls.forEach(function(call) { makeDecoratorCalls.forEach(function(call) {
// find all the decorators, signified by a call to `makeDecorator(metadata)` // find all the decorators, signified by a call to `make...Decorator<Decorator>(metadata)`
var makeDecorator = getMakeDecoratorCall(doc, call.type); var makeDecorator = getMakeDecoratorCall(doc, call.type);
if (makeDecorator) { if (makeDecorator) {
log.debug('mergeDecoratorDocs: found decorator', doc.docType, doc.name); log.debug('mergeDecoratorDocs: found decorator', doc.docType, doc.name);
doc.docType = 'decorator'; doc.docType = 'decorator';
doc.decoratorLocation = call.description; doc.decoratorLocation = call.description;
// get the type of the decorator metadata // Get the type of the decorator metadata from the first "type" argument of the call.
// For example the `X` of `createDecorator<X>(...)`.
doc.decoratorType = makeDecorator.arguments[0].text; doc.decoratorType = makeDecorator.arguments[0].text;
// clear the symbol type named (e.g. ComponentMetadataFactory) since it is not needed // clear the symbol type named since it is not needed
doc.symbolTypeName = undefined; doc.symbolTypeName = undefined;
// keep track of the names of the docs that need to be merged into this decorator doc // keep track of the names of the metadata interface that will need to be merged into this decorator doc
docsToMerge[doc.name + 'Decorator'] = doc; docsToMerge[doc.name + 'Decorator'] = doc;
} }
}); });
@ -35,11 +81,15 @@ module.exports = function mergeDecoratorDocs(log) {
// merge the metadata docs into the decorator docs // merge the metadata docs into the decorator docs
docs = docs.filter(function(doc) { docs = docs.filter(function(doc) {
if (docsToMerge[doc.name]) { if (docsToMerge[doc.name]) {
// We have found an `XxxDecorator` document that will hold the call signature of the decorator
var decoratorDoc = docsToMerge[doc.name]; var decoratorDoc = docsToMerge[doc.name];
log.debug( log.debug(
'mergeDecoratorDocs: merging', doc.name, 'into', decoratorDoc.name, 'mergeDecoratorDocs: merging', doc.name, 'into', decoratorDoc.name,
doc.callMember.description.substring(0, 50)); doc.callMember.description.substring(0, 50));
// Merge the documentation found in this call signature into the original decorator
decoratorDoc.description = doc.callMember.description; decoratorDoc.description = doc.callMember.description;
decoratorDoc.howToUse = doc.callMember.howToUse;
decoratorDoc.whatItDoes = doc.callMember.whatItDoes;
// remove doc from its module doc's exports // remove doc from its module doc's exports
doc.moduleDoc.exports = doc.moduleDoc.exports =

View File

@ -1,21 +1,40 @@
var testPackage = require('../../helpers/test-package'); var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni'); var Dgeni = require('dgeni');
describe('mergeDecoratorDocs processor', function() { describe('mergeDecoratorDocs processor', () => {
var dgeni, injector, processor, decoratorDoc, decoratorDocWithTypeAssertion, otherDoc; var dgeni, injector, processor, moduleDoc, decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc;
beforeEach(function() { beforeEach(() => {
dgeni = new Dgeni([testPackage('angular-api-package')]); dgeni = new Dgeni([testPackage('angular-api-package')]);
injector = dgeni.configureInjector(); injector = dgeni.configureInjector();
processor = injector.get('mergeDecoratorDocs'); processor = injector.get('mergeDecoratorDocs');
moduleDoc = {};
decoratorDoc = { decoratorDoc = {
name: 'X', name: 'Component',
docType: 'var', docType: 'const',
description: 'A description of the metadata for the Component decorator',
exportSymbol: { exportSymbol: {
valueDeclaration: valueDeclaration:
{initializer: {expression: {text: 'makeDecorator'}, arguments: [{text: 'X'}]}} {initializer: {expression: {text: 'makeDecorator'}, arguments: [{text: 'X'}]}}
} },
members: [
{ name: 'templateUrl', description: 'A description of the templateUrl property' }
],
moduleDoc
};
metadataDoc = {
name: 'ComponentDecorator',
docType: 'interface',
description: 'A description of the interface for the call signature for the Component decorator',
callMember: {
description: 'The actual description of the call signature',
whatItDoes: 'Does something cool...',
howToUse: 'Use it like this...'
},
moduleDoc
}; };
decoratorDocWithTypeAssertion = { decoratorDocWithTypeAssertion = {
@ -28,7 +47,8 @@ describe('mergeDecoratorDocs processor', function() {
{type: {}, expression: {text: 'makeDecorator'}, arguments: [{text: 'Y'}]} {type: {}, expression: {text: 'makeDecorator'}, arguments: [{text: 'Y'}]}
} }
} }
} },
moduleDoc
}; };
otherDoc = { otherDoc = {
name: 'Y', name: 'Y',
@ -36,22 +56,36 @@ describe('mergeDecoratorDocs processor', function() {
exportSymbol: { exportSymbol: {
valueDeclaration: valueDeclaration:
{initializer: {expression: {text: 'otherCall'}, arguments: [{text: 'param1'}]}} {initializer: {expression: {text: 'otherCall'}, arguments: [{text: 'param1'}]}}
} },
moduleDoc
}; };
moduleDoc.exports = [decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc];
}); });
it('should change the docType of only the docs that are initialied by a call to makeDecorator', it('should change the docType of only the docs that are initialied by a call to makeDecorator', () => {
function() { processor.$process([decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc]);
processor.$process([decoratorDoc, decoratorDocWithTypeAssertion, otherDoc]);
expect(decoratorDoc.docType).toEqual('decorator'); expect(decoratorDoc.docType).toEqual('decorator');
expect(decoratorDocWithTypeAssertion.docType).toEqual('decorator'); expect(decoratorDocWithTypeAssertion.docType).toEqual('decorator');
expect(otherDoc.docType).toEqual('var'); expect(otherDoc.docType).toEqual('var');
}); });
it('should extract the "type" of the decorator meta data', function() { it('should extract the "type" of the decorator meta data', () => {
processor.$process([decoratorDoc, decoratorDocWithTypeAssertion, otherDoc]); processor.$process([decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc]);
expect(decoratorDoc.decoratorType).toEqual('X'); expect(decoratorDoc.decoratorType).toEqual('X');
expect(decoratorDocWithTypeAssertion.decoratorType).toEqual('Y'); expect(decoratorDocWithTypeAssertion.decoratorType).toEqual('Y');
}); });
it('should copy across properties from the call signature doc', () => {
processor.$process([decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc]);
expect(decoratorDoc.description).toEqual('The actual description of the call signature');
expect(decoratorDoc.whatItDoes).toEqual('Does something cool...');
expect(decoratorDoc.howToUse).toEqual('Use it like this...');
});
it('should remove the metadataDoc from the module exports', () => {
processor.$process([decoratorDoc, metadataDoc, decoratorDocWithTypeAssertion, otherDoc]);
expect(moduleDoc.exports).not.toContain(metadataDoc);
});
}); });

View File

@ -1,7 +1,7 @@
{% import "lib/paramList.html" as params -%} {% import "lib/paramList.html" as params -%}
{% extends 'layout/api-base.template.html' %} {% extends 'layout/api-base.template.html' %}
{% block main %} {% block details %}
{% include "includes/_description.html" %} {% include "includes/_description.html" %}
{% include "includes/_metadata.html" %} {% include "includes/_metadata.html" %}
{% endblock %} {% endblock %}

View File

@ -4,10 +4,10 @@
{% for metadata in doc.members %}{% if not metadata.internal %} {% for metadata in doc.members %}{% if not metadata.internal %}
<div class="metadata-member"> <div class="metadata-member">
<a name="{$ metadata.name $}-anchor" class="anchor-offset"></a> <a name="{$ metadata.name $}-anchor" class="anchor-offset"></a>
<pre class="prettyprint no-bg" ng-class="{ 'anchor-focused': appCtrl.isApiDocMemberFocused('{$ metadata.name $}') }"> <pre>
<code>{$ metadata.name $}{$ params.paramList(metadata.parameters) | trim $}{$ params.returnType(metadata.returnType) $}</code> <code>{$ metadata.name $}{$ params.paramList(metadata.parameters) | trim $}{$ params.returnType(metadata.returnType) $}</code>
</pre> </pre>
{%- if not metadata.notYetDocumented %}{$ metadata.description | replace('### Example', '') | replace('## Example', '') | replace('# Example', '') | trimBlankLines | marked $}{% endif -%} {%- if not metadata.notYetDocumented %}{$ metadata.description | trimBlankLines | marked $}{% endif -%}
</div> </div>
{% if not loop.last %}<hr class="hr-margin">{% endif %} {% if not loop.last %}<hr class="hr-margin">{% endif %}
{% endif %}{% endfor %} {% endif %}{% endfor %}