Skip to main content

๐Ÿ’ฑ Module Builder

Digital ALchemy core supports a builder pattern to help build up specialized modules for different use cases. The internal modules power:

  • CreateLibrary
  • CreateApplication
  • TestRunner

These can all be translated between each other, but the intended use for the builder is to facilitate unit testing. The tools also allow for the creation of modules free from the type constraints that are enforced on the normal creation tools.

๐Ÿ—ƒ๏ธ APIโ€‹

๐Ÿ“ Creationโ€‹

Module creation starts with the createModule import from @digital-alchemy/core.

import { createModule } from "@digital-alchemy/core";

// blank / empty module
// only contains access to core tools
const myModule = createModule();

Modules can be blank, but most useful modules will be based off an existing one.

import { LIB_SPECIAL_THING } from "./my_library";
import { HOME_AUTOMATION_APP } from "./main";

const genericFromLib = createModule.fromLibrary(LIB_SPECIAL_THING);
const genericFromApp = createModule.fromApplication(HOME_AUTOMATION_APP);

๐Ÿ’ข Gotchaโ€‹

If your code is based off the standard quickstart projects, you will have a main.ts that looks something like this:

const MY_APP = CreateApplication(...);
setImmediate(() => MY_APP.bootstrap(...));

Utilizing the module builder with your app will require moving the definition of your application to a separate file from the .bootstrap call.

๐Ÿค” Consider app.module.ts

๐Ÿ–Š๏ธ Extensionโ€‹

Once you have your generic module, you can extend from it to start the process of creating a new module. Care needs to be taken with using these tools to prevent the creation of invalid modules.

Librariesโ€‹

The module dependencies can be manipulated in 2 ways:

.appendLibrary(LIBRARY_MODULE)

import { LIB_MOCK_ASSISTANT } from "@digital-alchemy/hass";

myModule.extend().appendLibrary(LIB_MOCK_ASSISTANT);

Appended libraries will be loaded at the same the as those defined with the creation of the module, but before the module itself initializes. This gives the appended library the ability to get ahead and perform monkey patching operations & set up state.

You also have the ability to outright replace an existing library with another that shares the same name. The name is the only check made against the module, no attempt to ensure compatibility with types beyond that is made.

.replaceLibrary(HOPEFULLY_COMPAT_MODULE)

myModule.extend().replaceLibrary(LIB_REPLACEMENT_HASS);

Servicesโ€‹

Services can be manipulated in similarly unsafe ways to libraries. You are able to strip out services with functionality that makes tests difficult, make subsets of services operate in isolation, and more.

OperationDescription
.appendService(name: string, service: ServiceFunction)Add a new service to the module as if were part of the original definition (type unsafe)
.replaceService(name: string, service: ServiceFunction)Replace service with a new one (type unsafe)
.rebuild(services: Partial<ORIGINAL_SERVICES>)Replace the entire service map (ymmv)
.omitService(...name: string)Remove service(s) by name
.pickService(...name: string)Keep listed services, remove others

๐Ÿ“’ Castingโ€‹

Once your module has all of it's substitutions made, you can perform a final transformation to get a more useful module.

  • .toApplication()
  • .toLibrary()
  • .toTest()

Full example of creating a test runner from an application

import { createModule } from "@digital-alchemy/core";
import { HOME_AUTOMATION_APP } from "./main";
import { LIB_MOCK_ASSISTANT } from "@digital-alchemy/hass";

export const testRunner = createModule
.fromApplication(HOME_AUTOMATION_APP)
.extend()
.appendLibrary(LIB_MOCK_ASSISTANT)
.toTest();