Modules
Modules are essentially named singletons structured in a dependency graph.
They may be classes (which will be instantiated on-demand):
class MyModule {}
moduleManager.register(MyModule, options);
These classes should have a no-argument constructor.
Or, they can be pre-created instances:
const myModule = new MyModule();
moduleManager.registerInstance(myModule, options);
You can also register fairly arbitrary objects as instances, but you will need to specify a module name via the registration options:
const foo = {bar: true}
moduleManager.registerInstance(foo, {name: "foo"})
Module Settings
Each module has optional settings that control how it is managed.
| setting | type | purpose |
|---|---|---|
name | string | module name; inferred when possible |
configurables | array | configurable schema definition |
inject | boolean | whether to inject dependencies; inferred true if init is not implemented |
lifecycle | boolean | whether to run lifecycle methods |
isMain | boolean | marks entry point; inferred true if main is implemented |
references | array | other modules used, see below |
provides | string | abstract/interface type name to provide |
config | string | if full, init receives entire application config |
lifecycleInit | string | method name override; defaults to init |
lifecycleStart | string | ...defaults to start |
lifecycleMain | string | ...defaults to main |
lifecycleStop | string | ...defaults to stop |
lifecycleTerminate | string | ...defaults to terminate |
These settings can be provided in one of three ways:
- as an individual static value in the module class prepended with
module, in camelCase:
class MyModule {
static moduleLifecycle = false;
}
- as a member of a static
moduleInfoobject:
class MyModule {
static moduleInfo = {
lifecycle: false
}
}
- as part of the
optionsobject passed to theregistercall:
class MyModule {}
moduleManager.register(MyModule, { lifecycle: false })
Module settings are individually merged with base class settings, allowing selective overrides. Any settings passed to the register call then override any internally defined settings.
Module References
Some modules make use of internal sub-modules that may well require their own configuration. Exporting all these to the top level of the application would add unnecessary direct dependencies, so modules can provide a list of modules to register when they are themselves loaded.
For example, this might be in one file..
import { ModuleManager} from '@versionzero/module-manager';
class Formatter {
static moduleProvides = "formatter"
format(s) { return s }
}
class UpperCase extends Formatter {
format(s) {
return s.toUpperCase();
}
}
class EndPadder extends Formatter {
static moduleInfo = {
name: 'pad',
configurables: [
{field: "length", type: "number", validator: "$positive", default: 50},
{field: "pad", type: "string", default: " "}
]
}
format(s) {
return s.padEnd(this.length, this.pad);
}
}
class Replacer extends Formatter {
static moduleConfigurables = [
{field: "pattern", type: "string", default: "x"},
{field: "replacement", type: "string", default: "_"}
]
format(s) {
return s.replaceAll(this.pattern, this.replacement);
}
}
export class Output {
static moduleInfo = {
configurables: [{field: "formatter", type: "formatter"}],
references: [Replacer, {ModuleClass: UpperCase, options: { name: 'upper'}}, EndPadder]
}
write(s) {
const line = (this.formatter? this.formatter.format(s) : s);
console.log(line);
}
}
this way, the application doesn't need to know the internal modules;
import { ModuleManager } from '@versionzero/module-manager';
import { Output } from './output.js'
class MyApp {
static moduleReferences = [Output]
static moduleConfigurables = [{field: 'output', type: 'Output'}, {field: 'message', default: 'hello, world'}]
async main() {
this.output.write(this.message);
}
}
await new ModuleManager()
.register(MyApp)
.run();