Skip to main content

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.

settingtypepurpose
namestringmodule name; inferred when possible
configurablesarrayconfigurable schema definition
injectbooleanwhether to inject dependencies; inferred true if init is not implemented
lifecyclebooleanwhether to run lifecycle methods
isMainbooleanmarks entry point; inferred true if main is implemented
referencesarrayother modules used, see below
providesstringabstract/interface type name to provide
configstringif full, init receives entire application config
lifecycleInitstringmethod name override; defaults to init
lifecycleStartstring...defaults to start
lifecycleMainstring...defaults to main
lifecycleStopstring...defaults to stop
lifecycleTerminatestring...defaults to terminate

These settings can be provided in one of three ways:

  1. as an individual static value in the module class prepended with module, in camelCase:
class MyModule {
static moduleLifecycle = false;
}
  1. as a member of a static moduleInfo object:
class MyModule {
static moduleInfo = {
lifecycle: false
}
}
  1. as part of the options object passed to the register call:
class MyModule {}
moduleManager.register(MyModule, { lifecycle: false })
tip

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..

output.js
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;

myapp.js
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();