UNPKG

28.3 kBPlain TextView Raw
1import * as ts from 'typescript';
2import * as fs from 'fs-extra';
3import * as path from 'path';
4import * as _ from 'lodash';
5import * as LiveServer from 'live-server';
6import * as Shelljs from 'shelljs';
7import marked from 'marked';
8
9const glob: any = require('glob');
10
11import { logger } from '../logger';
12import { HtmlEngine } from './engines/html.engine';
13import { MarkdownEngine } from './engines/markdown.engine';
14import { FileEngine } from './engines/file.engine';
15import { Configuration, IConfiguration } from './configuration';
16import { $dependenciesEngine } from './engines/dependencies.engine';
17import { NgdEngine } from './engines/ngd.engine';
18import { SearchEngine } from './engines/search.engine';
19import { Dependencies } from './compiler/dependencies';
20import { RouterParser } from '../utils/router.parser';
21
22import { COMPODOC_DEFAULTS } from '../utils/defaults';
23
24let pkg = require('../package.json'),
25 cwd = process.cwd(),
26 $htmlengine = new HtmlEngine(),
27 $fileengine = new FileEngine(),
28 $markdownengine = new MarkdownEngine(),
29 $ngdengine = new NgdEngine(),
30 $searchEngine = new SearchEngine(),
31 startTime = new Date();
32
33export class Application {
34 options:Object;
35 files: Array<string>;
36
37 configuration:IConfiguration;
38
39 /**
40 * Create a new compodoc application instance.
41 *
42 * @param options An object containing the options that should be used.
43 */
44 constructor(options?:Object) {
45 this.configuration = Configuration.getInstance();
46
47 for (let option in options ) {
48 if(typeof this.configuration.mainData[option] !== 'undefined') {
49 this.configuration.mainData[option] = options[option];
50 }
51 }
52 }
53
54 /**
55 * Start compodoc
56 */
57 protected generate() {
58 $htmlengine.init().then(() => {
59 this.processPackageJson();
60 });
61 }
62
63 setFiles(files:Array<string>) {
64 this.files = files;
65 }
66
67 processPackageJson() {
68 logger.info('Searching package.json file');
69 $fileengine.get('package.json').then((packageData) => {
70 let parsedData = JSON.parse(packageData);
71 if (typeof parsedData.name !== 'undefined' && this.configuration.mainData.documentationMainName === COMPODOC_DEFAULTS.title) {
72 this.configuration.mainData.documentationMainName = parsedData.name + ' documentation';
73 }
74 if (typeof parsedData.description !== 'undefined') {
75 this.configuration.mainData.documentationMainDescription = parsedData.description;
76 }
77 logger.info('package.json file found');
78 this.processMarkdown();
79 }, (errorMessage) => {
80 logger.error(errorMessage);
81 logger.error('Continuing without package.json file');
82 this.processMarkdown();
83 });
84 }
85
86 processMarkdown() {
87 logger.info('Searching README.md file');
88 $markdownengine.getReadmeFile().then((readmeData: string) => {
89 this.configuration.addPage({
90 name: 'index',
91 context: 'readme',
92 depth: 1,
93 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
94 });
95 this.configuration.addPage({
96 name: 'overview',
97 context: 'overview',
98 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
99 });
100 this.configuration.mainData.readme = readmeData;
101 logger.info('README.md file found');
102 this.getDependenciesData();
103 }, (errorMessage) => {
104 logger.error(errorMessage);
105 logger.error('Continuing without README.md file');
106 this.configuration.addPage({
107 name: 'index',
108 context: 'overview'
109 });
110 this.getDependenciesData();
111 });
112 }
113
114 getDependenciesData() {
115 logger.info('Get dependencies data');
116
117 let crawler = new Dependencies(
118 this.files, {
119 tsconfigDirectory: path.dirname(this.configuration.mainData.tsconfig)
120 }
121 );
122
123 let dependenciesData = crawler.getDependencies();
124
125 $dependenciesEngine.init(dependenciesData);
126
127 this.prepareModules();
128
129 this.prepareComponents().then((readmeData) => {
130 if ($dependenciesEngine.directives.length > 0) {
131 this.prepareDirectives();
132 }
133 if ($dependenciesEngine.injectables.length > 0) {
134 this.prepareInjectables();
135 }
136 if ($dependenciesEngine.routes.length > 0) {
137 this.prepareRoutes();
138 }
139
140 if ($dependenciesEngine.pipes.length > 0) {
141 this.preparePipes();
142 }
143
144 if ($dependenciesEngine.classes.length > 0) {
145 this.prepareClasses();
146 }
147
148 if ($dependenciesEngine.interfaces.length > 0) {
149 this.prepareInterfaces();
150 }
151
152 if ($dependenciesEngine.miscellaneous.variables.length > 0 ||
153 $dependenciesEngine.miscellaneous.functions.length > 0 ||
154 $dependenciesEngine.miscellaneous.typealiases.length > 0 ||
155 $dependenciesEngine.miscellaneous.enumerations.length > 0 ) {
156 this.prepareMiscellaneous();
157 }
158
159 if (!this.configuration.mainData.disableCoverage) {
160 this.prepareCoverage();
161 }
162
163 this.processPages();
164 }, (errorMessage) => {
165 logger.error(errorMessage);
166 });
167 }
168
169 prepareModules() {
170 logger.info('Prepare modules');
171 this.configuration.mainData.modules = $dependenciesEngine.getModules().map(ngModule => {
172 ['declarations', 'bootstrap', 'imports', 'exports'].forEach(metadataType => {
173 ngModule[metadataType] = ngModule[metadataType].filter(metaDataItem => {
174 switch (metaDataItem.type) {
175 case 'directive':
176 return $dependenciesEngine.getDirectives().some(directive => directive.name === metaDataItem.name);
177
178 case 'component':
179 return $dependenciesEngine.getComponents().some(component => component.name === metaDataItem.name);
180
181 case 'module':
182 return $dependenciesEngine.getModules().some(module => module.name === metaDataItem.name);
183
184 case 'pipe':
185 return $dependenciesEngine.getPipes().some(pipe => pipe.name === metaDataItem.name);
186
187 default:
188 return true;
189 }
190 });
191 });
192 ngModule.providers = ngModule.providers.filter(provider => {
193 return $dependenciesEngine.getInjectables().some(injectable => injectable.name === provider.name);
194 });
195 return ngModule;
196 });
197 this.configuration.addPage({
198 name: 'modules',
199 context: 'modules',
200 depth: 1,
201 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
202 });
203 let i = 0,
204 len = this.configuration.mainData.modules.length;
205
206 for(i; i<len; i++) {
207 this.configuration.addPage({
208 path: 'modules',
209 name: this.configuration.mainData.modules[i].name,
210 context: 'module',
211 module: this.configuration.mainData.modules[i],
212 depth: 2,
213 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
214 });
215 }
216 }
217
218 preparePipes = () => {
219 logger.info('Prepare pipes');
220 this.configuration.mainData.pipes = $dependenciesEngine.getPipes();
221 let i = 0,
222 len = this.configuration.mainData.pipes.length;
223
224 for(i; i<len; i++) {
225 this.configuration.addPage({
226 path: 'pipes',
227 name: this.configuration.mainData.pipes[i].name,
228 context: 'pipe',
229 pipe: this.configuration.mainData.pipes[i],
230 depth: 2,
231 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
232 });
233 }
234 }
235
236 prepareClasses = () => {
237 logger.info('Prepare classes');
238 this.configuration.mainData.classes = $dependenciesEngine.getClasses();
239 let i = 0,
240 len = this.configuration.mainData.classes.length;
241
242 for(i; i<len; i++) {
243 this.configuration.addPage({
244 path: 'classes',
245 name: this.configuration.mainData.classes[i].name,
246 context: 'class',
247 class: this.configuration.mainData.classes[i],
248 depth: 2,
249 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
250 });
251 }
252 }
253
254 prepareInterfaces() {
255 logger.info('Prepare interfaces');
256 this.configuration.mainData.interfaces = $dependenciesEngine.getInterfaces();
257 let i = 0,
258 len = this.configuration.mainData.interfaces.length;
259 for(i; i<len; i++) {
260 this.configuration.addPage({
261 path: 'interfaces',
262 name: this.configuration.mainData.interfaces[i].name,
263 context: 'interface',
264 interface: this.configuration.mainData.interfaces[i],
265 depth: 2,
266 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
267 });
268 }
269 }
270
271 prepareMiscellaneous() {
272 logger.info('Prepare miscellaneous');
273 this.configuration.mainData.miscellaneous = $dependenciesEngine.getMiscellaneous();
274
275 this.configuration.addPage({
276 name: 'miscellaneous',
277 context: 'miscellaneous',
278 depth: 1,
279 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
280 });
281 }
282
283 prepareComponents() {
284 logger.info('Prepare components');
285 let that = this;
286 that.configuration.mainData.components = $dependenciesEngine.getComponents();
287
288 return new Promise(function(resolve, reject) {
289 let i = 0,
290 len = that.configuration.mainData.components.length,
291 loop = () => {
292 if( i <= len-1) {
293 let dirname = path.dirname(that.configuration.mainData.components[i].file),
294 readmeFile = dirname + path.sep + 'README.md';
295 if (fs.existsSync(readmeFile)) {
296 logger.info('README.md exist for this component, include it');
297 fs.readFile(readmeFile, 'utf8', (err, data) => {
298 if (err) throw err;
299 that.configuration.mainData.components[i].readme = marked(data);
300 that.configuration.addPage({
301 path: 'components',
302 name: that.configuration.mainData.components[i].name,
303 context: 'component',
304 component: that.configuration.mainData.components[i],
305 depth: 2,
306 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
307 });
308 i++;
309 loop();
310 });
311 } else {
312 that.configuration.addPage({
313 path: 'components',
314 name: that.configuration.mainData.components[i].name,
315 context: 'component',
316 component: that.configuration.mainData.components[i]
317 });
318 i++;
319 loop();
320 }
321 } else {
322 resolve();
323 }
324 };
325 loop();
326 });
327 }
328
329 prepareDirectives = () => {
330 logger.info('Prepare directives');
331 this.configuration.mainData.directives = $dependenciesEngine.getDirectives();
332
333 let i = 0,
334 len = this.configuration.mainData.directives.length;
335
336 for(i; i<len; i++) {
337 this.configuration.addPage({
338 path: 'directives',
339 name: this.configuration.mainData.directives[i].name,
340 context: 'directive',
341 directive: this.configuration.mainData.directives[i],
342 depth: 2,
343 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
344 });
345 }
346 }
347
348 prepareInjectables() {
349 logger.info('Prepare injectables');
350 this.configuration.mainData.injectables = $dependenciesEngine.getInjectables();
351
352 let i = 0,
353 len = this.configuration.mainData.injectables.length;
354
355 for(i; i<len; i++) {
356 this.configuration.addPage({
357 path: 'injectables',
358 name: this.configuration.mainData.injectables[i].name,
359 context: 'injectable',
360 injectable: this.configuration.mainData.injectables[i],
361 depth: 2,
362 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.INTERNAL
363 });
364 }
365 }
366
367 prepareRoutes() {
368 logger.info('Process routes');
369 this.configuration.mainData.routes = $dependenciesEngine.getRoutes();
370
371 this.configuration.addPage({
372 name: 'routes',
373 context: 'routes',
374 depth: 1,
375 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
376 });
377 }
378
379 prepareCoverage() {
380 logger.info('Process documentation coverage report');
381
382 /*
383 * loop with components, classes, injectables, interfaces, pipes
384 */
385 var files = [],
386 totalProjectStatementDocumented = 0,
387 getStatus = function(percent) {
388 var status;
389 if (percent <= 25) {
390 status = 'low';
391 } else if (percent > 25 && percent <= 50) {
392 status = 'medium';
393 } else if (percent > 50 && percent <= 75) {
394 status = 'good';
395 } else {
396 status = 'very-good';
397 }
398 return status;
399 };
400
401 _.forEach(this.configuration.mainData.components, (component) => {
402 if (!component.propertiesClass ||
403 !component.methodsClass ||
404 !component.inputsClass ||
405 !component.outputsClass) {
406 return;
407 }
408 let cl = {
409 filePath: component.file,
410 type: component.type,
411 name: component.name
412 },
413 totalStatementDocumented = 0,
414 totalStatements = component.propertiesClass.length + component.methodsClass.length + component.inputsClass.length + component.outputsClass.length + 1; // +1 for component decorator comment
415 _.forEach(component.propertiesClass, (property) => {
416 if(property.description !== '') {
417 totalStatementDocumented += 1;
418 }
419 });
420 _.forEach(component.methodsClass, (method) => {
421 if(method.description !== '') {
422 totalStatementDocumented += 1;
423 }
424 });
425 _.forEach(component.inputsClass, (input) => {
426 if(input.description !== '') {
427 totalStatementDocumented += 1;
428 }
429 });
430 _.forEach(component.outputsClass, (output) => {
431 if(output.description !== '') {
432 totalStatementDocumented += 1;
433 }
434 });
435 if (component.description !== '') {
436 totalStatementDocumented += 1;
437 }
438 cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100);
439 if(totalStatements === 0) {
440 cl.coveragePercent = 0;
441 }
442 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
443 cl.status = getStatus(cl.coveragePercent);
444 totalProjectStatementDocumented += cl.coveragePercent;
445 files.push(cl);
446 })
447 _.forEach(this.configuration.mainData.classes, (classe) => {
448 if (!classe.properties ||
449 !classe.methods) {
450 return;
451 }
452 let cl = {
453 filePath: classe.file,
454 type: 'classe',
455 name: classe.name
456 },
457 totalStatementDocumented = 0,
458 totalStatements = classe.properties.length + classe.methods.length;
459 _.forEach(classe.properties, (property) => {
460 if(property.description !== '') {
461 totalStatementDocumented += 1;
462 }
463 });
464 _.forEach(classe.methods, (method) => {
465 if(method.description !== '') {
466 totalStatementDocumented += 1;
467 }
468 });
469 cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100);
470 if(totalStatements === 0) {
471 cl.coveragePercent = 0;
472 }
473 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
474 cl.status = getStatus(cl.coveragePercent);
475 totalProjectStatementDocumented += cl.coveragePercent;
476 files.push(cl);
477 });
478 _.forEach(this.configuration.mainData.injectables, (injectable) => {
479 if (!injectable.properties ||
480 !injectable.methods) {
481 return;
482 }
483 let cl = {
484 filePath: injectable.file,
485 type: injectable.type,
486 name: injectable.name
487 },
488 totalStatementDocumented = 0,
489 totalStatements = injectable.properties.length + injectable.methods.length;
490 _.forEach(injectable.properties, (property) => {
491 if(property.description !== '') {
492 totalStatementDocumented += 1;
493 }
494 });
495 _.forEach(injectable.methods, (method) => {
496 if(method.description !== '') {
497 totalStatementDocumented += 1;
498 }
499 });
500 cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100);
501 if(totalStatements === 0) {
502 cl.coveragePercent = 0;
503 }
504 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
505 cl.status = getStatus(cl.coveragePercent);
506 totalProjectStatementDocumented += cl.coveragePercent;
507 files.push(cl);
508 });
509 _.forEach(this.configuration.mainData.interfaces, (inter) => {
510 if (!inter.properties ||
511 !inter.methods) {
512 return;
513 }
514 let cl = {
515 filePath: inter.file,
516 type: inter.type,
517 name: inter.name
518 },
519 totalStatementDocumented = 0,
520 totalStatements = inter.properties.length + inter.methods.length;
521 _.forEach(inter.properties, (property) => {
522 if(property.description !== '') {
523 totalStatementDocumented += 1;
524 }
525 });
526 _.forEach(inter.methods, (method) => {
527 if(method.description !== '') {
528 totalStatementDocumented += 1;
529 }
530 });
531 cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100);
532 if(totalStatements === 0) {
533 cl.coveragePercent = 0;
534 }
535 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
536 cl.status = getStatus(cl.coveragePercent);
537 totalProjectStatementDocumented += cl.coveragePercent;
538 files.push(cl);
539 });
540 _.forEach(this.configuration.mainData.pipes, (pipe) => {
541 let cl = {
542 filePath: pipe.file,
543 type: pipe.type,
544 name: pipe.name
545 },
546 totalStatementDocumented = 0,
547 totalStatements = 1;
548 if (pipe.description !== '') {
549 totalStatementDocumented += 1;
550 }
551 cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100);
552 cl.coverageCount = totalStatementDocumented + '/' + totalStatements;
553 cl.status = getStatus(cl.coveragePercent);
554 totalProjectStatementDocumented += cl.coveragePercent;
555 files.push(cl);
556 });
557 files = _.sortBy(files, ['filePath']);
558 var coverageData = {
559 count: Math.floor(totalProjectStatementDocumented / files.length),
560 status: ''
561 };
562 coverageData.status = getStatus(coverageData.count);
563 this.configuration.addPage({
564 name: 'coverage',
565 context: 'coverage',
566 files: files,
567 data: coverageData,
568 depth: 1,
569 pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT
570 });
571 }
572
573 processPages() {
574 logger.info('Process pages');
575 let pages = this.configuration.pages,
576 i = 0,
577 len = pages.length,
578 loop = () => {
579 if( i <= len-1) {
580 logger.info('Process page', pages[i].name);
581 $htmlengine.render(this.configuration.mainData, pages[i]).then((htmlData) => {
582 let finalPath = this.configuration.mainData.output;
583 if(this.configuration.mainData.output.lastIndexOf('/') === -1) {
584 finalPath += '/';
585 }
586 if (pages[i].path) {
587 finalPath += pages[i].path + '/';
588 }
589 finalPath += pages[i].name + '.html';
590 $searchEngine.indexPage({
591 infos: pages[i],
592 rawData: htmlData,
593 url: finalPath
594 });
595 fs.outputFile(path.resolve(finalPath), htmlData, function (err) {
596 if (err) {
597 logger.error('Error during ' + pages[i].name + ' page generation');
598 } else {
599 i++;
600 loop();
601 }
602 });
603 }, (errorMessage) => {
604 logger.error(errorMessage);
605 });
606 } else {
607 $searchEngine.generateSearchIndexJson(this.configuration.mainData.output).then(() => {
608 if (this.configuration.mainData.assetsFolder !== '') {
609 this.processAssetsFolder();
610 }
611 this.processResources();
612 }, (e) => {
613 logger.error(e);
614 });
615 }
616 };
617 loop();
618 }
619
620 processAssetsFolder() {
621 logger.info('Copy assets folder');
622
623 if (!fs.existsSync(this.configuration.mainData.assetsFolder)) {
624 logger.error(`Provided assets folder ${this.configuration.mainData.assetsFolder} did not exist`);
625 } else {
626 let that = this;
627 fs.copy(path.resolve(this.configuration.mainData.assetsFolder), path.resolve(process.cwd() + path.sep + this.configuration.mainData.output + path.sep + this.configuration.mainData.assetsFolder), function (err) {
628 if(err) {
629 logger.error('Error during resources copy ', err);
630 }
631 });
632 }
633 }
634
635 processResources() {
636 logger.info('Copy main resources');
637 let that = this;
638 fs.copy(path.resolve(__dirname + '/../src/resources/'), path.resolve(process.cwd() + path.sep + this.configuration.mainData.output), function (err) {
639 if(err) {
640 logger.error('Error during resources copy ', err);
641 }
642 else {
643 if (that.configuration.mainData.extTheme) {
644 fs.copy(path.resolve(process.cwd() + path.sep + that.configuration.mainData.extTheme), path.resolve(process.cwd() + path.sep + that.configuration.mainData.output + '/styles/'), function (err) {
645 if (err) {
646 logger.error('Error during external styling theme copy ', err);
647 } else {
648 logger.info('External styling theme copy succeeded');
649 that.processGraphs();
650 }
651 });
652 }
653 else {
654 that.processGraphs();
655 }
656 }
657 });
658 }
659
660 processGraphs() {
661
662 const onComplete = () => {
663 let finalTime = (new Date() - startTime) / 1000;
664 logger.info('Documentation generated in ' + this.configuration.mainData.output + ' in ' + finalTime + ' seconds using ' + this.configuration.mainData.theme + ' theme');
665 if (this.configuration.mainData.serve) {
666 logger.info(`Serving documentation from ${this.configuration.mainData.output} at http://127.0.0.1:${this.configuration.mainData.port}`);
667 this.runWebServer(this.configuration.mainData.output);
668 }
669 };
670
671 if (this.configuration.mainData.disableGraph) {
672
673 logger.info('Graph generation disabled');
674 onComplete();
675
676 } else {
677
678 logger.info('Process main graph');
679 let modules = this.configuration.mainData.modules,
680 i = 0,
681 len = modules.length,
682 loop = () => {
683 if( i <= len-1) {
684 logger.info('Process module graph', modules[i].name);
685 let finalPath = this.configuration.mainData.output;
686 if(this.configuration.mainData.output.lastIndexOf('/') === -1) {
687 finalPath += '/';
688 }
689 finalPath += 'modules/' + modules[i].name;
690 $ngdengine.renderGraph(modules[i].file, finalPath, 'f', modules[i].name).then(() => {
691 i++;
692 loop();
693 }, (errorMessage) => {
694 logger.error(errorMessage);
695 });
696 } else {
697 onComplete();
698 }
699 };
700 let finalMainGraphPath = this.configuration.mainData.output;
701 if(finalMainGraphPath.lastIndexOf('/') === -1) {
702 finalMainGraphPath += '/';
703 }
704 finalMainGraphPath += 'graph';
705 $ngdengine.renderGraph(this.configuration.mainData.tsconfig, path.resolve(finalMainGraphPath), 'p').then(() => {
706 loop();
707 }, (err) => {
708 logger.error('Error during graph generation: ', err);
709 });
710
711 }
712 }
713
714 runWebServer(folder) {
715 LiveServer.start({
716 root: folder,
717 open: this.configuration.mainData.open,
718 quiet: true,
719 logLevel: 0,
720 port: this.configuration.mainData.port
721 });
722 }
723
724 /**
725 * Return the application / root component instance.
726 */
727 get application():Application {
728 return this;
729 }
730
731
732 get isCLI():boolean {
733 return false;
734 }
735}