Skip to content
This repository was archived by the owner on Oct 17, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ how to validate the configuration of the Processor.
to validate the properties of this Processor.


** Note that the validation feature has been moved to its own Dgeni Package `processorValidation`.
Currently dgeni automatically adds this new package to a new instance of dgeni so that is still available
for backward compatibility. In a future release this package will be moved to `dgeni-packages`.**

### Defining a Processor

You define Processors just like you would a Service:
Expand Down Expand Up @@ -334,6 +338,60 @@ myPackage.config(function(readFilesProcessor) {
});
```

## Dgeni Events

In Dgeni you can trigger and handle **events** to allow packages to take part in the processing
lifecycle of the documentation generation.


### Triggering Events

You trigger an event simply by calling `triggerEvent(eventName, ...)` on a `Dgeni` instance.

The `eventName` is a string that identifies the event to be triggered, which is used to wire up
event handlers. Additional arguments are passed through to the handlers.

Each handler that is registered for the event is called in series. The return value
from the call is a promise to the event being handled. This allows event handlers to be async.
If any handler returns a rejected promise the event triggering is cancelled and the rejected
promise is returned.

For example:

```js
var eventPromise = dgeni.triggerEvent('someEventName', someArg, otherArg);
```

### Handling Events

You register an event handler in a `Package`, by calling `handleEvent(eventName, handlerFactory)` on
the package instance. The handlerFactory will be used by the DI system to get the handler, which allows
you to inject services to be available to the handler.

The handler factory should return the handler function. This function will receive all the arguments passed
to the `triggerHandler` method. As a minimum this will include the `eventName`.

For example:

```js
myPackage.eventHandler('generationStart', function validateProcessors(log, dgeni) {
return function validateProcessorsImpl(eventName) {
...
};
});

```

### Built-in Events

Dgeni itself triggers the following events during documentation generation:

* `generationStart`: triggered after the injector has been configured and before the processors begin
their work.
* `generationEnd`: triggered after the processors have all completed their work successfully.
* `processorStart`: triggered just before the call to `$process`, for each processor.
* `processorEnd`: triggered just after `$process` has completed successfully, for each processor.

## License

Apache 2
196 changes: 126 additions & 70 deletions lib/Dgeni.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ var _ = require('lodash');
var di = require('di');
var Package = require('./Package');
var sortByDependency = require('./util/dependency-sort');
var validate = require('validate.js');
var Q = require('q');
var validationPackage = require('./legacyPackages/processorValidation');

/**
* Create an instance of the Dgeni documentation generator, loading any packages passed in as a
Expand All @@ -15,6 +15,10 @@ function Dgeni(packages) {

packages = packages || [];
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(validationPackage);

_.map(packages, this.package, this);
}

Expand All @@ -32,6 +36,8 @@ Dgeni.Package = Package;
*/
Dgeni.prototype.package = function(package, dependencies) {

if ( this.injector) { throw new Error('injector already configured - you cannot add a new package'); }

if ( typeof package === 'string' ) { package = new Package(package, dependencies); }
if ( !(Package.isPackage(package)) ) { throw new Error('package must be an instance of Package'); }
if ( this.packages[package.name] ) {
Expand Down Expand Up @@ -89,19 +95,18 @@ Dgeni.prototype.package = function(package, dependencies) {
*/
Dgeni.prototype.configureInjector = function() {

if ( !this.injector ) {
var dgeni = this;

if ( !dgeni.injector ) {

// Sort the packages by their dependency - ensures that services and configs are loaded in the
// correct order
var packages = sortByDependency(this.packages, 'namedDependencies');
var packages = dgeni.packages = sortByDependency(dgeni.packages, 'namedDependencies');

// Create a module containing basic shared services
var dgeniConfig = {
stopOnValidationError: true,
stopOnProcessingError: true
};
dgeni.stopOnProcessingError = true;
var dgeniModule = new di.Module()
.value('dgeni', dgeniConfig)
.value('dgeni', dgeni)
.factory('log', require('./util/log'))
.factory('getInjectables', require('./util/getInjectables'));

Expand All @@ -110,8 +115,7 @@ Dgeni.prototype.configureInjector = function() {
modules.unshift(dgeniModule);

// Create the injector and
var injector = this.injector = new di.Injector(modules);

var injector = dgeni.injector = new di.Injector(modules);

// Apply the config blocks
packages.forEach(function(package) {
Expand All @@ -120,110 +124,162 @@ Dgeni.prototype.configureInjector = function() {
});
});

// Get the collection of processors
// We use a Map here so that we only get one of each processor name
var processors = this.processors = {};
// Get the the processors and event handlers
var processorMap = {};
dgeni.handlerMap = {};
packages.forEach(function(package) {

package.processors.forEach(function(processorName) {
var processor = injector.get(processorName);
// Update the processor's name and package
processor.name = processorName;
processor.$package = package.name;

// Ignore disabled processors
if ( processor.$enabled !== false ) {
processors[processorName] = processor;
processorMap[processorName] = processor;
}
});

for(eventName in package.handlers) {
var handlers = dgeni.handlerMap[eventName] = (dgeni.handlerMap[eventName] || []);
package.handlers[eventName].forEach(function(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)
dgeni.processors = sortByDependency(processorMap, '$runAfter', '$runBefore');
}

return this.injector;
return dgeni.injector;
};

/**
* Generate the documentation using the loaded packages
* @return {Promise} A promise to the generated documents
*/
Dgeni.prototype.generate = function() {
var dgeni = this;
var injector = dgeni.configureInjector();
var log = injector.get('log');

this.configureInjector();

var dgeniConfig = this.injector.get('dgeni');
var processingPromise = dgeni.triggerEvent('generationStart');

// 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)
processors = sortByDependency(this.processors, '$runAfter', '$runBefore');
// Process the docs
var currentDocs = [];
processingPromise = processingPromise.then(function() {
return currentDocs;
});

var log = this.injector.get('log');
var processingPromise = Q();
var validationErrors = [];
_.forEach(dgeni.processors, function(processor) {

// Apply the validations on each processor
_.forEach(processors, function(processor) {
processingPromise = processingPromise.then(function() {
return validate.async(processor, processor.$validate).catch(function(errors) {
validationErrors.push({
processor: processor.name,
package: processor.$package,
errors: errors
});
log.error('Invalid property in "' + processor.name + '" (in "' + processor.$package + '" package)');
log.error(errors);
});
processingPromise = processingPromise.then(function(docs) {
return dgeni.runProcessor(processor, docs);
});
});

processingPromise = processingPromise.then(function() {
if ( validationErrors.length > 0 && dgeniConfig.stopOnValidationError ) {
return Q.reject(validationErrors);
}
});
processingPromise.catch(function(error) {
log.error('Error processing docs: ', error.stack || error.message || error );
})

// Process the docs
var currentDocs = [];
processingPromise = processingPromise.then(function() {
return currentDocs;
return processingPromise.then(function(docs) {
dgeni.triggerEvent('generationEnd');
return docs;
});
};


_.forEach(processors, function(processor) {
Dgeni.prototype.runProcessor = function(processor, docs) {
var dgeni = this;
var log = dgeni.injector.get('log');
var promise = Q(docs);

if( !processor.$process ) {
return promise;
}

if( processor.$process ) {
return promise

processingPromise = processingPromise.then(function(docs) {
currentDocs = docs;
log.info('running processor:', processor.name);
.then(function() {
log.info('running processor:', processor.name);
return dgeni.triggerProcessorEvent('processorStart', processor, docs);
})

return Q(currentDocs).then(function() {
// 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 processingPromise handlers
return processor.$process(docs) || docs;
}).catch(function(error) {
.then(function(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
return processor.$process(docs) || docs;
})

error.message = 'Error running processor "' + processor.name + '":\n' + error.message;
if ( error.stack ) {
log.error(error.stack);
}
.then(function(docs) {
return dgeni.triggerProcessorEvent('processorEnd', processor, docs);
})

if ( dgeniConfig.stopOnProcessingError ) { return Q.reject(error); }
.catch(function(error) {

return currentDocs;
error.message = 'Error running processor "' + processor.name + '":\n' + error.message;
log.error(error.stack || error.message);

if ( dgeni.stopOnProcessingError ) { return Q.reject(error); }

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
*/
Dgeni.prototype.triggerEvent = function(eventName) {
var args = arguments;
var handlers = this.handlerMap[eventName];
var handlersPromise = Q();
var results = [];
if (handlers) {
handlers.forEach(function(handler) {
handlersPromise = handlersPromise.then(function() {
var handlerPromise = Q(handler.apply(null, args));
handlerPromise.then(function(result) {
results.push(result);
});
return handlerPromise;
});
});
}
return handlersPromise.then(function() {
return results;
});
};

}

return currentDocs;
Dgeni.prototype.triggerProcessorEvent = function(eventName, processor, docs) {
return this.triggerEvent(eventName, processor, docs).then(function() { return docs; });
};


Dgeni.prototype.info = function() {
var injector = this.configureInjector();
var log = injector.get('log');

this.packages.forEach(function(pkg, index) {
log.info((index+1) + ': ' + pkg.name, '[' + pkg.dependencies.map(function(dep) { return JSON.stringify(dep.name); }).join(', ') + ']');
});

processingPromise.catch(function(error) {
log.error('Error processing docs: ', error );
log.info('== Processors (processing order) ==');

this.processors.forEach(function(processor, index) {
log.info((index+1) + ': ' + processor.name, processor.$process ? '' : '(abstract)', ' from ', processor.$package);
if ( processor.description ) { log.info(' ', processor.description); }
});
}

return processingPromise;
};

/**
* @module Dgeni
Expand Down
Loading