@shrub/core

A framework for modular server-side applications and front-end components.

Usage no npm install needed!

<script type="module">
  import shrubCore from 'https://cdn.skypack.dev/@shrub/core';
</script>

README

Description

Shrub is a simple framework for building modular server-side applications and front-end components. The Core package provides a basic API for defining and loading modules.

Creating a Module

A module can be defined as a class that implements the IModule interface or as a JSON object that satisfies the IModule interface.

export class FooModule implements IModule {
    readonly name = "foo";
}
const fooModule: IModule = {
    name: "foo"
};

There are a few methods a module can define that allow it to configure functionality provided by the module and each method is invoked in the following order.

1. initialize(init: IModuleInitializer): void

Allows a module the ability to perform pre-initialization that needs to happen before any modules or services are configured. Such tasks include binding settings or registering a configuration interface that is exposed to other modules.

2. configureServices(registration: IServiceRegistration): void

Register services with a service collection.

3. configure(configurator: IModuleConfigurator): void | Promise<void>

Allows a module to perform additional configuration, such as configure a dependency module. This method supports async operations which can be useful if data needs to be fetched from a remote service during configuration.

Dependencies

Module depenedencies are specified by defining a dependencies property on a module and lifecycle methods get invoked on the dependency before they are invoked on the dependent.

export class FooModule implements IModule {
    readonly name = "foo";
    readonly dependencies = [BarModule];
}

Configuration

export const IBarModuleConfiguration = createConfig<IBarModuleConfiguration>();
export interface IBarModuleConfiguration {
    registerWidget(widget: IWidget): void;
}

export class BarModule implements IModule {
    readonly name = "bar";

    initialize({ config }: IModuleInitializer): void {
        config(IBarModuleConfiguration).register(() => ({
            registerWidget: widget => {}
        }));
    }
}

export class FooModule implements IModule {
    readonly name = "foo";
    readonly dependencies = [BarModule];

    configure({ config }: IModuleConfigurator): void {
        config.get(IBarModuleConfiguration).registerWidget({});
    }
}

Settings and Options

While Dependency Configuration is a way for one module to configure another at load time. Settings provide a way to provide settings/configuration externally, such as from a config file.

Module Settings are provided as a simple object keyed by a module's name; for example, the below is an example settings object that defines settings for the 'foo' module:

const settings = {
    foo: {
        key: value
    }
};

A module can access these settings directly from the configure method:

export class FooModule implements IModule {
    readonly name = "foo";

    configure({ settings }: IModuleConfigurator): void {
        const keyValue = settings.key;
    }
}

Sometimes it's useful to pass settings to service instances and this can be done via Service Options.

export const IFooOptions = createOptions<IFooOptions>("foo-options");
export interface IFooOptions {
    readonly value: string;
}

export class FooModule implements IModule {
    readonly name = "foo";

    initialize({ settings }: IModuleInitializer): void {
        settings.bindToOptions(IFooOptions);
    }

    configureServices(registration: IServiceRegistration): void {
        registration.registerTransient(IFooService, FooService);
    }
}

export class FooService implements IFooService {
    constructor(@IFooOptions private readonly options: IFooOptions) {
    }
}

Loading Modules

Modules are loaded using the ModuleLoader class by simply invoking ModuleLoader.load.

await ModuleLoader.load([
    FooModule,
    BarModule
]);

Note: If a module has any dependencies not specified when calling ModuleLoader.load those dependencies will automatically get loaded.

If you want a little more control the module loader provides additional methods for configuring the service collection or settings.

await ModuleLoader()
    .useModules([
        FooModule,
        BarModule
    ])
    .useSettings({
        foo: { value: "Hello!" }
    })
    .load();