Skip to content

Commit b94436d

Browse files
petebacondarwinkara
authored andcommitted
build(docs-infra): process and render ngmodule exports (angular#25734)
All directives and pipes must now be tagged with one ore more public NgModule, from which they are exported. If an item is exported transitively via a re-exported internal NgModule then it may be that the item appears to be exported from more than one public NgModule. For example, there are shared directives that are exported in this way from `FormsModule` and `ReactiveFormsModule`. The doc-gen will error and fail if a directive or pipe is not tagged correctly. NgModule pages now list all the directives and pipes that are exported from it. Directive and Pipe pages now list any NgModule from which they are exported. Packages also now list any NgModules that are contained - previously they were missed. PR Close angular#25734
1 parent bc5cb81 commit b94436d

File tree

10 files changed

+196
-11
lines changed

10 files changed

+196
-11
lines changed

aio/src/styles/2-modules/_api-pages.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,15 @@
102102
clear: left;
103103
}
104104
}
105-
}
105+
}
106106

107107
.from-constructor, .read-only-property {
108108
font-style: italic;
109109
color: $blue;
110110
}
111+
112+
.ngmodule-list {
113+
list-style: none;
114+
padding: 0;
115+
}
111116
}

aio/tools/transforms/angular-api-package/processors/processNgModuleDocs.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
module.exports = function processNgModuleDocs() {
1+
module.exports = function processNgModuleDocs(getDocFromAlias, createDocMessage, log) {
22
return {
3-
$runAfter: ['extractDecoratedClassesProcessor'],
4-
$runBefore: ['docs-processed'],
3+
$runAfter: ['extractDecoratedClassesProcessor', 'computeIdsProcessor'],
4+
$runBefore: ['createSitemap'],
55
$process(docs) {
66
docs.forEach(doc => {
77
if (doc.docType === 'ngmodule') {
@@ -13,6 +13,43 @@ module.exports = function processNgModuleDocs() {
1313
});
1414
}
1515
});
16+
17+
// Match all the directives/pipes to their module
18+
const errors = [];
19+
docs.forEach(doc => {
20+
if (['directive', 'pipe'].indexOf(doc.docType) !== -1) {
21+
if (!doc.ngModules || doc.ngModules.length === 0) {
22+
errors.push(createDocMessage(`"${doc.id}" has no @ngModule tag. Docs of type "${doc.docType}" must have this tag.`, doc));
23+
return;
24+
}
25+
26+
doc.ngModules.forEach((ngModule, index) => {
27+
28+
const ngModuleDocs = getDocFromAlias(ngModule, doc);
29+
30+
if (ngModuleDocs.length === 0) {
31+
errors.push(createDocMessage(`"@ngModule ${ngModule}" does not match a public NgModule`, doc));
32+
return;
33+
}
34+
35+
if (ngModuleDocs.length > 1) {
36+
errors.push(createDocMessage(`"@ngModule ${ngModule}" is ambiguous. Matches: ${ngModuleDocs.map(d => d.id).join(', ')}`, doc));
37+
return;
38+
}
39+
40+
const ngModuleDoc = ngModuleDocs[0];
41+
const container = ngModuleDoc[doc.docType + 's'] = ngModuleDoc[doc.docType + 's'] || [];
42+
container.push(doc);
43+
44+
doc.ngModules[index] = ngModuleDoc;
45+
});
46+
}
47+
});
48+
49+
if (errors.length) {
50+
errors.forEach(error => log.error(error));
51+
throw new Error('Failed to process NgModule relationships.');
52+
}
1653
}
1754
};
1855
};

aio/tools/transforms/angular-api-package/processors/processNgModuleDocs.spec.js

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ const Dgeni = require('dgeni');
33

44
describe('processNgModuleDocs processor', () => {
55
let processor;
6+
let injector;
67
beforeEach(() => {
78
const dgeni = new Dgeni([testPackage('angular-api-package')]);
8-
const injector = dgeni.configureInjector();
9+
injector = dgeni.configureInjector();
910
processor = injector.get('processNgModuleDocs');
1011
});
1112

@@ -14,11 +15,105 @@ describe('processNgModuleDocs processor', () => {
1415
});
1516

1617
it('should run before the correct processor', () => {
17-
expect(processor.$runBefore).toEqual(['docs-processed']);
18+
expect(processor.$runBefore).toEqual(['createSitemap']);
1819
});
1920

2021
it('should run after the correct processor', () => {
21-
expect(processor.$runAfter).toEqual(['extractDecoratedClassesProcessor']);
22+
expect(processor.$runAfter).toEqual(['extractDecoratedClassesProcessor', 'computeIdsProcessor']);
2223
});
2324

25+
it('should non-arrayNgModule options to arrays', () => {
26+
const docs = [{
27+
docType: 'ngmodule',
28+
ngmoduleOptions: {
29+
a: ['AAA'],
30+
b: 'BBB',
31+
c: 42
32+
}
33+
}];
34+
processor.$process(docs);
35+
expect(docs[0].ngmoduleOptions.a).toEqual(['AAA']);
36+
expect(docs[0].ngmoduleOptions.b).toEqual(['BBB']);
37+
expect(docs[0].ngmoduleOptions.c).toEqual([42]);
38+
});
39+
40+
it('should link directive/pipe docs with their NgModule docs', () => {
41+
const aliasMap = injector.get('aliasMap');
42+
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {}};
43+
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {}};
44+
const directive1 = { docType: 'directive', id: 'Directive1', ngModules: ['NgModule1']};
45+
const directive2 = { docType: 'directive', id: 'Directive2', ngModules: ['NgModule2']};
46+
const directive3 = { docType: 'directive', id: 'Directive3', ngModules: ['NgModule1', 'NgModule2']};
47+
const pipe1 = { docType: 'pipe', id: 'Pipe1', ngModules: ['NgModule1']};
48+
const pipe2 = { docType: 'pipe', id: 'Pipe2', ngModules: ['NgModule2']};
49+
const pipe3 = { docType: 'pipe', id: 'Pipe3', ngModules: ['NgModule1', 'NgModule2']};
50+
51+
aliasMap.addDoc(ngModule1);
52+
aliasMap.addDoc(ngModule2);
53+
processor.$process([ngModule1, ngModule2, directive1, directive2, directive3, pipe1, pipe2, pipe3]);
54+
55+
expect(ngModule1.directives).toEqual([directive1, directive3]);
56+
expect(ngModule1.pipes).toEqual([pipe1, pipe3]);
57+
expect(ngModule2.directives).toEqual([directive2, directive3]);
58+
expect(ngModule2.pipes).toEqual([pipe2, pipe3]);
59+
60+
expect(directive1.ngModules).toEqual([ngModule1]);
61+
expect(directive2.ngModules).toEqual([ngModule2]);
62+
expect(directive3.ngModules).toEqual([ngModule1, ngModule2]);
63+
64+
expect(pipe1.ngModules).toEqual([ngModule1]);
65+
expect(pipe2.ngModules).toEqual([ngModule2]);
66+
expect(pipe3.ngModules).toEqual([ngModule1, ngModule2]);
67+
});
68+
69+
it('should error if a pipe/directive does not have a `@ngModule` tag', () => {
70+
const log = injector.get('log');
71+
expect(() => {
72+
processor.$process([{ docType: 'directive', id: 'Directive1' }]);
73+
}).toThrowError('Failed to process NgModule relationships.');
74+
expect(log.error).toHaveBeenCalledWith(
75+
'"Directive1" has no @ngModule tag. Docs of type "directive" must have this tag. - doc "Directive1" (directive) ');
76+
77+
expect(() => {
78+
processor.$process([{ docType: 'pipe', id: 'Pipe1' }]);
79+
}).toThrowError('Failed to process NgModule relationships.');
80+
expect(log.error).toHaveBeenCalledWith(
81+
'"Pipe1" has no @ngModule tag. Docs of type "pipe" must have this tag. - doc "Pipe1" (pipe) ');
82+
});
83+
84+
it('should error if a pipe/directive has an @ngModule tag that does not match an NgModule doc', () => {
85+
const log = injector.get('log');
86+
expect(() => {
87+
processor.$process([{ docType: 'directive', id: 'Directive1', ngModules: ['MissingNgModule'] }]);
88+
}).toThrowError('Failed to process NgModule relationships.');
89+
expect(log.error).toHaveBeenCalledWith(
90+
'"@ngModule MissingNgModule" does not match a public NgModule - doc "Directive1" (directive) ');
91+
92+
expect(() => {
93+
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['MissingNgModule'] }]);
94+
}).toThrowError('Failed to process NgModule relationships.');
95+
expect(log.error).toHaveBeenCalledWith(
96+
'"@ngModule MissingNgModule" does not match a public NgModule - doc "Pipe1" (pipe) ');
97+
});
98+
99+
it('should error if a pipe/directive has an @ngModule tag that matches more than one NgModule doc', () => {
100+
const aliasMap = injector.get('aliasMap');
101+
const log = injector.get('log');
102+
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModuleAlias'], ngmoduleOptions: {}};
103+
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModuleAlias'], ngmoduleOptions: {}};
104+
aliasMap.addDoc(ngModule1);
105+
aliasMap.addDoc(ngModule2);
106+
107+
expect(() => {
108+
processor.$process([{ docType: 'directive', id: 'Directive1', ngModules: ['NgModuleAlias'] }]);
109+
}).toThrowError('Failed to process NgModule relationships.');
110+
expect(log.error).toHaveBeenCalledWith(
111+
'"@ngModule NgModuleAlias" is ambiguous. Matches: NgModule1, NgModule2 - doc "Directive1" (directive) ');
112+
113+
expect(() => {
114+
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['NgModuleAlias'] }]);
115+
}).toThrowError('Failed to process NgModule relationships.');
116+
expect(log.error).toHaveBeenCalledWith(
117+
'"@ngModule NgModuleAlias" is ambiguous. Matches: NgModule1, NgModule2 - doc "Pipe1" (pipe) ');
118+
});
24119
});

aio/tools/transforms/angular-api-package/processors/processPackages.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = function processPackages() {
2626

2727
// Partition the exports into groups by type
2828
if (doc.exports) {
29+
doc.ngmodules = doc.exports.filter(doc => doc.docType === 'ngmodule');
2930
doc.classes = doc.exports.filter(doc => doc.docType === 'class');
3031
doc.decorators = doc.exports.filter(doc => doc.docType === 'decorator');
3132
doc.functions = doc.exports.filter(doc => doc.docType === 'function');
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
module.exports = function() {
2-
return {name: 'ngModule'};
2+
return {
3+
name: 'ngModule',
4+
docProperty: 'ngModules',
5+
multi: true,
6+
};
37
};

aio/tools/transforms/templates/api/directive.template.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
{% block overview %}{% include "includes/directive-overview.html" %}{% endblock %}
66
{% block additional -%}
7-
{% include "includes/selectors.html" %}
7+
{% include "includes/ngmodule.html" %}
8+
{% include "includes/selectors.html" %}
89
{$ directiveHelper.renderBindings(doc.inputs, 'inputs', 'input', 'Inputs') $}
910
{$ directiveHelper.renderBindings(doc.outputs, 'outputs', 'output', 'Outputs') $}
1011
{% include "includes/export-as.html" %}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<h2>NgModules</h2>
2+
<ul class="ngmodule-list">
3+
{% for ngModule in doc.ngModules %}
4+
<li>
5+
<a href="{$ ngModule.path $}">
6+
<code-example language="ts" hideCopy="true" linenums="false" class="no-box">{$ ngModule.name | escape $}</code-example>
7+
</a>
8+
</li>
9+
{% endfor %}
10+
</ul>
11+

aio/tools/transforms/templates/api/includes/pipe-overview.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
{%- if param.isOptional or param.defaultValue !== undefined %} ]{% endif %}
1111
{%- endfor %} }}</code-example>
1212

13+
{% include "includes/ngmodule.html" %}
14+
1315
{% if doc.valueParam.type %}
1416
<h2>Input value</h2>
1517
{$ params.renderParameters([doc.valueParam], 'pipe-parameters', 'pipe-parameter', true) $}

aio/tools/transforms/templates/api/ngmodule.template.html

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ <h2>{$ headingText $}</h2>
2727
</section>
2828
{% endmacro %}
2929

30+
{% macro renderExports(items, cssClass, itemType) %}
31+
<section class="{$ cssClass $}s">
32+
<h2>{$ itemType $}</h2>
33+
<table class="is-full-width list-table {$ cssClass $}-table">
34+
<thead>
35+
<tr><th>Name</th><th>Description</th></tr>
36+
</thead>
37+
<tbody>
38+
{% for item in items %}
39+
<tr>
40+
<td>
41+
<a href="{$ item.path $}">
42+
<code-example language="ts" hideCopy="true" linenums="false" class="no-box">{$ item.name | escape $}</code-example>
43+
</a>
44+
</td>
45+
<td>
46+
{$ item.shortDescription | marked $}
47+
</td>
48+
</tr>
49+
{% endfor %}
50+
</tbody>
51+
</table>
52+
</section>
53+
{% endmacro %}
54+
3055
{% block overview %}
3156
{% include "includes/class-overview.html" %}
3257
{% endblock %}
@@ -47,8 +72,11 @@ <h2>Constructor</h2>
4772
{$ renderTable(doc.ngmoduleOptions.providers, 'providers', 'Providers', 'Provider') $}
4873
{% endif %}
4974

50-
{% if doc.ngmoduleOptions.exports %}
51-
{$ renderTable(doc.ngmoduleOptions.exports, 'exports', 'Exports', 'Export') $}
75+
{% if doc.directives.length %}
76+
{$ renderExports(doc.directives, 'directive', 'Directives') $}
77+
{% endif %}
78+
{% if doc.pipes.length %}
79+
{$ renderExports(doc.pipes, 'pipe', 'Pipes') $}
5280
{% endif %}
5381

5482
{% endblock %}

aio/tools/transforms/templates/api/package.template.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ <h2>Entry points</h2>
3232
{% endif %}
3333

3434
<h2>{% if doc.isPrimaryPackage %}Primary entry{% else %}Entry{% endif %} point exports</h2>
35+
{$ listItems(doc.ngmodules, 'NgModules') $}
3536
{$ listItems(doc.classes, 'Classes') $}
3637
{$ listItems(doc.decorators, 'Decorators') $}
3738
{$ listItems(doc.functions, 'Functions') $}

0 commit comments

Comments
 (0)