Skip to main content

Module Extension

createModule is a builder that wraps an existing application or library definition and provides a chainable API for modifying it before finalizing. It's primarily used for creating test variants, composing shared service sets, or overriding specific services for a particular entrypoint.

Creating a module​

You can start from scratch or wrap an existing application or library:

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

// From an existing application
const module = createModule.fromApplication(MY_APP);

// From an existing library
const module = createModule.fromLibrary(MY_LIB);

// From scratch (less common)
const module = createModule({
name: "my_module",
services: { ... },
configuration: { ... },
depends: [],
optionalDepends: [],
priorityInit: [],
});

ModuleExtension — chainable operations​

Call .extend() on a DigitalAlchemyModule to get a ModuleExtension builder. All methods return the same builder for chaining. Mutations are applied immediately — all methods throw on invalid operations.

const extended = createModule.fromApplication(MY_APP).extend();

appendLibrary(library)​

Add a library that isn't in the base module's depends. Throws if the library is already present (use replaceLibrary instead).

extended.appendLibrary(EXTRA_LIB);

appendService(name, fn)​

Add a service not in the base module. Throws if a service with that name already exists.

extended.appendService("metrics", MetricsService);

replaceLibrary(library)​

Swap an existing library for a different implementation. The library's name must match an existing dependency. Use for mocking an entire library in tests.

extended.replaceLibrary(MOCK_DATABASE_LIB);

replaceService(name, fn)​

Swap a specific service for a different implementation. The name must match an existing service. TypeScript updates the return type to match the new function.

extended.replaceService("database", MockDatabaseService);

Throws if name doesn't exist in services.

pickService(...names)​

Keep only the named services, drop everything else. Useful for creating a minimal test module.

extended.pickService("auth", "users");

Throws if any name doesn't exist.

omitService(...names)​

Remove specific services from the module.

extended.omitService("metrics", "scheduler");

Throws if any name doesn't exist.

rebuild(services)​

Replace the entire service map with a new one. Useful when you want to construct the service set from scratch while keeping the module's configuration and dependencies.

extended.rebuild({
api: MockApiService,
cache: MockCacheService,
});

Terminal methods​

Terminal methods finalize the builder and return a usable module.

toApplication()​

Returns an ApplicationDefinition with all accumulated changes. Can be bootstrapped directly.

const testApp = createModule.fromApplication(MY_APP)
.extend()
.replaceLibrary(MOCK_DATABASE_LIB)
.appendService("testHelper", TestHelperService)
.toApplication();

await testApp.bootstrap();

toLibrary()​

Returns a LibraryDefinition with all accumulated changes.

const extendedLib = createModule.fromLibrary(MY_LIB)
.extend()
.appendService("extra", ExtraService)
.toLibrary();

toTest()​

Returns an iTestRunner (a configured TestRunner instance) targeting the built application. Shortcut for TestRunner({ target: extend.toApplication() }).

const runner = createModule.fromApplication(MY_APP)
.extend()
.replaceLibrary(MOCK_DATABASE_LIB)
.toTest();

await runner.run(async ({ my_app }) => {
// test code
});

Typical patterns​

Create a test variant of an app:

// test-app.mts
export const TEST_APP = createModule.fromApplication(MY_APP)
.extend()
.replaceLibrary(MockDatabaseLib)
.replaceLibrary(MockCacheLib)
.toApplication();

Create a minimal integration test module:

createModule.fromApplication(MY_APP)
.extend()
.pickService("auth", "users")
.toTest()
.run(async ({ my_app }) => {
// only auth and users services are present
});