UNPKG

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