UNPKG

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