UNPKG

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