npm package: @notjustcoders/ioc-arise
Description: Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
Author: Sid Ali Assoul
Latest version: 1.1.32

# 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.