Why
Your application codebases grow, the code become coupled and messy ā hard to reuse, hard to share.TsdkArclets you compose modules like building blocks, nest them, and share them across projects.
Each module declares what it needs and what it provides. Then calling start([modules]) resolves the full dependency graph, boots modules in order, and returns a typed context.
Quickstart
Define modules, reuse the modules, and run application.
import { defineModule, type ContextOf } from "tsdkarc"; // 1. Define Config (Returns namespace: ctx.config) const configModule = defineModule({ name: "config" }).init(() => ({ port: 3000, })); // 2. Define Server (Depends on Config) const serverModule = defineModule({ name: "server", modules: [configModule], }).init((ctx) => ({ listen: () => console.log(`š Server running on port ${ctx.config.port}`), })); // Export inferred types for use elsewhere in your app export type AppCtx = ContextOf<typeof serverModule>; // 3. Compose and Launch async function bootstrap() { try { // Wrap top-level modules in an anonymous root module const appModule = defineModule({ modules: [serverModule] }).init( () => ({}) ); // Display the generated dependency graph console.log("\nDependency Tree:\n" + appModule.graph().formatted); const app = await appModule.start({ afterBoot: () => console.log("ā All modules booted successfully!"), }); // Run the app! (Fully type-safe) app.ctx.server.listen(); } catch (error) { // tsdkarc automatically rolls back already-booted modules if a failure occurs here console.error("ā Failed to boot application:", error); process.exit(1); } } bootstrap();
Core Concepts
| Term | Description |
|---|---|
| Slice | The shape a module adds to the shared context ({ key: Type }) |
| Module | Declares dependencies, registers values, and optionally tears them down |
| Context | The merged union of all slices ā fully typed at each module's boundary |
Online Playground
API Reference
defineModule
import { defineModule, type AnyModule } from 'tsdkarc'; // Step 1: Configuration (Identity & Dependencies) defineModule(meta?: { name?: string; // Optional: Namespaces the returned state under ctx[name] modules?: AnyModule[]; // Optional: Array of modules this module depends on ignoreConflicts?: string[]; // Optional: Keys allowed to deep-merge if anonymous module fields collide }) // Step 2: Implementation (Logic & Lifecycle) .init( // The boot function: receives dependency ctx, returns this module's state/API bootFn?: (ctx: DepContext) => OwnSlice | Promise<OwnSlice> | void | Promise<void>, // Module-level lifecycle hooks (scoped ONLY to this module) hooks?: { beforeBoot?(ctx: DepContext): any | Promise<any>; afterBoot?(ctx: DepContext): any | Promise<any>; beforeShutdown?(ctx: DepContext): any | Promise<any>; shutdown?(ctx: DepContext): any | Promise<any>; afterShutdown?(ctx: DepContext): any | Promise<any>; } ) // -> Returns an InitializedModule with the following methods: /* => { // Starts the composition (boots all dependencies in topological order) start(options?: StartOptions): Promise<{ ctx: FinalCtx; stop(reason?: string): Promise<void>; }>; // Injects additional dependencies on the fly (returns a new module instance) with(...modules: AnyModule[]): InitializedModule; // Generates a visual and structural dependency tree graph(): { formatted: string; // The printable tree string // ... internal nodes/edges array ... }; } */
start
import { defineModule, type ModuleMeta } from 'tsdkarc'; // .start() is a method chained off your root initialized module const rootModule = defineModule({ modules: [/* ... */] }).init(); rootModule.start(options?: { // Global boot hooks beforeBoot?(ctx: Record<never, never>): any | Promise<any>; afterBoot?(ctx: Context): any | Promise<any>; // Global shutdown hooks (includes optional reason) beforeShutdown?(ctx: Context, reason?: string): any | Promise<any>; afterShutdown?(ctx: Context, reason?: string): any | Promise<any>; // Per-module boot hooks beforeEachBoot?(ctx: object, meta: ModuleMeta): any | Promise<any>; afterEachBoot?(ctx: object, meta: ModuleMeta): any | Promise<any>; // Per-module shutdown hooks beforeEachShutdown?(ctx: object, meta: ModuleMeta, reason?: string): any | Promise<any>; afterEachShutdown?(ctx: object, meta: ModuleMeta, reason?: string): any | Promise<any>; // ā onError is REMOVED. // If boot fails, .start() automatically rolls back and throws the error directly. }): Promise<{ ctx: Context; stop(reason?: string): Promise<void>; }>;
Dependency Chain
Downstream modules declare upstream modules and get their context fully typed. start() walks the dependency graph and deduplicates ā each module boots exactly once.
import { defineModule, type ContextOf } from "tsdkarc"; // 1. Define Config (Returns namespace: ctx.config) const configModule = defineModule({ name: "config" }).init(() => ({ port: 3000, })); // 2. Define Server (Depends on Config) const serverModule = defineModule({ name: "server", modules: [configModule], }).init((ctx) => ({ listen: () => console.log(`š Server running on port ${ctx.config.port}`), })); // Export inferred types for use elsewhere in your app export type AppCtx = ContextOf<typeof serverModule>; // 3. Compose and Launch async function bootstrap() { try { // Wrap top-level modules in an anonymous root module const appModule = defineModule({ modules: [serverModule] }).init(); // Display the generated dependency graph console.log("\nDependency Tree:\n" + appModule.graph().formatted); const app = await appModule.start({ afterBoot: () => console.log("ā All modules booted successfully!"), }); // Run the app! (Fully type-safe) app.ctx.server.listen(); } catch (error) { // tsdkarc automatically rolls back already-booted modules if a failure occurs here console.error("ā Failed to boot application:", error); process.exit(1); } } bootstrap();
Patterns
Register anything, not just data
Functions, class instances, and middleware are all valid context values.
import { defineModule, type ContextOf } from "tsdkarc"; import type { Request, Response, NextFunction } from "express"; export const authModule = defineModule({ name: "auth", }).init(() => { return { authenticate: (req: Request, res: Response, next: NextFunction) => { if (!req.headers.authorization) { return res.status(401).end(); } next(); }, }; }); // Optional: If you need to export the inferred type for use elsewhere export type AuthModuleCtx = ContextOf<typeof authModule>; // Evaluates to: { auth: { authenticate: (req, res, next) => void } }
Lifecycle
| Hook | Fires | Purpose |
|---|---|---|
| beforeBoot | once | Before the first module begins booting |
| afterBoot | once | After the last module has finished booting ā cross-module ctx is ready |
| beforeShutdown | once | Before the first module begins shutting down; receives an optional reason string |
| afterShutdown | once | After the last module has finished shutting down; receives an optional reason string |
| Global Per-Module ā fires once per module during .start() execution | ||
| beforeEachBoot | per module | Before each individual module boots; receives the module's meta object as the second argument |
| afterEachBoot | per module | After each individual module finishes booting; receives the module's meta object as the second argument |
| beforeEachShutdown | per module | Before each individual module shuts down; receives meta and an optional reason |
| afterEachShutdown | per module | After each individual module finishes shutting down; receives meta and an optional reason |
