UNPKG

dgeni

Version:

Flexible JavaScript documentation generator used by both AngularJS and Angular

265 lines (220 loc) 9.14 kB
/* tslint globals: require: true */ const di = require('di'); import {Package, PackageRef} from './Package'; import {Injector} from './Injector'; import {Processor} from './Processor'; import {processorValidationPackage} from './legacyPackages/processorValidation'; import {sortByDependency} from './util/dependency-sort'; import {getInjectablesFactory} from './util/getInjectables'; import {logFactory} from './util/log'; /** * Create an instance of the Dgeni documentation generator, loading any packages passed in as a * parameter. * @param {Package[]} [packages] A collection of packages to load */ export class Dgeni { static Package = Package; injector: Injector; packages: Record<string, Package>|Package[] = {}; processors: Processor[]; stopOnProcessingError: boolean; handlerMap: {[key: string]: Function[]}; constructor(packages: Package[] = []) { if ( !Array.isArray(packages) ) { throw new Error('packages must be an array'); } // Add in the legacy validation that was originally part of the core Dgeni tool. this.package(processorValidationPackage); packages.map(p => this.package(p)); } /** * Load a package into dgeni * @param package The package to load or the name of a new package to create. * @param dependencies A collection of dependencies for this package * @return The package that was loaded, to allow chaining */ package(pkg: PackageRef, dependencies: PackageRef[] = []) { if ( this.injector) { throw new Error('injector already configured - you cannot add a new package'); } if ( typeof pkg === 'string' ) { pkg = new Package(pkg, dependencies); } if ( !(Package.isPackage(pkg)) ) { throw new Error('package must be an instance of Package'); } if ( this.packages[pkg.name] ) { throw new Error('The "' + pkg.name + '" package has already been loaded'); } this.packages[pkg.name] = pkg; // Extract all inline packages and load them into dgeni; pkg.namedDependencies = pkg.dependencies.map((dependency) => { if ( Package.isPackage(dependency) ) { // Only load dependent package if not already loaded if ( !this.packages[dependency.name] ) { this.package(dependency); } return dependency.name; } return dependency; }); // Return the package to allow chaining return pkg; } /** * Configure the injector using the loaded packages. * * The injector is assigned to the `injector` property on `this`, which is used by the * `generate()` method. Subsequent calls to this method will just return the same injector. * * This method is useful in unit testing services and processors as it gives an easy way to * get hold of an instance of a ready instantiated component without having to load in all * the potential dependencies manually: * * ``` * const Dgeni = require('dgeni'); * * function getInjector() { * const dgeni = new Dgeni(); * dgeni.package('testPackage', [require('dgeni-packages/base')]) * .factory('templateEngine', function dummyTemplateEngine() {}); * return dgeni.configureInjector(); * }; * * describe('someService', function() { * const someService; * beforeEach(function() { * const injector = getInjector(); * someService = injector.get('someService'); * }); * * it("should do something", function() { * someService.doSomething(); * ... * }); * }); * ``` */ configureInjector() { if ( !this.injector ) { // Sort the packages by their dependency - ensures that services and configs are loaded in the // correct order const packages: Package[] = this.packages = sortByDependency(this.packages, 'namedDependencies'); // Create a module containing basic shared services this.stopOnProcessingError = true; const dgeniModule = new di.Module() .value('dgeni', this) .factory('log', logFactory) .factory('getInjectables', getInjectablesFactory); // Create the dependency injection container, from all the packages' modules const modules = packages.map(pkg => pkg.module); modules.unshift(dgeniModule); // Create the injector and const injector = this.injector = new di.Injector(modules); // Apply the config blocks packages.forEach((pkg) => pkg.configFns.forEach((configFn) => injector.invoke(configFn))); // Get the the processors and event handlers const processorMap = {}; this.handlerMap = {}; packages.forEach((pkg) => { pkg.processors.forEach(function(processorName) { const processor = injector.get(processorName); // Update the processor's name and package processor.name = processorName; processor.$package = pkg.name; // Ignore disabled processors if ( processor.$enabled !== false ) { processorMap[processorName] = processor; } }); for (const eventName in pkg.handlers) { const handlers: Function[] = this.handlerMap[eventName] = (this.handlerMap[eventName] || []); pkg.handlers[eventName].forEach(handlerName => handlers.push(injector.get(handlerName))); } }); // Once we have configured everything sort the processors. // This allows the config blocks to modify the $runBefore and $runAfter properties of processors. // (Crazy idea, I know, but useful for things like debugDumpProcessor) this.processors = sortByDependency(processorMap, '$runAfter', '$runBefore'); } return this.injector; } /** * Generate the documentation using the loaded packages * @return {Promise} A promise to the generated documents */ generate() { const injector = this.configureInjector(); const log = injector.get('log'); let processingPromise = this.triggerEvent('generationStart'); // Process the docs const currentDocs = []; processingPromise = processingPromise.then(() => currentDocs); this.processors.forEach(processor => { processingPromise = processingPromise.then(docs => this.runProcessor(processor, docs)); }); processingPromise.catch(error => { log.error(error.message); if (error.stack) { log.debug(error.stack); } log.error('Failed to process the docs'); }); return processingPromise.then(docs => { this.triggerEvent('generationEnd'); return docs; }); } runProcessor(processor, docs) { const log = this.injector.get('log'); const promise = Promise.resolve(docs); if ( !processor.$process ) { return promise; } return promise .then(() => { log.info('running processor:', processor.name); return this.triggerProcessorEvent('processorStart', processor, docs); }) // We need to wrap this $process call in a new promise handler so that we can catch // errors triggered by exceptions thrown in the $process method // before they reach the promise handlers .then(docs => processor.$process(docs) || docs) .then(docs => this.triggerProcessorEvent('processorEnd', processor, docs)) .catch((error) => { error.message = 'Error running processor "' + processor.name + '":\n' + error.message; if ( this.stopOnProcessingError ) { return Promise.reject(error); } else { log.error(error.message); } return docs; }); } /** * Trigger a dgeni event and run all the registered handlers * All the arguments to this call are passed through to each handler * @param {string} eventName The event being triggered * @return {Promise} A promise to an array of the results from each of the handlers */ triggerEvent(eventName: string, ...extras: any[]) { const handlers = this.handlerMap[eventName]; let handlersPromise = Promise.resolve(); const results = []; if (handlers) { handlers.forEach(handler => { handlersPromise = handlersPromise.then(() => { const handlerPromise = Promise.resolve(handler(eventName, ...extras)); handlerPromise.then(result => results.push(result)); return handlerPromise; }); }); } return handlersPromise.then(() => results); } triggerProcessorEvent(eventName: string, processor, docs) { return this.triggerEvent(eventName, processor, docs).then(() => docs); } info() { const injector = this.configureInjector(); const log = injector.get('log'); for (const pkgName in this.packages) { log.info(pkgName, '[' + this.packages[pkgName].dependencies.map(function(dep) { return JSON.stringify((dep as Package).name); }).join(', ') + ']'); } log.info('== Processors (processing order) =='); this.processors.forEach((processor, index) => { log.info((index + 1) + ': ' + processor.name, processor.$process ? '' : '(abstract)', ' from ', processor.$package); if ( processor.description ) { log.info(' ', processor.description); } }); } }