TsdkArc LogoTsdkArc

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

TermDescription
SliceThe shape a module adds to the shared context ({ key: Type })
ModuleDeclares dependencies, registers values, and optionally tears them down
ContextThe merged union of all slices — fully typed at each module's boundary

Online Playground

Visit online playground (Better view on Desktop)

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

beforeBoot → init / boot → afterBoot → [running] → beforeShutdown → shutdown → afterShutdown
HookFiresPurpose
beforeBootonceBefore the first module begins booting
afterBootonceAfter the last module has finished booting — cross-module ctx is ready
beforeShutdownonceBefore the first module begins shutting down; receives an optional reason string
afterShutdownonceAfter the last module has finished shutting down; receives an optional reason string
Global Per-Module — fires once per module during .start() execution
beforeEachBootper moduleBefore each individual module boots; receives the module's meta object as the second argument
afterEachBootper moduleAfter each individual module finishes booting; receives the module's meta object as the second argument
beforeEachShutdownper moduleBefore each individual module shuts down; receives meta and an optional reason
afterEachShutdownper moduleAfter each individual module finishes shutting down; receives meta and an optional reason

Other Awesome Projects