UNPKG

@notjustcoders/ioc-arise

Version:

Arise type-safe IoC containers from your code. Zero overhead, zero coupling.

280 lines (216 loc) • 8.66 kB
# IoC Arise A command-line tool that automatically generates type-safe IoC (Inversion of Control) containers for TypeScript projects using the `ioc-arise` runtime library. It analyzes your classes, detects dependencies, and generates the necessary registration code. ## Table of Contents - [Features](#features) - [Installation](#installation) - [Usage](#usage) - [Configuration File](#configuration-file) - [Config File Location](#config-file-location) - [Priority Order](#priority-order) - [Examples](#examples) - [Basic Example](#basic-example) - [Instance Factories](#instance-factories) - [External & Monorepo Types](#external--monorepo-types) - [Usage in Your Code](#usage-in-your-code) - [Development](#development) - [Limitations](#limitations) - [Contributing](#contributing) ## Features - šŸ” **Automatic Detection**: Finds classes, factory functions, and value objects to register - 🧠 **Dependency Analysis**: Parses constructor and function dependencies from TypeScript code - šŸ›”ļø **Type Safety**: Uses the type-safe `ioc-arise` runtime library - 🚫 **No Decorators**: Pure static analysis, no runtime decorators required - āš ļø **Circular Dependency Detection**: Warns about dependency cycles - šŸ­ **Factory Functions**: Supports `@factory`-annotated functions with separate params or context object pattern - šŸ”Œ **Instance Factories**: Functions with an explicit interface return type automatically act as implementation providers for that interface - šŸ’Ž **Value Objects**: Supports `@value`-annotated constants registered with `useValue` - šŸ“¦ **Modular Containers**: Generates per-module files with `ContainerModule` for large projects - 🌐 **External Package Types**: Constructor and factory parameters typed with interfaces from external npm packages or sibling monorepo packages are correctly resolved using the TypeScript symbol name as the DI token ## Installation First, install the runtime library and the CLI: ```bash # Install runtime library npm install ioc-arise # Install CLI globally npm install -g @notjustcoders/ioc-arise # Or use with npx npx @notjustcoders/ioc-arise --help ``` ## Usage ```bash # Basic usage ioc-arise generate # With custom source and output ioc-arise generate --source src --output src/container.gen.ts ``` ## Configuration File You can create an `ioc.config.json` file in the same directory as your source code to set default options. CLI arguments will override config file settings. ```json { "source": "src", "output": "container.gen.ts", "interface": "I[A-Z].*", "exclude": [ "**/*.test.ts", "**/*.spec.ts", "**/node_modules/**" ], "checkCycles": false, "verbose": true } ``` ### Config File Location The config file should be placed in the same directory as your source code. For example, if your source directory is `src`, place `ioc.config.json` in the `src` directory. ### Priority Order 1. CLI arguments (highest priority) 2. Config file settings 3. Default values (lowest priority) ## Examples ### Basic Example Directory structure: ``` minimal-todo/ ā”œā”€ā”€ entities/Todo.ts ā”œā”€ā”€ repositories/ │ ā”œā”€ā”€ ITodoRepository.ts │ └── InMemoryTodoRepository.ts ā”œā”€ā”€ services/ │ ā”œā”€ā”€ ITodoService.ts │ └── TodoService.ts ā”œā”€ā”€ ioc.config.json └── container.gen.ts (generated) ``` Generated container (`container.gen.ts`): ```typescript /** * This file is auto-generated by ioc-arise. * Do not modify this file manually. */ import { Container, Lifecycle } from '@notjustcoders/di-container'; import { InMemoryTodoRepository } from './repositories/InMemoryTodoRepository'; import { TodoService } from './services/TodoService'; export const container = new Container(); container.register(InMemoryTodoRepository, { useClass: InMemoryTodoRepository, lifecycle: Lifecycle.Singleton, }); container.register(TodoService, { useClass: TodoService, dependencies: [InMemoryTodoRepository], lifecycle: Lifecycle.Transient, }); ``` ### Instance Factories An **instance factory** is any exported function whose explicit return type annotation resolves to a registered interface. No `@factory` annotation is needed — the return type is the signal. The function is registered under the interface name as the token (just like a class that `implements` the interface). **Separate params:** ```typescript // repositories/createUserRepository.ts import { IAppConfig } from '../config/IAppConfig'; import { ILogger } from '../logger/ILogger'; import { IUserRepository } from './IUserRepository'; export function createUserRepository( config: IAppConfig, logger: ILogger, ): IUserRepository { if (config.getStorageType() === 'persistent') { logger.info('Using persistent storage'); return new PersistentUserRepository(config.getDbPath()); } logger.info('Using in-memory storage'); return new InMemoryUserRepository(); } ``` Generated registration: ```typescript container.register('IUserRepository', { useFactory: createUserRepository, dependencies: ['IAppConfig', 'ILogger'], lifecycle: Lifecycle.Singleton, }); ``` **Context object:** ```typescript // repositories/createUserRepository.ts export function createUserRepository( context: { config: IAppConfig; logger: ILogger }, ): IUserRepository { const { config, logger } = context; if (config.getStorageType() === 'persistent') { logger.info('Using persistent storage'); return new PersistentUserRepository(config.getDbPath()); } logger.info('Using in-memory storage'); return new InMemoryUserRepository(); } ``` Generated registration: ```typescript container.register('IUserRepository', { useFactory: (config, logger) => createUserRepository({ config, logger }), dependencies: ['IAppConfig', 'ILogger'], lifecycle: Lifecycle.Singleton, }); ``` > **Uniqueness rule**: an interface can have at most one implementation provider — either a class that `implements` it, or an instance factory that returns it. Registering both throws an error at generation time. ### External & Monorepo Types Dependencies typed with interfaces from external npm packages or sibling monorepo packages are fully supported. The TypeScript symbol name is used as the DI token — the same convention as for local interfaces. **Shape 1 — Direct external import:** ```typescript // services/OrderService.ts import type { IPaymentGateway } from '@payments/sdk'; import type { IOrderService } from './IOrderService'; export class OrderService implements IOrderService { constructor(private gateway: IPaymentGateway) {} } ``` Generated registration: ```typescript container.register('IOrderService', { useClass: OrderService, dependencies: ['IPaymentGateway'], lifecycle: Lifecycle.Singleton, }); ``` **Shape 2 — Local re-export stub (common monorepo pattern):** ```typescript // src/ports/ITombstoneRepository.ts — re-export stub export type { ITombstoneRepository } from '@word-tracker/sync'; // src/PermanentlyDeleteWordUseCase.ts — factory using it import type { IWordEntryRepository } from './IWordEntryRepository'; // local import type { ITombstoneRepository } from './ports/ITombstoneRepository'; // re-export stub /** @factory */ export function createPermanentlyDeleteWordUseCase(deps: { wordEntryRepository: IWordEntryRepository; tombstoneRepository: ITombstoneRepository; }) { ... } ``` Generated registration: ```typescript container.register('createPermanentlyDeleteWordUseCase', { useFactory: (wordEntryRepository, tombstoneRepository) => createPermanentlyDeleteWordUseCase({ wordEntryRepository, tombstoneRepository }), dependencies: ['IWordEntryRepository', 'ITombstoneRepository'], }); ``` The token for the external type (`ITombstoneRepository`) is the TypeScript symbol name. You are responsible for registering a concrete implementation under that token elsewhere (e.g., in another module). ## Usage in Your Code ```typescript import { container } from './container.gen'; import { TodoService } from './services/TodoService'; // Resolve dependencies const todoService = container.resolve(TodoService); const todos = await todoService.getAllTodos(); ``` ## Development ```bash # Run in development mode pnpm run dev # Build for production pnpm run build ``` ## Limitations - Constructor parameters typed with non-interface types (e.g., concrete classes from external packages) are not auto-resolved - Circular dependencies are detected and warned about - Only analyzes TypeScript files (`.ts` extension) ## Contributing Contributions are welcome! Please feel free to submit issues and pull requests.