UNPKG

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