UNPKG

115 kBPlain TextView Raw
1import * as fs from 'fs-extra';
2import * as LiveServer from 'live-server';
3import * as _ from 'lodash';
4import * as path from 'path';
5
6import { SyntaxKind } from 'ts-simple-ast';
7
8const chokidar = require('chokidar');
9const marked = require('marked');
10const traverse = require('traverse');
11const crypto = require('crypto');
12
13import { logger } from '../utils/logger';
14
15import Configuration from './configuration';
16
17import DependenciesEngine from './engines/dependencies.engine';
18import ExportEngine from './engines/export.engine';
19import FileEngine from './engines/file.engine';
20import HtmlEngine from './engines/html.engine';
21import I18nEngine from './engines/i18n.engine';
22import MarkdownEngine, { markdownReadedDatas } from './engines/markdown.engine';
23import NgdEngine from './engines/ngd.engine';
24import SearchEngine from './engines/search.engine';
25
26import { AngularDependencies } from './compiler/angular-dependencies';
27import { AngularJSDependencies } from './compiler/angularjs-dependencies';
28
29import AngularVersionUtil from '../utils/angular-version.util';
30import { COMPODOC_CONSTANTS } from '../utils/constants';
31import { COMPODOC_DEFAULTS } from '../utils/defaults';
32import { promiseSequential } from '../utils/promise-sequential';
33import RouterParserUtil from '../utils/router-parser.util';
34
35import {
36 cleanNameWithoutSpaceAndToLowerCase,
37 cleanSourcesForWatch,
38 findMainSourceFolder
39} from '../utils/utils';
40
41import { AdditionalNode } from './interfaces/additional-node.interface';
42import { CoverageData } from './interfaces/coverageData.interface';
43import { LiveServerConfiguration } from './interfaces/live-server-configuration.interface';
44
45let cwd = process.cwd();
46let startTime = new Date();
47let generationPromiseResolve;
48let generationPromiseReject;
49let generationPromise = new Promise((resolve, reject) => {
50 generationPromiseResolve = resolve;
51 generationPromiseReject = reject;
52});
53
54export class Application {
55 /**
56 * Files processed during initial scanning
57 */
58 public files: Array<string>;
59 /**
60 * Files processed during watch scanning
61 */
62 public updatedFiles: Array<string>;
63 /**
64 * Files changed during watch scanning
65 */
66 public watchChangedFiles: Array<string> = [];
67 /**
68 * Boolean for watching status
69 * @type {boolean}
70 */
71 public isWatching: boolean = false;
72
73 /**
74 * Store package.json data
75 */
76 private packageJsonData = {};
77
78 /**
79 * Create a new compodoc application instance.
80 *
81 * @param options An object containing the options that should be used.
82 */
83 constructor(options?: Object) {
84 for (let option in options) {
85 if (typeof Configuration.mainData[option] !== 'undefined') {
86 Configuration.mainData[option] = options[option];
87 }
88 // For documentationMainName, process it outside the loop, for handling conflict with pages name
89 if (option === 'name') {
90 Configuration.mainData.documentationMainName = options[option];
91 }
92 // For documentationMainName, process it outside the loop, for handling conflict with pages name
93 if (option === 'silent') {
94 logger.silent = false;
95 }
96 }
97 }
98
99 /**
100 * Start compodoc process
101 */
102 protected generate(): Promise<{}> {
103 process.on('unhandledRejection', this.unhandledRejectionListener);
104 process.on('uncaughtException', this.uncaughtExceptionListener);
105
106 I18nEngine.init(Configuration.mainData.language);
107
108 if (
109 Configuration.mainData.output.charAt(Configuration.mainData.output.length - 1) !== '/'
110 ) {
111 Configuration.mainData.output += '/';
112 }
113
114 if (Configuration.mainData.exportFormat !== COMPODOC_DEFAULTS.exportFormat) {
115 this.processPackageJson();
116 } else {
117 HtmlEngine.init(Configuration.mainData.templates).then(() => this.processPackageJson());
118 }
119 return generationPromise;
120 }
121
122 private endCallback() {
123 process.removeListener('unhandledRejection', this.unhandledRejectionListener);
124 process.removeListener('uncaughtException', this.uncaughtExceptionListener);
125 }
126
127 private unhandledRejectionListener(err, p) {
128 console.log('Unhandled Rejection at:', p, 'reason:', err);
129 logger.error(
130 'Sorry, but there was a problem during parsing or generation of the documentation. Please fill an issue on github. (https://github.com/compodoc/compodoc/issues/new)'
131 ); // tslint:disable-line
132 process.exit(1);
133 }
134
135 private uncaughtExceptionListener(err) {
136 logger.error(err);
137 logger.error(
138 'Sorry, but there was a problem during parsing or generation of the documentation. Please fill an issue on github. (https://github.com/compodoc/compodoc/issues/new)'
139 ); // tslint:disable-line
140 process.exit(1);
141 }
142
143 /**
144 * Start compodoc documentation coverage
145 */
146 protected testCoverage() {
147 this.getDependenciesData();
148 }
149
150 /**
151 * Store files for initial processing
152 * @param {Array<string>} files Files found during source folder and tsconfig scan
153 */
154 public setFiles(files: Array<string>) {
155 this.files = files;
156 }
157
158 /**
159 * Store files for watch processing
160 * @param {Array<string>} files Files found during source folder and tsconfig scan
161 */
162 public setUpdatedFiles(files: Array<string>) {
163 this.updatedFiles = files;
164 }
165
166 /**
167 * Return a boolean indicating presence of one TypeScript file in updatedFiles list
168 * @return {boolean} Result of scan
169 */
170 public hasWatchedFilesTSFiles(): boolean {
171 let result = false;
172
173 _.forEach(this.updatedFiles, file => {
174 if (path.extname(file) === '.ts') {
175 result = true;
176 }
177 });
178
179 return result;
180 }
181
182 /**
183 * Return a boolean indicating presence of one root markdown files in updatedFiles list
184 * @return {boolean} Result of scan
185 */
186 public hasWatchedFilesRootMarkdownFiles(): boolean {
187 let result = false;
188
189 _.forEach(this.updatedFiles, file => {
190 if (path.extname(file) === '.md' && path.dirname(file) === cwd) {
191 result = true;
192 }
193 });
194
195 return result;
196 }
197
198 /**
199 * Clear files for watch processing
200 */
201 public clearUpdatedFiles(): void {
202 this.updatedFiles = [];
203 this.watchChangedFiles = [];
204 }
205
206 private processPackageJson(): void {
207 logger.info('Searching package.json file');
208 FileEngine.get(cwd + path.sep + 'package.json').then(
209 packageData => {
210 let parsedData = JSON.parse(packageData);
211 this.packageJsonData = parsedData;
212 if (
213 typeof parsedData.name !== 'undefined' &&
214 Configuration.mainData.documentationMainName === COMPODOC_DEFAULTS.title
215 ) {
216 Configuration.mainData.documentationMainName =
217 parsedData.name + ' documentation';
218 }
219 if (typeof parsedData.description !== 'undefined') {
220 Configuration.mainData.documentationMainDescription = parsedData.description;
221 }
222 Configuration.mainData.angularVersion = AngularVersionUtil.getAngularVersionOfProject(
223 parsedData
224 );
225 logger.info('package.json file found');
226
227 if (!Configuration.mainData.disableDependencies) {
228 if (typeof parsedData.dependencies !== 'undefined') {
229 this.processPackageDependencies(parsedData.dependencies);
230 }
231 if (typeof parsedData.peerDependencies !== 'undefined') {
232 this.processPackagePeerDependencies(parsedData.peerDependencies);
233 }
234 }
235
236 this.processMarkdowns().then(
237 () => {
238 this.getDependenciesData();
239 },
240 errorMessage => {
241 logger.error(errorMessage);
242 }
243 );
244 },
245 errorMessage => {
246 logger.error(errorMessage);
247 logger.error('Continuing without package.json file');
248 this.processMarkdowns().then(
249 () => {
250 this.getDependenciesData();
251 },
252 errorMessage1 => {
253 logger.error(errorMessage1);
254 }
255 );
256 }
257 );
258 }
259
260 private processPackagePeerDependencies(dependencies): void {
261 logger.info('Processing package.json peerDependencies');
262 Configuration.mainData.packagePeerDependencies = dependencies;
263 if (!Configuration.hasPage('dependencies')) {
264 Configuration.addPage({
265 name: 'dependencies',
266 id: 'packageDependencies',
267 context: 'package-dependencies',
268 depth: 0,
269 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
270 });
271 }
272 }
273
274 private processPackageDependencies(dependencies): void {
275 logger.info('Processing package.json dependencies');
276 Configuration.mainData.packageDependencies = dependencies;
277 Configuration.addPage({
278 name: 'dependencies',
279 id: 'packageDependencies',
280 context: 'package-dependencies',
281 depth: 0,
282 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
283 });
284 }
285
286 private processMarkdowns(): Promise<any> {
287 logger.info(
288 'Searching README.md, CHANGELOG.md, CONTRIBUTING.md, LICENSE.md, TODO.md files'
289 );
290
291 return new Promise((resolve, reject) => {
292 let i = 0;
293 let markdowns = ['readme', 'changelog', 'contributing', 'license', 'todo'];
294 let numberOfMarkdowns = 5;
295 let loop = () => {
296 if (i < numberOfMarkdowns) {
297 MarkdownEngine.getTraditionalMarkdown(markdowns[i].toUpperCase()).then(
298 (readmeData: markdownReadedDatas) => {
299 Configuration.addPage({
300 name: markdowns[i] === 'readme' ? 'index' : markdowns[i],
301 context: 'getting-started',
302 id: 'getting-started',
303 markdown: readmeData.markdown,
304 data: readmeData.rawData,
305 depth: 0,
306 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
307 });
308 if (markdowns[i] === 'readme') {
309 Configuration.mainData.readme = true;
310 Configuration.addPage({
311 name: 'overview',
312 id: 'overview',
313 context: 'overview',
314 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
315 });
316 } else {
317 Configuration.mainData.markdowns.push({
318 name: markdowns[i],
319 uppername: markdowns[i].toUpperCase(),
320 depth: 0,
321 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
322 });
323 }
324 logger.info(`${markdowns[i].toUpperCase()}.md file found`);
325 i++;
326 loop();
327 },
328 errorMessage => {
329 logger.warn(errorMessage);
330 logger.warn(`Continuing without ${markdowns[i].toUpperCase()}.md file`);
331 if (markdowns[i] === 'readme') {
332 Configuration.addPage({
333 name: 'index',
334 id: 'index',
335 context: 'overview',
336 depth: 0,
337 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
338 });
339 }
340 i++;
341 loop();
342 }
343 );
344 } else {
345 resolve();
346 }
347 };
348 loop();
349 });
350 }
351
352 private rebuildRootMarkdowns(): void {
353 logger.info(
354 'Regenerating README.md, CHANGELOG.md, CONTRIBUTING.md, LICENSE.md, TODO.md pages'
355 );
356
357 let actions = [];
358
359 Configuration.resetRootMarkdownPages();
360
361 actions.push(() => {
362 return this.processMarkdowns();
363 });
364
365 promiseSequential(actions)
366 .then(res => {
367 this.processPages();
368 this.clearUpdatedFiles();
369 })
370 .catch(errorMessage => {
371 logger.error(errorMessage);
372 });
373 }
374
375 /**
376 * Get dependency data for small group of updated files during watch process
377 */
378 private getMicroDependenciesData(): void {
379 logger.info('Get diff dependencies data');
380
381 let dependenciesClass: AngularDependencies | AngularJSDependencies = AngularDependencies;
382 Configuration.mainData.angularProject = true;
383
384 if (this.detectAngularJSProjects()) {
385 logger.info('AngularJS project detected');
386 Configuration.mainData.angularProject = false;
387 Configuration.mainData.angularJSProject = true;
388 dependenciesClass = AngularJSDependencies;
389 }
390
391 let crawler = new dependenciesClass(
392 this.updatedFiles,
393 {
394 tsconfigDirectory: path.dirname(Configuration.mainData.tsconfig)
395 },
396 Configuration,
397 RouterParserUtil
398 );
399
400 let dependenciesData = crawler.getDependencies();
401
402 DependenciesEngine.update(dependenciesData);
403
404 this.prepareJustAFewThings(dependenciesData);
405 }
406
407 /**
408 * Rebuild external documentation during watch process
409 */
410 private rebuildExternalDocumentation(): void {
411 logger.info('Rebuild external documentation');
412
413 let actions = [];
414
415 Configuration.resetAdditionalPages();
416
417 if (Configuration.mainData.includes !== '') {
418 actions.push(() => {
419 return this.prepareExternalIncludes();
420 });
421 }
422
423 promiseSequential(actions)
424 .then(res => {
425 this.processPages();
426 this.clearUpdatedFiles();
427 })
428 .catch(errorMessage => {
429 logger.error(errorMessage);
430 });
431 }
432
433 private detectAngularJSProjects() {
434 let result = false;
435 if (typeof this.packageJsonData.dependencies !== 'undefined') {
436 if (typeof this.packageJsonData.dependencies.angular !== 'undefined') {
437 result = true;
438 } else {
439 let countJSFiles = 0;
440 this.files.forEach(file => {
441 if (path.extname(file) === '.js') {
442 countJSFiles += 1;
443 }
444 });
445 let percentOfJSFiles = (countJSFiles * 100) / this.files.length;
446 if (percentOfJSFiles >= 75) {
447 result = true;
448 }
449 }
450 }
451 return false;
452 }
453
454 private getDependenciesData(): void {
455 logger.info('Get dependencies data');
456
457 /**
458 * AngularJS detection strategy :
459 * - if in package.json
460 * - if 75% of scanned files are *.js files
461 */
462 let dependenciesClass: AngularDependencies | AngularJSDependencies = AngularDependencies;
463 Configuration.mainData.angularProject = true;
464
465 if (this.detectAngularJSProjects()) {
466 logger.info('AngularJS project detected');
467 Configuration.mainData.angularProject = false;
468 Configuration.mainData.angularJSProject = true;
469 dependenciesClass = AngularJSDependencies;
470 }
471
472 let crawler = new dependenciesClass(
473 this.files,
474 {
475 tsconfigDirectory: path.dirname(Configuration.mainData.tsconfig)
476 },
477 Configuration,
478 RouterParserUtil
479 );
480
481 let dependenciesData = crawler.getDependencies();
482
483 DependenciesEngine.init(dependenciesData);
484
485 Configuration.mainData.routesLength = RouterParserUtil.routesLength();
486
487 this.printStatistics();
488
489 this.prepareEverything();
490 }
491
492 private prepareJustAFewThings(diffCrawledData): void {
493 let actions = [];
494
495 Configuration.resetPages();
496
497 if (!Configuration.mainData.disableRoutesGraph) {
498 actions.push(() => this.prepareRoutes());
499 }
500
501 if (diffCrawledData.components.length > 0) {
502 actions.push(() => this.prepareComponents());
503 }
504 if (diffCrawledData.controllers.length > 0) {
505 actions.push(() => this.prepareControllers());
506 }
507 if (diffCrawledData.modules.length > 0) {
508 actions.push(() => this.prepareModules());
509 }
510
511 if (diffCrawledData.directives.length > 0) {
512 actions.push(() => this.prepareDirectives());
513 }
514
515 if (diffCrawledData.injectables.length > 0) {
516 actions.push(() => this.prepareInjectables());
517 }
518
519 if (diffCrawledData.interceptors.length > 0) {
520 actions.push(() => this.prepareInterceptors());
521 }
522
523 if (diffCrawledData.guards.length > 0) {
524 actions.push(() => this.prepareGuards());
525 }
526
527 if (diffCrawledData.pipes.length > 0) {
528 actions.push(() => this.preparePipes());
529 }
530
531 if (diffCrawledData.classes.length > 0) {
532 actions.push(() => this.prepareClasses());
533 }
534
535 if (diffCrawledData.interfaces.length > 0) {
536 actions.push(() => this.prepareInterfaces());
537 }
538
539 if (
540 diffCrawledData.miscellaneous.variables.length > 0 ||
541 diffCrawledData.miscellaneous.functions.length > 0 ||
542 diffCrawledData.miscellaneous.typealiases.length > 0 ||
543 diffCrawledData.miscellaneous.enumerations.length > 0
544 ) {
545 actions.push(() => this.prepareMiscellaneous());
546 }
547
548 if (!Configuration.mainData.disableCoverage) {
549 actions.push(() => this.prepareCoverage());
550 }
551
552 promiseSequential(actions)
553 .then(res => {
554 this.processGraphs();
555 this.clearUpdatedFiles();
556 })
557 .catch(errorMessage => {
558 logger.error(errorMessage);
559 });
560 }
561
562 private printStatistics() {
563 logger.info('-------------------');
564 logger.info('Project statistics ');
565 if (DependenciesEngine.modules.length > 0) {
566 logger.info(`- files : ${this.files.length}`);
567 }
568 if (DependenciesEngine.modules.length > 0) {
569 logger.info(`- module : ${DependenciesEngine.modules.length}`);
570 }
571 if (DependenciesEngine.components.length > 0) {
572 logger.info(`- component : ${DependenciesEngine.components.length}`);
573 }
574 if (DependenciesEngine.controllers.length > 0) {
575 logger.info(`- controller : ${DependenciesEngine.controllers.length}`);
576 }
577 if (DependenciesEngine.directives.length > 0) {
578 logger.info(`- directive : ${DependenciesEngine.directives.length}`);
579 }
580 if (DependenciesEngine.injectables.length > 0) {
581 logger.info(`- injectable : ${DependenciesEngine.injectables.length}`);
582 }
583 if (DependenciesEngine.interceptors.length > 0) {
584 logger.info(`- injector : ${DependenciesEngine.interceptors.length}`);
585 }
586 if (DependenciesEngine.guards.length > 0) {
587 logger.info(`- guard : ${DependenciesEngine.guards.length}`);
588 }
589 if (DependenciesEngine.pipes.length > 0) {
590 logger.info(`- pipe : ${DependenciesEngine.pipes.length}`);
591 }
592 if (DependenciesEngine.classes.length > 0) {
593 logger.info(`- class : ${DependenciesEngine.classes.length}`);
594 }
595 if (DependenciesEngine.interfaces.length > 0) {
596 logger.info(`- interface : ${DependenciesEngine.interfaces.length}`);
597 }
598 if (Configuration.mainData.routesLength > 0) {
599 logger.info(`- route : ${Configuration.mainData.routesLength}`);
600 }
601 logger.info('-------------------');
602 }
603
604 private prepareEverything() {
605 let actions = [];
606
607 actions.push(() => {
608 return this.prepareComponents();
609 });
610 actions.push(() => {
611 return this.prepareModules();
612 });
613
614 if (DependenciesEngine.directives.length > 0) {
615 actions.push(() => {
616 return this.prepareDirectives();
617 });
618 }
619
620 if (DependenciesEngine.controllers.length > 0) {
621 actions.push(() => {
622 return this.prepareControllers();
623 });
624 }
625
626 if (DependenciesEngine.injectables.length > 0) {
627 actions.push(() => {
628 return this.prepareInjectables();
629 });
630 }
631
632 if (DependenciesEngine.interceptors.length > 0) {
633 actions.push(() => {
634 return this.prepareInterceptors();
635 });
636 }
637
638 if (DependenciesEngine.guards.length > 0) {
639 actions.push(() => {
640 return this.prepareGuards();
641 });
642 }
643
644 if (
645 DependenciesEngine.routes &&
646 DependenciesEngine.routes.children.length > 0 &&
647 !Configuration.mainData.disableRoutesGraph
648 ) {
649 actions.push(() => {
650 return this.prepareRoutes();
651 });
652 }
653
654 if (DependenciesEngine.pipes.length > 0) {
655 actions.push(() => {
656 return this.preparePipes();
657 });
658 }
659
660 if (DependenciesEngine.classes.length > 0) {
661 actions.push(() => {
662 return this.prepareClasses();
663 });
664 }
665
666 if (DependenciesEngine.interfaces.length > 0) {
667 actions.push(() => {
668 return this.prepareInterfaces();
669 });
670 }
671
672 if (
673 DependenciesEngine.miscellaneous.variables.length > 0 ||
674 DependenciesEngine.miscellaneous.functions.length > 0 ||
675 DependenciesEngine.miscellaneous.typealiases.length > 0 ||
676 DependenciesEngine.miscellaneous.enumerations.length > 0
677 ) {
678 actions.push(() => {
679 return this.prepareMiscellaneous();
680 });
681 }
682
683 if (!Configuration.mainData.disableCoverage) {
684 actions.push(() => {
685 return this.prepareCoverage();
686 });
687 }
688
689 if (Configuration.mainData.unitTestCoverage !== '') {
690 actions.push(() => {
691 return this.prepareUnitTestCoverage();
692 });
693 }
694
695 if (Configuration.mainData.includes !== '') {
696 actions.push(() => {
697 return this.prepareExternalIncludes();
698 });
699 }
700
701 promiseSequential(actions)
702 .then(res => {
703 if (Configuration.mainData.exportFormat !== COMPODOC_DEFAULTS.exportFormat) {
704 if (
705 COMPODOC_DEFAULTS.exportFormatsSupported.indexOf(
706 Configuration.mainData.exportFormat
707 ) > -1
708 ) {
709 logger.info(
710 `Generating documentation in export format ${Configuration.mainData.exportFormat}`
711 );
712 ExportEngine.export(
713 Configuration.mainData.output,
714 Configuration.mainData
715 ).then(() => {
716 generationPromiseResolve();
717 this.endCallback();
718 logger.info(
719 'Documentation generated in ' +
720 Configuration.mainData.output +
721 ' in ' +
722 this.getElapsedTime() +
723 ' seconds'
724 );
725 });
726 } else {
727 logger.warn(`Exported format not supported`);
728 }
729 } else {
730 this.processGraphs();
731 }
732 })
733 .catch(errorMessage => {
734 logger.error(errorMessage);
735 });
736 }
737
738 private getIncludedPathForFile(file) {
739 return path.join(Configuration.mainData.includes, file);
740 }
741
742 private prepareExternalIncludes() {
743 logger.info('Adding external markdown files');
744 // Scan include folder for files detailed in summary.json
745 // For each file, add to Configuration.mainData.additionalPages
746 // Each file will be converted to html page, inside COMPODOC_DEFAULTS.additionalEntryPath
747 return new Promise((resolve, reject) => {
748 FileEngine.get(this.getIncludedPathForFile('summary.json')).then(
749 summaryData => {
750 logger.info('Additional documentation: summary.json file found');
751
752 const parsedSummaryData = JSON.parse(summaryData);
753
754 let that = this;
755 let lastLevelOnePage = undefined;
756
757 traverse(parsedSummaryData).forEach(function() {
758 // tslint:disable-next-line:no-invalid-this
759 if (this.notRoot && typeof this.node === 'object') {
760 // tslint:disable-next-line:no-invalid-this
761 let rawPath = this.path;
762 // tslint:disable-next-line:no-invalid-this
763 let additionalNode: AdditionalNode = this.node;
764 let file = additionalNode.file;
765 let title = additionalNode.title;
766 let finalPath = Configuration.mainData.includesFolder;
767
768 let finalDepth = rawPath.filter(el => {
769 return !isNaN(parseInt(el, 10));
770 });
771
772 if (typeof file !== 'undefined' && typeof title !== 'undefined') {
773 const url = cleanNameWithoutSpaceAndToLowerCase(title);
774
775 /**
776 * Id created with title + file path hash, seems to be hypothetically unique here
777 */
778 const id = crypto
779 .createHash('md5')
780 .update(title + file)
781 .digest('hex');
782
783 // tslint:disable-next-line:no-invalid-this
784 this.node.id = id;
785
786 let lastElementRootTree = undefined;
787 finalDepth.forEach(el => {
788 let elementTree =
789 typeof lastElementRootTree === 'undefined'
790 ? parsedSummaryData
791 : lastElementRootTree;
792 if (typeof elementTree.children !== 'undefined') {
793 elementTree = elementTree.children[el];
794 } else {
795 elementTree = elementTree[el];
796 }
797 finalPath +=
798 '/' +
799 cleanNameWithoutSpaceAndToLowerCase(elementTree.title);
800 lastElementRootTree = elementTree;
801 });
802
803 finalPath = finalPath.replace('/' + url, '');
804 let markdownFile = MarkdownEngine.getTraditionalMarkdownSync(
805 that.getIncludedPathForFile(file)
806 );
807
808 if (finalDepth.length > 5) {
809 logger.error('Only 5 levels of depth are supported');
810 } else {
811 let _page = {
812 name: title,
813 id: id,
814 filename: url,
815 context: 'additional-page',
816 path: finalPath,
817 additionalPage: markdownFile,
818 depth: finalDepth.length,
819 childrenLength: additionalNode.children
820 ? additionalNode.children.length
821 : 0,
822 children: [],
823 lastChild: false,
824 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
825 };
826 if (finalDepth.length === 1) {
827 lastLevelOnePage = _page;
828 }
829 if (finalDepth.length > 1) {
830 // store all child pages of the last root level 1 page inside it
831 lastLevelOnePage.children.push(_page);
832 } else {
833 Configuration.addAdditionalPage(_page);
834 }
835 }
836 }
837 }
838 });
839
840 resolve();
841 },
842 errorMessage => {
843 logger.error(errorMessage);
844 reject('Error during Additional documentation generation');
845 }
846 );
847 });
848 }
849
850 public prepareModules(someModules?): Promise<any> {
851 logger.info('Prepare modules');
852 let i = 0;
853 let _modules = someModules ? someModules : DependenciesEngine.getModules();
854
855 return new Promise((resolve, reject) => {
856 Configuration.mainData.modules = _modules.map(ngModule => {
857 ngModule.compodocLinks = {
858 components: [],
859 controllers: [],
860 directives: [],
861 injectables: [],
862 pipes: []
863 };
864 ['declarations', 'bootstrap', 'imports', 'exports', 'controllers'].forEach(
865 metadataType => {
866 ngModule[metadataType] = ngModule[metadataType].filter(metaDataItem => {
867 switch (metaDataItem.type) {
868 case 'directive':
869 return DependenciesEngine.getDirectives().some(directive => {
870 let selectedDirective;
871 if (typeof metaDataItem.id !== 'undefined') {
872 selectedDirective =
873 (directive as any).id === metaDataItem.id;
874 } else {
875 selectedDirective =
876 (directive as any).name === metaDataItem.name;
877 }
878 if (
879 selectedDirective &&
880 !ngModule.compodocLinks.directives.includes(directive)
881 ) {
882 ngModule.compodocLinks.directives.push(directive);
883 }
884 return selectedDirective;
885 });
886
887 case 'component':
888 return DependenciesEngine.getComponents().some(component => {
889 let selectedComponent;
890 if (typeof metaDataItem.id !== 'undefined') {
891 selectedComponent =
892 (component as any).id === metaDataItem.id;
893 } else {
894 selectedComponent =
895 (component as any).name === metaDataItem.name;
896 }
897 if (
898 selectedComponent &&
899 !ngModule.compodocLinks.components.includes(component)
900 ) {
901 ngModule.compodocLinks.components.push(component);
902 }
903 return selectedComponent;
904 });
905
906 case 'controller':
907 return DependenciesEngine.getControllers().some(controller => {
908 let selectedController;
909 if (typeof metaDataItem.id !== 'undefined') {
910 selectedController =
911 (controller as any).id === metaDataItem.id;
912 } else {
913 selectedController =
914 (controller as any).name === metaDataItem.name;
915 }
916 if (
917 selectedController &&
918 !ngModule.compodocLinks.controllers.includes(controller)
919 ) {
920 ngModule.compodocLinks.controllers.push(controller);
921 }
922 return selectedController;
923 });
924
925 case 'module':
926 return DependenciesEngine.getModules().some(
927 module => (module as any).name === metaDataItem.name
928 );
929
930 case 'pipe':
931 return DependenciesEngine.getPipes().some(pipe => {
932 let selectedPipe;
933 if (typeof metaDataItem.id !== 'undefined') {
934 selectedPipe = (pipe as any).id === metaDataItem.id;
935 } else {
936 selectedPipe = (pipe as any).name === metaDataItem.name;
937 }
938 if (
939 selectedPipe &&
940 !ngModule.compodocLinks.pipes.includes(pipe)
941 ) {
942 ngModule.compodocLinks.pipes.push(pipe);
943 }
944 return selectedPipe;
945 });
946
947 default:
948 return true;
949 }
950 });
951 }
952 );
953 ngModule.providers = ngModule.providers.filter(provider => {
954 return (
955 DependenciesEngine.getInjectables().some(injectable => {
956 let selectedInjectable = (injectable as any).name === provider.name;
957 if (
958 selectedInjectable &&
959 !ngModule.compodocLinks.injectables.includes(injectable)
960 ) {
961 ngModule.compodocLinks.injectables.push(injectable);
962 }
963 return selectedInjectable;
964 }) ||
965 DependenciesEngine.getInterceptors().some(
966 interceptor => (interceptor as any).name === provider.name
967 )
968 );
969 });
970 // Try fixing type undefined for each providers
971 _.forEach(ngModule.providers, provider => {
972 if (
973 DependenciesEngine.getInjectables().find(
974 injectable => (injectable as any).name === provider.name
975 )
976 ) {
977 provider.type = 'injectable';
978 }
979 if (
980 DependenciesEngine.getInterceptors().find(
981 interceptor => (interceptor as any).name === provider.name
982 )
983 ) {
984 provider.type = 'interceptor';
985 }
986 });
987 // Order things
988 ngModule.compodocLinks.components = _.sortBy(ngModule.compodocLinks.components, [
989 'name'
990 ]);
991 ngModule.compodocLinks.controllers = _.sortBy(ngModule.compodocLinks.controllers, [
992 'name'
993 ]);
994 ngModule.compodocLinks.directives = _.sortBy(ngModule.compodocLinks.directives, [
995 'name'
996 ]);
997 ngModule.compodocLinks.injectables = _.sortBy(ngModule.compodocLinks.injectables, [
998 'name'
999 ]);
1000 ngModule.compodocLinks.pipes = _.sortBy(ngModule.compodocLinks.pipes, ['name']);
1001
1002 ngModule.declarations = _.sortBy(ngModule.declarations, ['name']);
1003 ngModule.entryComponents = _.sortBy(ngModule.entryComponents, ['name']);
1004 ngModule.providers = _.sortBy(ngModule.providers, ['name']);
1005 ngModule.imports = _.sortBy(ngModule.imports, ['name']);
1006 ngModule.exports = _.sortBy(ngModule.exports, ['name']);
1007
1008 return ngModule;
1009 });
1010
1011 Configuration.addPage({
1012 name: 'modules',
1013 id: 'modules',
1014 context: 'modules',
1015 depth: 0,
1016 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
1017 });
1018
1019 let len = Configuration.mainData.modules.length;
1020 let loop = () => {
1021 if (i < len) {
1022 if (
1023 MarkdownEngine.hasNeighbourReadmeFile(
1024 Configuration.mainData.modules[i].file
1025 )
1026 ) {
1027 logger.info(
1028 ` ${Configuration.mainData.modules[i].name} has a README file, include it`
1029 );
1030 let readme = MarkdownEngine.readNeighbourReadmeFile(
1031 Configuration.mainData.modules[i].file
1032 );
1033 Configuration.mainData.modules[i].readme = marked(readme);
1034 }
1035 Configuration.addPage({
1036 path: 'modules',
1037 name: Configuration.mainData.modules[i].name,
1038 id: Configuration.mainData.modules[i].id,
1039 navTabs: this.getNavTabs(Configuration.mainData.modules[i]),
1040 context: 'module',
1041 module: Configuration.mainData.modules[i],
1042 depth: 1,
1043 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1044 });
1045 i++;
1046 loop();
1047 } else {
1048 resolve();
1049 }
1050 };
1051 loop();
1052 });
1053 }
1054
1055 public preparePipes = (somePipes?) => {
1056 logger.info('Prepare pipes');
1057 Configuration.mainData.pipes = somePipes ? somePipes : DependenciesEngine.getPipes();
1058
1059 return new Promise((resolve, reject) => {
1060 let i = 0;
1061 let len = Configuration.mainData.pipes.length;
1062 let loop = () => {
1063 if (i < len) {
1064 let pipe = Configuration.mainData.pipes[i];
1065 if (MarkdownEngine.hasNeighbourReadmeFile(pipe.file)) {
1066 logger.info(` ${pipe.name} has a README file, include it`);
1067 let readme = MarkdownEngine.readNeighbourReadmeFile(pipe.file);
1068 pipe.readme = marked(readme);
1069 }
1070 let page = {
1071 path: 'pipes',
1072 name: pipe.name,
1073 id: pipe.id,
1074 navTabs: this.getNavTabs(pipe),
1075 context: 'pipe',
1076 pipe: pipe,
1077 depth: 1,
1078 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1079 };
1080 if (pipe.isDuplicate) {
1081 page.name += '-' + pipe.duplicateId;
1082 }
1083 Configuration.addPage(page);
1084 i++;
1085 loop();
1086 } else {
1087 resolve();
1088 }
1089 };
1090 loop();
1091 });
1092 };
1093
1094 public prepareClasses = (someClasses?) => {
1095 logger.info('Prepare classes');
1096 Configuration.mainData.classes = someClasses
1097 ? someClasses
1098 : DependenciesEngine.getClasses();
1099
1100 return new Promise((resolve, reject) => {
1101 let i = 0;
1102 let len = Configuration.mainData.classes.length;
1103 let loop = () => {
1104 if (i < len) {
1105 let classe = Configuration.mainData.classes[i];
1106 if (MarkdownEngine.hasNeighbourReadmeFile(classe.file)) {
1107 logger.info(` ${classe.name} has a README file, include it`);
1108 let readme = MarkdownEngine.readNeighbourReadmeFile(classe.file);
1109 classe.readme = marked(readme);
1110 }
1111 let page = {
1112 path: 'classes',
1113 name: classe.name,
1114 id: classe.id,
1115 navTabs: this.getNavTabs(classe),
1116 context: 'class',
1117 class: classe,
1118 depth: 1,
1119 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1120 };
1121 if (classe.isDuplicate) {
1122 page.name += '-' + classe.duplicateId;
1123 }
1124 Configuration.addPage(page);
1125 i++;
1126 loop();
1127 } else {
1128 resolve();
1129 }
1130 };
1131 loop();
1132 });
1133 };
1134
1135 public prepareInterfaces(someInterfaces?) {
1136 logger.info('Prepare interfaces');
1137 Configuration.mainData.interfaces = someInterfaces
1138 ? someInterfaces
1139 : DependenciesEngine.getInterfaces();
1140
1141 return new Promise((resolve, reject) => {
1142 let i = 0;
1143 let len = Configuration.mainData.interfaces.length;
1144 let loop = () => {
1145 if (i < len) {
1146 let interf = Configuration.mainData.interfaces[i];
1147 if (MarkdownEngine.hasNeighbourReadmeFile(interf.file)) {
1148 logger.info(` ${interf.name} has a README file, include it`);
1149 let readme = MarkdownEngine.readNeighbourReadmeFile(interf.file);
1150 interf.readme = marked(readme);
1151 }
1152 let page = {
1153 path: 'interfaces',
1154 name: interf.name,
1155 id: interf.id,
1156 navTabs: this.getNavTabs(interf),
1157 context: 'interface',
1158 interface: interf,
1159 depth: 1,
1160 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1161 };
1162 if (interf.isDuplicate) {
1163 page.name += '-' + interf.duplicateId;
1164 }
1165 Configuration.addPage(page);
1166 i++;
1167 loop();
1168 } else {
1169 resolve();
1170 }
1171 };
1172 loop();
1173 });
1174 }
1175
1176 public prepareMiscellaneous(someMisc?) {
1177 logger.info('Prepare miscellaneous');
1178 Configuration.mainData.miscellaneous = someMisc
1179 ? someMisc
1180 : DependenciesEngine.getMiscellaneous();
1181
1182 return new Promise((resolve, reject) => {
1183 if (Configuration.mainData.miscellaneous.functions.length > 0) {
1184 Configuration.addPage({
1185 path: 'miscellaneous',
1186 name: 'functions',
1187 id: 'miscellaneous-functions',
1188 context: 'miscellaneous-functions',
1189 depth: 1,
1190 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1191 });
1192 }
1193 if (Configuration.mainData.miscellaneous.variables.length > 0) {
1194 Configuration.addPage({
1195 path: 'miscellaneous',
1196 name: 'variables',
1197 id: 'miscellaneous-variables',
1198 context: 'miscellaneous-variables',
1199 depth: 1,
1200 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1201 });
1202 }
1203 if (Configuration.mainData.miscellaneous.typealiases.length > 0) {
1204 Configuration.addPage({
1205 path: 'miscellaneous',
1206 name: 'typealiases',
1207 id: 'miscellaneous-typealiases',
1208 context: 'miscellaneous-typealiases',
1209 depth: 1,
1210 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1211 });
1212 }
1213 if (Configuration.mainData.miscellaneous.enumerations.length > 0) {
1214 Configuration.addPage({
1215 path: 'miscellaneous',
1216 name: 'enumerations',
1217 id: 'miscellaneous-enumerations',
1218 context: 'miscellaneous-enumerations',
1219 depth: 1,
1220 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1221 });
1222 }
1223
1224 resolve();
1225 });
1226 }
1227
1228 private handleTemplateurl(component): Promise<any> {
1229 let dirname = path.dirname(component.file);
1230 let templatePath = path.resolve(dirname + path.sep + component.templateUrl);
1231
1232 if (!FileEngine.existsSync(templatePath)) {
1233 let err = `Cannot read template for ${component.name}`;
1234 logger.error(err);
1235 return new Promise((resolve, reject) => {});
1236 }
1237
1238 return FileEngine.get(templatePath).then(
1239 data => (component.templateData = data),
1240 err => {
1241 logger.error(err);
1242 return Promise.reject('');
1243 }
1244 );
1245 }
1246
1247 private handleStyles(component): Promise<any> {
1248 let styles = component.styles;
1249 component.stylesData = '';
1250 return new Promise((resolveStyles, rejectStyles) => {
1251 styles.forEach(style => {
1252 component.stylesData = component.stylesData + style + '\n';
1253 });
1254 resolveStyles();
1255 });
1256 }
1257
1258 private handleStyleurls(component): Promise<any> {
1259 let dirname = path.dirname(component.file);
1260
1261 let styleDataPromise = component.styleUrls.map(styleUrl => {
1262 let stylePath = path.resolve(dirname + path.sep + styleUrl);
1263
1264 if (!FileEngine.existsSync(stylePath)) {
1265 let err = `Cannot read style url ${stylePath} for ${component.name}`;
1266 logger.error(err);
1267 return new Promise((resolve, reject) => {});
1268 }
1269
1270 return new Promise((resolve, reject) => {
1271 FileEngine.get(stylePath).then(data => {
1272 resolve({
1273 data,
1274 styleUrl
1275 });
1276 });
1277 });
1278 });
1279
1280 return Promise.all(styleDataPromise).then(
1281 data => (component.styleUrlsData = data),
1282 err => {
1283 logger.error(err);
1284 return Promise.reject('');
1285 }
1286 );
1287 }
1288
1289 private getNavTabs(dependency): Array<any> {
1290 let navTabConfig = Configuration.mainData.navTabConfig;
1291 navTabConfig =
1292 navTabConfig.length === 0
1293 ? _.cloneDeep(COMPODOC_CONSTANTS.navTabDefinitions)
1294 : navTabConfig;
1295 let matchDepType = (depType: string) => {
1296 return depType === 'all' || depType === dependency.type;
1297 };
1298
1299 let navTabs = [];
1300 _.forEach(navTabConfig, customTab => {
1301 let navTab = _.find(COMPODOC_CONSTANTS.navTabDefinitions, { id: customTab.id });
1302 if (!navTab) {
1303 throw new Error(`Invalid tab ID '${customTab.id}' specified in tab configuration`);
1304 }
1305
1306 navTab.label = customTab.label;
1307
1308 // is tab applicable to target dependency?
1309 if (-1 === _.findIndex(navTab.depTypes, matchDepType)) {
1310 return;
1311 }
1312
1313 // global config
1314 if (customTab.id === 'tree' && Configuration.mainData.disableDomTree) {
1315 return;
1316 }
1317 if (customTab.id === 'source' && Configuration.mainData.disableSourceCode) {
1318 return;
1319 }
1320 if (customTab.id === 'templateData' && Configuration.mainData.disableTemplateTab) {
1321 return;
1322 }
1323 if (customTab.id === 'styleData' && Configuration.mainData.disableStyleTab) {
1324 return;
1325 }
1326
1327 // per dependency config
1328 if (customTab.id === 'readme' && !dependency.readme) {
1329 return;
1330 }
1331 if (customTab.id === 'example' && !dependency.exampleUrls) {
1332 return;
1333 }
1334 if (
1335 customTab.id === 'templateData' &&
1336 (!dependency.templateUrl || dependency.templateUrl.length === 0)
1337 ) {
1338 return;
1339 }
1340 if (
1341 customTab.id === 'styleData' &&
1342 ((!dependency.styleUrls || dependency.styleUrls.length === 0) &&
1343 (!dependency.styles || dependency.styles.length === 0))
1344 ) {
1345 return;
1346 }
1347
1348 navTabs.push(navTab);
1349 });
1350
1351 if (navTabs.length === 0) {
1352 throw new Error(`No valid navigation tabs have been defined for dependency type '${dependency.type}'. Specify \
1353at least one config for the 'info' or 'source' tab in --navTabConfig.`);
1354 }
1355
1356 return navTabs;
1357 }
1358
1359 public prepareControllers(someControllers?) {
1360 logger.info('Prepare controllers');
1361 Configuration.mainData.controllers = someControllers
1362 ? someControllers
1363 : DependenciesEngine.getControllers();
1364
1365 return new Promise((resolve, reject) => {
1366 let i = 0;
1367 let len = Configuration.mainData.controllers.length;
1368 let loop = () => {
1369 if (i < len) {
1370 let controller = Configuration.mainData.controllers[i];
1371 let page = {
1372 path: 'controllers',
1373 name: controller.name,
1374 id: controller.id,
1375 navTabs: this.getNavTabs(controller),
1376 context: 'controller',
1377 controller: controller,
1378 depth: 1,
1379 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1380 };
1381 if (controller.isDuplicate) {
1382 page.name += '-' + controller.duplicateId;
1383 }
1384 Configuration.addPage(page);
1385 i++;
1386 loop();
1387 } else {
1388 resolve();
1389 }
1390 };
1391 loop();
1392 });
1393 }
1394
1395 public prepareComponents(someComponents?) {
1396 logger.info('Prepare components');
1397 Configuration.mainData.components = someComponents
1398 ? someComponents
1399 : DependenciesEngine.getComponents();
1400
1401 return new Promise((mainPrepareComponentResolve, mainPrepareComponentReject) => {
1402 let i = 0;
1403 let len = Configuration.mainData.components.length;
1404 let loop = () => {
1405 if (i <= len - 1) {
1406 let component = Configuration.mainData.components[i];
1407 if (MarkdownEngine.hasNeighbourReadmeFile(component.file)) {
1408 logger.info(` ${component.name} has a README file, include it`);
1409 let readmeFile = MarkdownEngine.readNeighbourReadmeFile(component.file);
1410 component.readme = marked(readmeFile);
1411 }
1412 let page = {
1413 path: 'components',
1414 name: component.name,
1415 id: component.id,
1416 navTabs: this.getNavTabs(component),
1417 context: 'component',
1418 component: component,
1419 depth: 1,
1420 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1421 };
1422
1423 if (component.isDuplicate) {
1424 page.name += '-' + component.duplicateId;
1425 }
1426 Configuration.addPage(page);
1427
1428 const componentTemplateUrlPromise = new Promise(
1429 (componentTemplateUrlResolve, componentTemplateUrlReject) => {
1430 if (component.templateUrl.length > 0) {
1431 logger.info(` ${component.name} has a templateUrl, include it`);
1432 this.handleTemplateurl(component).then(
1433 () => {
1434 componentTemplateUrlResolve();
1435 },
1436 e => {
1437 logger.error(e);
1438 componentTemplateUrlReject();
1439 }
1440 );
1441 } else {
1442 componentTemplateUrlResolve();
1443 }
1444 }
1445 );
1446 const componentStyleUrlsPromise = new Promise(
1447 (componentStyleUrlsResolve, componentStyleUrlsReject) => {
1448 if (component.styleUrls.length > 0) {
1449 logger.info(` ${component.name} has styleUrls, include them`);
1450 this.handleStyleurls(component).then(
1451 () => {
1452 componentStyleUrlsResolve();
1453 },
1454 e => {
1455 logger.error(e);
1456 componentStyleUrlsReject();
1457 }
1458 );
1459 } else {
1460 componentStyleUrlsResolve();
1461 }
1462 }
1463 );
1464 const componentStylesPromise = new Promise(
1465 (componentStylesResolve, componentStylesReject) => {
1466 if (component.styles.length > 0) {
1467 logger.info(` ${component.name} has styles, include them`);
1468 this.handleStyles(component).then(
1469 () => {
1470 componentStylesResolve();
1471 },
1472 e => {
1473 logger.error(e);
1474 componentStylesReject();
1475 }
1476 );
1477 } else {
1478 componentStylesResolve();
1479 }
1480 }
1481 );
1482
1483 Promise.all([
1484 componentTemplateUrlPromise,
1485 componentStyleUrlsPromise,
1486 componentStylesPromise
1487 ]).then(() => {
1488 i++;
1489 loop();
1490 });
1491 } else {
1492 mainPrepareComponentResolve();
1493 }
1494 };
1495 loop();
1496 });
1497 }
1498
1499 public prepareDirectives(someDirectives?) {
1500 logger.info('Prepare directives');
1501
1502 Configuration.mainData.directives = someDirectives
1503 ? someDirectives
1504 : DependenciesEngine.getDirectives();
1505
1506 return new Promise((resolve, reject) => {
1507 let i = 0;
1508 let len = Configuration.mainData.directives.length;
1509 let loop = () => {
1510 if (i < len) {
1511 let directive = Configuration.mainData.directives[i];
1512 if (MarkdownEngine.hasNeighbourReadmeFile(directive.file)) {
1513 logger.info(` ${directive.name} has a README file, include it`);
1514 let readme = MarkdownEngine.readNeighbourReadmeFile(directive.file);
1515 directive.readme = marked(readme);
1516 }
1517 let page = {
1518 path: 'directives',
1519 name: directive.name,
1520 id: directive.id,
1521 navTabs: this.getNavTabs(directive),
1522 context: 'directive',
1523 directive: directive,
1524 depth: 1,
1525 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1526 };
1527 if (directive.isDuplicate) {
1528 page.name += '-' + directive.duplicateId;
1529 }
1530 Configuration.addPage(page);
1531 i++;
1532 loop();
1533 } else {
1534 resolve();
1535 }
1536 };
1537 loop();
1538 });
1539 }
1540
1541 public prepareInjectables(someInjectables?): Promise<void> {
1542 logger.info('Prepare injectables');
1543
1544 Configuration.mainData.injectables = someInjectables
1545 ? someInjectables
1546 : DependenciesEngine.getInjectables();
1547
1548 return new Promise((resolve, reject) => {
1549 let i = 0;
1550 let len = Configuration.mainData.injectables.length;
1551 let loop = () => {
1552 if (i < len) {
1553 let injec = Configuration.mainData.injectables[i];
1554 if (MarkdownEngine.hasNeighbourReadmeFile(injec.file)) {
1555 logger.info(` ${injec.name} has a README file, include it`);
1556 let readme = MarkdownEngine.readNeighbourReadmeFile(injec.file);
1557 injec.readme = marked(readme);
1558 }
1559 let page = {
1560 path: 'injectables',
1561 name: injec.name,
1562 id: injec.id,
1563 navTabs: this.getNavTabs(injec),
1564 context: 'injectable',
1565 injectable: injec,
1566 depth: 1,
1567 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1568 };
1569 if (injec.isDuplicate) {
1570 page.name += '-' + injec.duplicateId;
1571 }
1572 Configuration.addPage(page);
1573 i++;
1574 loop();
1575 } else {
1576 resolve();
1577 }
1578 };
1579 loop();
1580 });
1581 }
1582
1583 public prepareInterceptors(someInterceptors?): Promise<void> {
1584 logger.info('Prepare interceptors');
1585
1586 Configuration.mainData.interceptors = someInterceptors
1587 ? someInterceptors
1588 : DependenciesEngine.getInterceptors();
1589
1590 return new Promise((resolve, reject) => {
1591 let i = 0;
1592 let len = Configuration.mainData.interceptors.length;
1593 let loop = () => {
1594 if (i < len) {
1595 let interceptor = Configuration.mainData.interceptors[i];
1596 if (MarkdownEngine.hasNeighbourReadmeFile(interceptor.file)) {
1597 logger.info(` ${interceptor.name} has a README file, include it`);
1598 let readme = MarkdownEngine.readNeighbourReadmeFile(interceptor.file);
1599 interceptor.readme = marked(readme);
1600 }
1601 let page = {
1602 path: 'interceptors',
1603 name: interceptor.name,
1604 id: interceptor.id,
1605 navTabs: this.getNavTabs(interceptor),
1606 context: 'interceptor',
1607 injectable: interceptor,
1608 depth: 1,
1609 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1610 };
1611 if (interceptor.isDuplicate) {
1612 page.name += '-' + interceptor.duplicateId;
1613 }
1614 Configuration.addPage(page);
1615 i++;
1616 loop();
1617 } else {
1618 resolve();
1619 }
1620 };
1621 loop();
1622 });
1623 }
1624
1625 public prepareGuards(someGuards?): Promise<void> {
1626 logger.info('Prepare guards');
1627
1628 Configuration.mainData.guards = someGuards ? someGuards : DependenciesEngine.getGuards();
1629
1630 return new Promise((resolve, reject) => {
1631 let i = 0;
1632 let len = Configuration.mainData.guards.length;
1633 let loop = () => {
1634 if (i < len) {
1635 let guard = Configuration.mainData.guards[i];
1636 if (MarkdownEngine.hasNeighbourReadmeFile(guard.file)) {
1637 logger.info(` ${guard.name} has a README file, include it`);
1638 let readme = MarkdownEngine.readNeighbourReadmeFile(guard.file);
1639 guard.readme = marked(readme);
1640 }
1641 let page = {
1642 path: 'guards',
1643 name: guard.name,
1644 id: guard.id,
1645 navTabs: this.getNavTabs(guard),
1646 context: 'guard',
1647 injectable: guard,
1648 depth: 1,
1649 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
1650 };
1651 if (guard.isDuplicate) {
1652 page.name += '-' + guard.duplicateId;
1653 }
1654 Configuration.addPage(page);
1655 i++;
1656 loop();
1657 } else {
1658 resolve();
1659 }
1660 };
1661 loop();
1662 });
1663 }
1664
1665 public prepareRoutes(): Promise<void> {
1666 logger.info('Process routes');
1667 Configuration.mainData.routes = DependenciesEngine.getRoutes();
1668
1669 return new Promise((resolve, reject) => {
1670 Configuration.addPage({
1671 name: 'routes',
1672 id: 'routes',
1673 context: 'routes',
1674 depth: 0,
1675 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
1676 });
1677
1678 if (Configuration.mainData.exportFormat === COMPODOC_DEFAULTS.exportFormat) {
1679 RouterParserUtil.generateRoutesIndex(
1680 Configuration.mainData.output,
1681 Configuration.mainData.routes
1682 ).then(
1683 () => {
1684 logger.info(' Routes index generated');
1685 resolve();
1686 },
1687 e => {
1688 logger.error(e);
1689 reject();
1690 }
1691 );
1692 } else {
1693 resolve();
1694 }
1695 });
1696 }
1697
1698 public prepareCoverage() {
1699 logger.info('Process documentation coverage report');
1700
1701 return new Promise((resolve, reject) => {
1702 /*
1703 * loop with components, directives, controllers, classes, injectables, interfaces, pipes, guards, misc functions variables
1704 */
1705 let files = [];
1706 let totalProjectStatementDocumented = 0;
1707 let getStatus = function(percent) {
1708 let status;
1709 if (percent <= 25) {
1710 status = 'low';
1711 } else if (percent > 25 && percent <= 50) {
1712 status = 'medium';
1713 } else if (percent > 50 && percent <= 75) {
1714 status = 'good';
1715 } else {
1716 status = 'very-good';
1717 }
1718 return status;
1719 };
1720 let processComponentsAndDirectivesAndControllers = list => {
1721 _.forEach(list, (el: any) => {
1722 let element = (Object as any).assign({}, el);
1723 if (!element.propertiesClass) {
1724 element.propertiesClass = [];
1725 }
1726 if (!element.methodsClass) {
1727 element.methodsClass = [];
1728 }
1729 if (!element.hostBindings) {
1730 element.hostBindings = [];
1731 }
1732 if (!element.hostListeners) {
1733 element.hostListeners = [];
1734 }
1735 if (!element.inputsClass) {
1736 element.inputsClass = [];
1737 }
1738 if (!element.outputsClass) {
1739 element.outputsClass = [];
1740 }
1741 let cl: any = {
1742 filePath: element.file,
1743 type: element.type,
1744 linktype: element.type,
1745 name: element.name
1746 };
1747 let totalStatementDocumented = 0;
1748 let totalStatements =
1749 element.propertiesClass.length +
1750 element.methodsClass.length +
1751 element.inputsClass.length +
1752 element.hostBindings.length +
1753 element.hostListeners.length +
1754 element.outputsClass.length +
1755 1; // +1 for element decorator comment
1756
1757 if (element.constructorObj) {
1758 totalStatements += 1;
1759 if (
1760 element.constructorObj &&
1761 element.constructorObj.description &&
1762 element.constructorObj.description !== ''
1763 ) {
1764 totalStatementDocumented += 1;
1765 }
1766 }
1767 if (element.description && element.description !== '') {
1768 totalStatementDocumented += 1;
1769 }
1770
1771 _.forEach(element.propertiesClass, (property: any) => {
1772 if (property.modifierKind === SyntaxKind.PrivateKeyword) {
1773 // Doesn't handle private for coverage
1774 totalStatements -= 1;
1775 }
1776 if (
1777 property.description &&
1778 property.description !== '' &&
1779 property.modifierKind !== SyntaxKind.PrivateKeyword
1780 ) {
1781 totalStatementDocumented += 1;
1782 }
1783 });
1784 _.forEach(element.methodsClass, (method: any) => {
1785 if (method.modifierKind === SyntaxKind.PrivateKeyword) {
1786 // Doesn't handle private for coverage
1787 totalStatements -= 1;
1788 }
1789 if (
1790 method.description &&
1791 method.description !== '' &&
1792 method.modifierKind !== SyntaxKind.PrivateKeyword
1793 ) {
1794 totalStatementDocumented += 1;
1795 }
1796 });
1797 _.forEach(element.hostBindings, (property: any) => {
1798 if (property.modifierKind === SyntaxKind.PrivateKeyword) {
1799 // Doesn't handle private for coverage
1800 totalStatements -= 1;
1801 }
1802 if (
1803 property.description &&
1804 property.description !== '' &&
1805 property.modifierKind !== SyntaxKind.PrivateKeyword
1806 ) {
1807 totalStatementDocumented += 1;
1808 }
1809 });
1810 _.forEach(element.hostListeners, (method: any) => {
1811 if (method.modifierKind === SyntaxKind.PrivateKeyword) {
1812 // Doesn't handle private for coverage
1813 totalStatements -= 1;
1814 }
1815 if (
1816 method.description &&
1817 method.description !== '' &&
1818 method.modifierKind !== SyntaxKind.PrivateKeyword
1819 ) {
1820 totalStatementDocumented += 1;
1821 }
1822 });
1823 _.forEach(element.inputsClass, (input: any) => {
1824 if (input.modifierKind === SyntaxKind.PrivateKeyword) {
1825 // Doesn't handle private for coverage
1826 totalStatements -= 1;
1827 }
1828 if (
1829 input.description &&
1830 input.description !== '' &&
1831 input.modifierKind !== SyntaxKind.PrivateKeyword
1832 ) {
1833 totalStatementDocumented += 1;
1834 }
1835 });
1836 _.forEach(element.outputsClass, (output: any) => {
1837 if (output.modifierKind === SyntaxKind.PrivateKeyword) {
1838 // Doesn't handle private for coverage
1839 totalStatements -= 1;
1840 }
1841 if (
1842 output.description &&
1843 output.description !== '' &&
1844 output.modifierKind !== SyntaxKind.PrivateKeyword
1845 ) {
1846 totalStatementDocumented += 1;
1847 }
1848 });
1849
1850 cl.coveragePercent = Math.floor(
1851 (totalStatementDocumented / totalStatements) * 100
1852 );
1853 if (totalStatements === 0) {
1854 cl.coveragePercent = 0;
1855 }
1856 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
1857 cl.status = getStatus(cl.coveragePercent);
1858 totalProjectStatementDocumented += cl.coveragePercent;
1859 files.push(cl);
1860 });
1861 };
1862 let processCoveragePerFile = () => {
1863 logger.info('Process documentation coverage per file');
1864 logger.info('-------------------');
1865
1866 let overFiles = files.filter(f => {
1867 let overTest =
1868 f.coveragePercent >= Configuration.mainData.coverageMinimumPerFile;
1869 if (overTest && !Configuration.mainData.coverageTestShowOnlyFailed) {
1870 logger.info(
1871 `${f.coveragePercent} % for file ${f.filePath} - ${f.name} - over minimum per file`
1872 );
1873 }
1874 return overTest;
1875 });
1876 let underFiles = files.filter(f => {
1877 let underTest =
1878 f.coveragePercent < Configuration.mainData.coverageMinimumPerFile;
1879 if (underTest) {
1880 logger.error(
1881 `${f.coveragePercent} % for file ${f.filePath} - ${f.name} - under minimum per file`
1882 );
1883 }
1884 return underTest;
1885 });
1886
1887 logger.info('-------------------');
1888 return {
1889 overFiles: overFiles,
1890 underFiles: underFiles
1891 };
1892 };
1893 let processFunctionsAndVariables = (id, type) => {
1894 _.forEach(id, (el: any) => {
1895 let cl: any = {
1896 filePath: el.file,
1897 type: type,
1898 linktype: el.type,
1899 linksubtype: el.subtype,
1900 name: el.name
1901 };
1902 if (type === 'variable') {
1903 cl.linktype = 'miscellaneous';
1904 }
1905 let totalStatementDocumented = 0;
1906 let totalStatements = 1;
1907
1908 if (el.modifierKind === SyntaxKind.PrivateKeyword) {
1909 // Doesn't handle private for coverage
1910 totalStatements -= 1;
1911 }
1912 if (
1913 el.description &&
1914 el.description !== '' &&
1915 el.modifierKind !== SyntaxKind.PrivateKeyword
1916 ) {
1917 totalStatementDocumented += 1;
1918 }
1919
1920 cl.coveragePercent = Math.floor(
1921 (totalStatementDocumented / totalStatements) * 100
1922 );
1923 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
1924 cl.status = getStatus(cl.coveragePercent);
1925 totalProjectStatementDocumented += cl.coveragePercent;
1926 files.push(cl);
1927 });
1928 };
1929
1930 let processClasses = (list, type, linktype) => {
1931 _.forEach(list, (cl: any) => {
1932 let element = (Object as any).assign({}, cl);
1933 if (!element.properties) {
1934 element.properties = [];
1935 }
1936 if (!element.methods) {
1937 element.methods = [];
1938 }
1939 let cla: any = {
1940 filePath: element.file,
1941 type: type,
1942 linktype: linktype,
1943 name: element.name
1944 };
1945 let totalStatementDocumented = 0;
1946 let totalStatements = element.properties.length + element.methods.length + 1; // +1 for element itself
1947
1948 if (element.constructorObj) {
1949 totalStatements += 1;
1950 if (
1951 element.constructorObj &&
1952 element.constructorObj.description &&
1953 element.constructorObj.description !== ''
1954 ) {
1955 totalStatementDocumented += 1;
1956 }
1957 }
1958 if (element.description && element.description !== '') {
1959 totalStatementDocumented += 1;
1960 }
1961
1962 _.forEach(element.properties, (property: any) => {
1963 if (property.modifierKind === SyntaxKind.PrivateKeyword) {
1964 // Doesn't handle private for coverage
1965 totalStatements -= 1;
1966 }
1967 if (
1968 property.description &&
1969 property.description !== '' &&
1970 property.modifierKind !== SyntaxKind.PrivateKeyword
1971 ) {
1972 totalStatementDocumented += 1;
1973 }
1974 });
1975 _.forEach(element.methods, (method: any) => {
1976 if (method.modifierKind === SyntaxKind.PrivateKeyword) {
1977 // Doesn't handle private for coverage
1978 totalStatements -= 1;
1979 }
1980 if (
1981 method.description &&
1982 method.description !== '' &&
1983 method.modifierKind !== SyntaxKind.PrivateKeyword
1984 ) {
1985 totalStatementDocumented += 1;
1986 }
1987 });
1988
1989 cla.coveragePercent = Math.floor(
1990 (totalStatementDocumented / totalStatements) * 100
1991 );
1992 if (totalStatements === 0) {
1993 cla.coveragePercent = 0;
1994 }
1995 cla.coverageCount = totalStatementDocumented + '/' + totalStatements;
1996 cla.status = getStatus(cla.coveragePercent);
1997 totalProjectStatementDocumented += cla.coveragePercent;
1998 files.push(cla);
1999 });
2000 };
2001
2002 processComponentsAndDirectivesAndControllers(Configuration.mainData.components);
2003 processComponentsAndDirectivesAndControllers(Configuration.mainData.directives);
2004 processComponentsAndDirectivesAndControllers(Configuration.mainData.controllers);
2005
2006 processClasses(Configuration.mainData.classes, 'class', 'classe');
2007 processClasses(Configuration.mainData.injectables, 'injectable', 'injectable');
2008 processClasses(Configuration.mainData.interfaces, 'interface', 'interface');
2009 processClasses(Configuration.mainData.guards, 'guard', 'guard');
2010 processClasses(Configuration.mainData.interceptors, 'interceptor', 'interceptor');
2011
2012 _.forEach(Configuration.mainData.pipes, (pipe: any) => {
2013 let cl: any = {
2014 filePath: pipe.file,
2015 type: pipe.type,
2016 linktype: pipe.type,
2017 name: pipe.name
2018 };
2019 let totalStatementDocumented = 0;
2020 let totalStatements = 1;
2021 if (pipe.description && pipe.description !== '') {
2022 totalStatementDocumented += 1;
2023 }
2024
2025 cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100);
2026 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
2027 cl.status = getStatus(cl.coveragePercent);
2028 totalProjectStatementDocumented += cl.coveragePercent;
2029 files.push(cl);
2030 });
2031
2032 processFunctionsAndVariables(
2033 Configuration.mainData.miscellaneous.functions,
2034 'function'
2035 );
2036 processFunctionsAndVariables(
2037 Configuration.mainData.miscellaneous.variables,
2038 'variable'
2039 );
2040
2041 files = _.sortBy(files, ['filePath']);
2042
2043 let coverageData = {
2044 count:
2045 files.length > 0
2046 ? Math.floor(totalProjectStatementDocumented / files.length)
2047 : 0,
2048 status: '',
2049 files
2050 };
2051 coverageData.status = getStatus(coverageData.count);
2052 Configuration.addPage({
2053 name: 'coverage',
2054 id: 'coverage',
2055 context: 'coverage',
2056 files: files,
2057 data: coverageData,
2058 depth: 0,
2059 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
2060 });
2061 coverageData.files = files;
2062 Configuration.mainData.coverageData = coverageData;
2063 if (Configuration.mainData.exportFormat === COMPODOC_DEFAULTS.exportFormat) {
2064 HtmlEngine.generateCoverageBadge(
2065 Configuration.mainData.output,
2066 'documentation',
2067 coverageData
2068 );
2069 }
2070 files = _.sortBy(files, ['coveragePercent']);
2071
2072 let coverageTestPerFileResults;
2073 if (
2074 Configuration.mainData.coverageTest &&
2075 !Configuration.mainData.coverageTestPerFile
2076 ) {
2077 // Global coverage test and not per file
2078 if (coverageData.count >= Configuration.mainData.coverageTestThreshold) {
2079 logger.info(
2080 `Documentation coverage (${coverageData.count}%) is over threshold (${Configuration.mainData.coverageTestThreshold}%)`
2081 );
2082 generationPromiseResolve();
2083 process.exit(0);
2084 } else {
2085 let message = `Documentation coverage (${coverageData.count}%) is not over threshold (${Configuration.mainData.coverageTestThreshold}%)`;
2086 generationPromiseReject();
2087 if (Configuration.mainData.coverageTestThresholdFail) {
2088 logger.error(message);
2089 process.exit(1);
2090 } else {
2091 logger.warn(message);
2092 process.exit(0);
2093 }
2094 }
2095 } else if (
2096 !Configuration.mainData.coverageTest &&
2097 Configuration.mainData.coverageTestPerFile
2098 ) {
2099 coverageTestPerFileResults = processCoveragePerFile();
2100 // Per file coverage test and not global
2101 if (coverageTestPerFileResults.underFiles.length > 0) {
2102 let message = `Documentation coverage per file is not over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`;
2103 generationPromiseReject();
2104 if (Configuration.mainData.coverageTestThresholdFail) {
2105 logger.error(message);
2106 process.exit(1);
2107 } else {
2108 logger.warn(message);
2109 process.exit(0);
2110 }
2111 } else {
2112 logger.info(
2113 `Documentation coverage per file is over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`
2114 );
2115 generationPromiseResolve();
2116 process.exit(0);
2117 }
2118 } else if (
2119 Configuration.mainData.coverageTest &&
2120 Configuration.mainData.coverageTestPerFile
2121 ) {
2122 // Per file coverage test and global
2123 coverageTestPerFileResults = processCoveragePerFile();
2124 if (
2125 coverageData.count >= Configuration.mainData.coverageTestThreshold &&
2126 coverageTestPerFileResults.underFiles.length === 0
2127 ) {
2128 logger.info(
2129 `Documentation coverage (${coverageData.count}%) is over threshold (${Configuration.mainData.coverageTestThreshold}%)`
2130 );
2131 logger.info(
2132 `Documentation coverage per file is over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`
2133 );
2134 generationPromiseResolve();
2135 process.exit(0);
2136 } else if (
2137 coverageData.count >= Configuration.mainData.coverageTestThreshold &&
2138 coverageTestPerFileResults.underFiles.length > 0
2139 ) {
2140 logger.info(
2141 `Documentation coverage (${coverageData.count}%) is over threshold (${Configuration.mainData.coverageTestThreshold}%)`
2142 );
2143 let message = `Documentation coverage per file is not over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`;
2144 generationPromiseReject();
2145 if (Configuration.mainData.coverageTestThresholdFail) {
2146 logger.error(message);
2147 process.exit(1);
2148 } else {
2149 logger.warn(message);
2150 process.exit(0);
2151 }
2152 } else if (
2153 coverageData.count < Configuration.mainData.coverageTestThreshold &&
2154 coverageTestPerFileResults.underFiles.length > 0
2155 ) {
2156 let messageGlobal = `Documentation coverage (${coverageData.count}%) is not over threshold (${Configuration.mainData.coverageTestThreshold}%)`,
2157 messagePerFile = `Documentation coverage per file is not over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`;
2158 generationPromiseReject();
2159 if (Configuration.mainData.coverageTestThresholdFail) {
2160 logger.error(messageGlobal);
2161 logger.error(messagePerFile);
2162 process.exit(1);
2163 } else {
2164 logger.warn(messageGlobal);
2165 logger.warn(messagePerFile);
2166 process.exit(0);
2167 }
2168 } else {
2169 let message = `Documentation coverage (${coverageData.count}%) is not over threshold (${Configuration.mainData.coverageTestThreshold}%)`,
2170 messagePerFile = `Documentation coverage per file is over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`;
2171 generationPromiseReject();
2172 if (Configuration.mainData.coverageTestThresholdFail) {
2173 logger.error(message);
2174 logger.info(messagePerFile);
2175 process.exit(1);
2176 } else {
2177 logger.warn(message);
2178 logger.info(messagePerFile);
2179 process.exit(0);
2180 }
2181 }
2182 } else {
2183 resolve();
2184 }
2185 });
2186 }
2187
2188 public prepareUnitTestCoverage() {
2189 logger.info('Process unit test coverage report');
2190 return new Promise((resolve, reject) => {
2191 let covDat, covFileNames;
2192
2193 let coverageData: CoverageData = Configuration.mainData.coverageData;
2194
2195 if (!coverageData.files) {
2196 logger.warn('Missing documentation coverage data');
2197 } else {
2198 covDat = {};
2199 covFileNames = _.map(coverageData.files, el => {
2200 let fileName = el.filePath;
2201 covDat[fileName] = {
2202 type: el.type,
2203 linktype: el.linktype,
2204 linksubtype: el.linksubtype,
2205 name: el.name
2206 };
2207 return fileName;
2208 });
2209 }
2210 // read coverage summary file and data
2211 let unitTestSummary = {};
2212 let fileDat = FileEngine.getSync(Configuration.mainData.unitTestCoverage);
2213 if (fileDat) {
2214 unitTestSummary = JSON.parse(fileDat);
2215 } else {
2216 return Promise.reject('Error reading unit test coverage file');
2217 }
2218 let getCovStatus = function(percent, totalLines) {
2219 let status;
2220 if (totalLines === 0) {
2221 status = 'uncovered';
2222 } else if (percent <= 25) {
2223 status = 'low';
2224 } else if (percent > 25 && percent <= 50) {
2225 status = 'medium';
2226 } else if (percent > 50 && percent <= 75) {
2227 status = 'good';
2228 } else {
2229 status = 'very-good';
2230 }
2231 return status;
2232 };
2233 let getCoverageData = function(data, fileName) {
2234 let out = {};
2235 if (fileName !== 'total') {
2236 if (covDat === undefined) {
2237 // need a name to include in output but this isn't visible
2238 out = { name: fileName, filePath: fileName };
2239 } else {
2240 let findMatch = _.filter(covFileNames, el => {
2241 return el.includes(fileName) || fileName.includes(el);
2242 });
2243 if (findMatch.length > 0) {
2244 out = _.clone(covDat[findMatch[0]]);
2245 out['filePath'] = fileName;
2246 }
2247 }
2248 }
2249 let keysToGet = ['statements', 'branches', 'functions', 'lines'];
2250 _.forEach(keysToGet, key => {
2251 if (data[key]) {
2252 let t = data[key];
2253 out[key] = {
2254 coveragePercent: Math.round(t.pct),
2255 coverageCount: '' + t.covered + '/' + t.total,
2256 status: getCovStatus(t.pct, t.total)
2257 };
2258 }
2259 });
2260 return out;
2261 };
2262
2263 let unitTestData = {};
2264 let files = [];
2265 for (let file in unitTestSummary) {
2266 let dat = getCoverageData(unitTestSummary[file], file);
2267 if (file === 'total') {
2268 unitTestData['total'] = dat;
2269 } else {
2270 files.push(dat);
2271 }
2272 }
2273 unitTestData['files'] = files;
2274 unitTestData['idColumn'] = covDat !== undefined; // should we include the id column
2275 Configuration.mainData.unitTestData = unitTestData;
2276 Configuration.addPage({
2277 name: 'unit-test',
2278 id: 'unit-test',
2279 context: 'unit-test',
2280 files: files,
2281 data: unitTestData,
2282 depth: 0,
2283 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
2284 });
2285
2286 if (Configuration.mainData.exportFormat === COMPODOC_DEFAULTS.exportFormat) {
2287 let keysToGet = ['statements', 'branches', 'functions', 'lines'];
2288 _.forEach(keysToGet, key => {
2289 if (unitTestData['total'][key]) {
2290 HtmlEngine.generateCoverageBadge(Configuration.mainData.output, key, {
2291 count: unitTestData['total'][key]['coveragePercent'],
2292 status: unitTestData['total'][key]['status']
2293 });
2294 }
2295 });
2296 }
2297 resolve();
2298 });
2299 }
2300
2301 private processPage(page): Promise<void> {
2302 logger.info('Process page', page.name);
2303
2304 let htmlData = HtmlEngine.render(Configuration.mainData, page);
2305 let finalPath = Configuration.mainData.output;
2306
2307 if (Configuration.mainData.output.lastIndexOf('/') === -1) {
2308 finalPath += '/';
2309 }
2310 if (page.path) {
2311 finalPath += page.path + '/';
2312 }
2313
2314 if (page.filename) {
2315 finalPath += page.filename + '.html';
2316 } else {
2317 finalPath += page.name + '.html';
2318 }
2319
2320 if (!Configuration.mainData.disableSearch) {
2321 SearchEngine.indexPage({
2322 infos: page,
2323 rawData: htmlData,
2324 url: finalPath
2325 });
2326 }
2327
2328 FileEngine.writeSync(finalPath, htmlData);
2329 return Promise.resolve();
2330 }
2331
2332 public processPages() {
2333 let pages = _.sortBy(Configuration.pages, ['name']);
2334
2335 logger.info('Process pages');
2336 Promise.all(pages.map(page => this.processPage(page)))
2337 .then(() => {
2338 let callbacksAfterGenerateSearchIndexJson = () => {
2339 if (Configuration.mainData.additionalPages.length > 0) {
2340 this.processAdditionalPages();
2341 } else {
2342 if (Configuration.mainData.assetsFolder !== '') {
2343 this.processAssetsFolder();
2344 }
2345 this.processResources();
2346 }
2347 };
2348 if (!Configuration.mainData.disableSearch) {
2349 SearchEngine.generateSearchIndexJson(Configuration.mainData.output).then(
2350 () => {
2351 callbacksAfterGenerateSearchIndexJson();
2352 },
2353 e => {
2354 logger.error(e);
2355 }
2356 );
2357 } else {
2358 callbacksAfterGenerateSearchIndexJson();
2359 }
2360 })
2361 .then(() => {
2362 return this.processMenu(Configuration.mainData);
2363 })
2364 .catch(e => {
2365 logger.error(e);
2366 });
2367 }
2368
2369 private processMenu(mainData): Promise<void> {
2370 logger.info('Process menu...');
2371
2372 return HtmlEngine.renderMenu(Configuration.mainData.templates, mainData).then(htmlData => {
2373 let finalPath = `${mainData.output}/js/menu-wc.js`;
2374 return FileEngine.write(finalPath, htmlData).catch(err => {
2375 logger.error('Error during ' + finalPath + ' page generation');
2376 return Promise.reject('');
2377 });
2378 });
2379 }
2380
2381 public processAdditionalPages() {
2382 logger.info('Process additional pages');
2383 let pages = Configuration.mainData.additionalPages;
2384 Promise.all(
2385 pages.map(page => {
2386 if (page.children.length > 0) {
2387 return Promise.all([
2388 this.processPage(page),
2389 ...page.children.map(childPage => this.processPage(childPage))
2390 ]);
2391 } else {
2392 return this.processPage(page);
2393 }
2394 })
2395 )
2396 .then(() => {
2397 SearchEngine.generateSearchIndexJson(Configuration.mainData.output).then(() => {
2398 if (Configuration.mainData.assetsFolder !== '') {
2399 this.processAssetsFolder();
2400 }
2401 this.processResources();
2402 });
2403 })
2404 .catch(e => {
2405 logger.error(e);
2406 return Promise.reject(e);
2407 });
2408 }
2409
2410 public processAssetsFolder(): void {
2411 logger.info('Copy assets folder');
2412
2413 if (!FileEngine.existsSync(Configuration.mainData.assetsFolder)) {
2414 logger.error(
2415 `Provided assets folder ${Configuration.mainData.assetsFolder} did not exist`
2416 );
2417 } else {
2418 let finalOutput = Configuration.mainData.output;
2419
2420 let testOutputDir = Configuration.mainData.output.match(cwd);
2421
2422 if (testOutputDir && testOutputDir.length > 0) {
2423 finalOutput = Configuration.mainData.output.replace(cwd + path.sep, '');
2424 }
2425
2426 const destination = path.join(
2427 finalOutput,
2428 path.basename(Configuration.mainData.assetsFolder)
2429 );
2430 fs.copy(
2431 path.resolve(Configuration.mainData.assetsFolder),
2432 path.resolve(destination),
2433 err => {
2434 if (err) {
2435 logger.error('Error during resources copy ', err);
2436 }
2437 }
2438 );
2439 }
2440 }
2441
2442 public processResources() {
2443 logger.info('Copy main resources');
2444
2445 const onComplete = () => {
2446 logger.info(
2447 'Documentation generated in ' +
2448 Configuration.mainData.output +
2449 ' in ' +
2450 this.getElapsedTime() +
2451 ' seconds using ' +
2452 Configuration.mainData.theme +
2453 ' theme'
2454 );
2455 if (Configuration.mainData.serve) {
2456 logger.info(
2457 `Serving documentation from ${Configuration.mainData.output} at http://${Configuration.mainData.hostname}:${Configuration.mainData.port}`
2458 );
2459 this.runWebServer(Configuration.mainData.output);
2460 } else {
2461 generationPromiseResolve();
2462 this.endCallback();
2463 }
2464 };
2465
2466 let finalOutput = Configuration.mainData.output;
2467
2468 let testOutputDir = Configuration.mainData.output.match(cwd);
2469
2470 if (testOutputDir && testOutputDir.length > 0) {
2471 finalOutput = Configuration.mainData.output.replace(cwd + path.sep, '');
2472 }
2473
2474 fs.copy(
2475 path.resolve(__dirname + '/../src/resources/'),
2476 path.resolve(finalOutput),
2477 errorCopy => {
2478 if (errorCopy) {
2479 logger.error('Error during resources copy ', errorCopy);
2480 } else {
2481 const extThemePromise = new Promise((extThemeResolve, extThemeReject) => {
2482 if (Configuration.mainData.extTheme) {
2483 fs.copy(
2484 path.resolve(cwd + path.sep + Configuration.mainData.extTheme),
2485 path.resolve(finalOutput + '/styles/'),
2486 function(errorCopyTheme) {
2487 if (errorCopyTheme) {
2488 logger.error(
2489 'Error during external styling theme copy ',
2490 errorCopyTheme
2491 );
2492 extThemeReject();
2493 } else {
2494 logger.info('External styling theme copy succeeded');
2495 extThemeResolve();
2496 }
2497 }
2498 );
2499 } else {
2500 extThemeResolve();
2501 }
2502 });
2503
2504 const customFaviconPromise = new Promise(
2505 (customFaviconResolve, customFaviconReject) => {
2506 if (Configuration.mainData.customFavicon !== '') {
2507 logger.info(`Custom favicon supplied`);
2508 fs.copy(
2509 path.resolve(
2510 cwd + path.sep + Configuration.mainData.customFavicon
2511 ),
2512 path.resolve(finalOutput + '/images/favicon.ico'),
2513 errorCopyFavicon => {
2514 // tslint:disable-line
2515 if (errorCopyFavicon) {
2516 logger.error(
2517 'Error during resources copy of favicon',
2518 errorCopyFavicon
2519 );
2520 customFaviconReject();
2521 } else {
2522 logger.info('External custom favicon copy succeeded');
2523 customFaviconResolve();
2524 }
2525 }
2526 );
2527 } else {
2528 customFaviconResolve();
2529 }
2530 }
2531 );
2532
2533 const customLogoPromise = new Promise((customLogoResolve, customLogoReject) => {
2534 if (Configuration.mainData.customLogo !== '') {
2535 logger.info(`Custom logo supplied`);
2536 fs.copy(
2537 path.resolve(cwd + path.sep + Configuration.mainData.customLogo),
2538 path.resolve(
2539 finalOutput +
2540 '/images/' +
2541 Configuration.mainData.customLogo.split('/').pop()
2542 ),
2543 errorCopyLogo => {
2544 // tslint:disable-line
2545 if (errorCopyLogo) {
2546 logger.error(
2547 'Error during resources copy of logo',
2548 errorCopyLogo
2549 );
2550 customLogoReject();
2551 } else {
2552 logger.info('External custom logo copy succeeded');
2553 customLogoResolve();
2554 }
2555 }
2556 );
2557 } else {
2558 customLogoResolve();
2559 }
2560 });
2561
2562 Promise.all([extThemePromise, customFaviconPromise, customLogoPromise]).then(
2563 () => {
2564 onComplete();
2565 }
2566 );
2567 }
2568 }
2569 );
2570 }
2571
2572 /**
2573 * Calculates the elapsed time since the program was started.
2574 *
2575 * @returns {number}
2576 */
2577 private getElapsedTime() {
2578 return (new Date().valueOf() - startTime.valueOf()) / 1000;
2579 }
2580
2581 public processGraphs() {
2582 if (Configuration.mainData.disableGraph) {
2583 logger.info('Graph generation disabled');
2584 this.processPages();
2585 } else {
2586 logger.info('Process main graph');
2587 let modules = Configuration.mainData.modules;
2588 let i = 0;
2589 let len = modules.length;
2590 let loop = () => {
2591 if (i <= len - 1) {
2592 logger.info('Process module graph ', modules[i].name);
2593 let finalPath = Configuration.mainData.output;
2594 if (Configuration.mainData.output.lastIndexOf('/') === -1) {
2595 finalPath += '/';
2596 }
2597 finalPath += 'modules/' + modules[i].name;
2598 let _rawModule = DependenciesEngine.getRawModule(modules[i].name);
2599 if (
2600 _rawModule.declarations.length > 0 ||
2601 _rawModule.bootstrap.length > 0 ||
2602 _rawModule.imports.length > 0 ||
2603 _rawModule.exports.length > 0 ||
2604 _rawModule.providers.length > 0
2605 ) {
2606 NgdEngine.renderGraph(
2607 modules[i].file,
2608 finalPath,
2609 'f',
2610 modules[i].name
2611 ).then(
2612 () => {
2613 NgdEngine.readGraph(
2614 path.resolve(finalPath + path.sep + 'dependencies.svg'),
2615 modules[i].name
2616 ).then(
2617 data => {
2618 modules[i].graph = data;
2619 i++;
2620 loop();
2621 },
2622 err => {
2623 logger.error('Error during graph read: ', err);
2624 }
2625 );
2626 },
2627 errorMessage => {
2628 logger.error(errorMessage);
2629 }
2630 );
2631 } else {
2632 i++;
2633 loop();
2634 }
2635 } else {
2636 this.processPages();
2637 }
2638 };
2639 let finalMainGraphPath = Configuration.mainData.output;
2640 if (finalMainGraphPath.lastIndexOf('/') === -1) {
2641 finalMainGraphPath += '/';
2642 }
2643 finalMainGraphPath += 'graph';
2644 NgdEngine.init(path.resolve(finalMainGraphPath));
2645
2646 NgdEngine.renderGraph(
2647 Configuration.mainData.tsconfig,
2648 path.resolve(finalMainGraphPath),
2649 'p'
2650 ).then(
2651 () => {
2652 NgdEngine.readGraph(
2653 path.resolve(finalMainGraphPath + path.sep + 'dependencies.svg'),
2654 'Main graph'
2655 ).then(
2656 data => {
2657 Configuration.mainData.mainGraph = data;
2658 loop();
2659 },
2660 err => {
2661 logger.error('Error during main graph reading : ', err);
2662 Configuration.mainData.disableMainGraph = true;
2663 loop();
2664 }
2665 );
2666 },
2667 err => {
2668 logger.error(
2669 'Ooops error during main graph generation, moving on next part with main graph disabled : ',
2670 err
2671 );
2672 Configuration.mainData.disableMainGraph = true;
2673 loop();
2674 }
2675 );
2676 }
2677 }
2678
2679 public runWebServer(folder) {
2680 if (!this.isWatching) {
2681 let liveServerConfiguration: LiveServerConfiguration = {
2682 root: folder,
2683 open: Configuration.mainData.open,
2684 quiet: true,
2685 logLevel: 0,
2686 wait: 1000,
2687 port: Configuration.mainData.port
2688 };
2689 if (Configuration.mainData.host !== '') {
2690 liveServerConfiguration.host = Configuration.mainData.host;
2691 }
2692 LiveServer.start(liveServerConfiguration);
2693 }
2694 if (Configuration.mainData.watch && !this.isWatching) {
2695 if (typeof this.files === 'undefined') {
2696 logger.error('No sources files available, please use -p flag');
2697 generationPromiseReject();
2698 process.exit(1);
2699 } else {
2700 this.runWatch();
2701 }
2702 } else if (Configuration.mainData.watch && this.isWatching) {
2703 let srcFolder = findMainSourceFolder(this.files);
2704 logger.info(`Already watching sources in ${srcFolder} folder`);
2705 }
2706 }
2707
2708 public runWatch() {
2709 let sources = [findMainSourceFolder(this.files)];
2710 let watcherReady = false;
2711
2712 this.isWatching = true;
2713
2714 logger.info(`Watching sources in ${findMainSourceFolder(this.files)} folder`);
2715
2716 if (MarkdownEngine.hasRootMarkdowns()) {
2717 sources = sources.concat(MarkdownEngine.listRootMarkdowns());
2718 }
2719
2720 if (Configuration.mainData.includes !== '') {
2721 sources = sources.concat(Configuration.mainData.includes);
2722 }
2723
2724 // Check all elements of sources list exist
2725 sources = cleanSourcesForWatch(sources);
2726
2727 let watcher = chokidar.watch(sources, {
2728 awaitWriteFinish: true,
2729 ignoreInitial: true,
2730 ignored: /(spec|\.d)\.ts/
2731 });
2732 let timerAddAndRemoveRef;
2733 let timerChangeRef;
2734 let runnerAddAndRemove = () => {
2735 startTime = new Date();
2736 this.generate();
2737 };
2738 let waiterAddAndRemove = () => {
2739 clearTimeout(timerAddAndRemoveRef);
2740 timerAddAndRemoveRef = setTimeout(runnerAddAndRemove, 1000);
2741 };
2742 let runnerChange = () => {
2743 startTime = new Date();
2744 this.setUpdatedFiles(this.watchChangedFiles);
2745 if (this.hasWatchedFilesTSFiles()) {
2746 this.getMicroDependenciesData();
2747 } else if (this.hasWatchedFilesRootMarkdownFiles()) {
2748 this.rebuildRootMarkdowns();
2749 } else {
2750 this.rebuildExternalDocumentation();
2751 }
2752 };
2753 let waiterChange = () => {
2754 clearTimeout(timerChangeRef);
2755 timerChangeRef = setTimeout(runnerChange, 1000);
2756 };
2757
2758 watcher.on('ready', () => {
2759 if (!watcherReady) {
2760 watcherReady = true;
2761 watcher
2762 .on('add', file => {
2763 logger.debug(`File ${file} has been added`);
2764 // Test extension, if ts
2765 // rescan everything
2766 if (path.extname(file) === '.ts') {
2767 waiterAddAndRemove();
2768 }
2769 })
2770 .on('change', file => {
2771 logger.debug(`File ${file} has been changed`);
2772 // Test extension, if ts
2773 // rescan only file
2774 if (
2775 path.extname(file) === '.ts' ||
2776 path.extname(file) === '.md' ||
2777 path.extname(file) === '.json'
2778 ) {
2779 this.watchChangedFiles.push(path.join(cwd + path.sep + file));
2780 waiterChange();
2781 }
2782 })
2783 .on('unlink', file => {
2784 logger.debug(`File ${file} has been removed`);
2785 // Test extension, if ts
2786 // rescan everything
2787 if (path.extname(file) === '.ts') {
2788 waiterAddAndRemove();
2789 }
2790 });
2791 }
2792 });
2793 }
2794
2795 /**
2796 * Return the application / root component instance.
2797 */
2798 get application(): Application {
2799 return this;
2800 }
2801
2802 get isCLI(): boolean {
2803 return false;
2804 }
2805}