Getting Started
Modular applications tend to have a lot of tedious and error-prone complexity that emerges from the unavoidable need to wire up and manage the configuration and lifecycles of multiple interconnected subsystems. The goal of this library is to reduce this complexity by providing some consistent yet flexible structure around these needs, without imposing the constraints of a rigid framework.
In this library, Modules are fundamentally an abstracted way to provide encapsulated services throughout an application, via a dependency graph of automatically instantiated singletons that support dependency injection.
This example shows the smallest possible application that leverages the ModuleManager:
a single application module, with a single configurable field. (This is of course rather silly,
as the entire point of this library is to tame the complexity of larger applications that are built of multiple
interconnected subsystems!)
% npm install --save @versionzero/module-manager
import { ModuleManager } from '@versionzero/module-manager';
class WelcomeApp {
static moduleConfigurables = [{ field: 'message' }]
message = 'configure me!';
async main() {
console.log(this.message);
}
}
await new ModuleManager()
.register(WelcomeApp)
.run()
% node welcome.js --message "hello world"
or
% export WELCOME_APP_MESSAGE="hello world"
% node welcome.js
Modules have a variety of optional settings to control their behavior, which can either be specified in static module properties or provided during registration. Some settings are set to implicit defaults based on the properties of the registered module class itself.
In this case, the WelcomeApp module is implicitly treated as the entry point of
the application, and is set up for implicit dependency injection of a single configurable
message field. Configurable field values can be provided in a variety of ways; built-in
defaults, environment variables, command line arguments, configuration files, or even user
extensions such as cloud secrets providers. These capabilities are provided by the Configurator library,
documented in its own section on this site.
When ModuleManager.run() is called, it acts as an "inversion of control" (IoC) container, and choreographs
execution by first populating a validated configuration object containing requested simple field
values (as well as the resolved instances of any referenced module dependencies.)
It then runs opt-in lifecycle methods on all instantiated modules in phases:
(load configuration + instantiate dependency graph)
-> initialize
-> start
-> [main]
-> stop
-> terminate
-> (process exits)
Only referenced modules are instantiated. During the initialization phase, the configurables
requested by each module will be either passed to the init lifecycle method, or directly
injected into the module.