@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
280 lines (216 loc) ⢠8.66 kB
Markdown
# 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 ``-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 ``-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 /ioc-arise
# Or use with npx
npx /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 `` 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.