Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
# NexusDI

<div align="center">
<img src="logo.svg" alt="NexusDI Logo" width="120" height="120" />
<img src="https://nexus.js.org/img/logo.svg" alt="NexusDI Logo" width="120" height="120" />
<br />
<p><strong>A modern, lightweight dependency injection container for TypeScript with decorator support, inspired by industry-leading frameworks.</strong></p>
<p><strong>A modern, lightweight dependency injection container for TypeScript with native decorators, inspired by industry-leading frameworks.</strong></p>
<p><em>The DI library that doesn't make you want to inject yourself with coffee ☕</em></p>
</div>

<div align="center">

[![npm version](https://badge.fury.io/js/%40nexusdi%2Fcore.svg)](https://badge.fury.io/js/%40nexusdi%2Fcore)
[![npm version](https://img.shields.io/npm/v/@nexusdi/core.svg)](https://www.npmjs.com/package/@nexusdi/core)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nexusdi/core/ci.yml)
![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/nexusdi/core)

![npm bundle size (scoped)](https://img.shields.io/bundlephobia/min/%40nexusdi/core)
![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40nexusdi%2Fcore)
![Source language](https://img.shields.io/badge/language-TypeScript-blue)

[![npm downloads](https://img.shields.io/npm/dm/@nexusdi/core.svg)](https://www.npmjs.com/package/@nexusdi/core)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Bundle Size](https://img.shields.io/bundlephobia/min/@nexusdi/core)](https://bundlephobia.com/package/@nexusdi/core)
![GitHub License](https://img.shields.io/github/license/NexusDI/core)
[![GitHub stars](https://img.shields.io/github/stars/NexusDI/core.svg?style=social&label=Star)](https://github.com/NexusDI/core)

</div>
Expand Down Expand Up @@ -88,7 +93,7 @@ interface DatabaseConfig {
const DATABASE_CONFIG = Symbol('DATABASE_CONFIG');

@Module({
services: [DatabaseService], // Simplified format - uses @Service decorator token
providers: [DatabaseService], // Simplified format - uses @Service decorator token
})
class DatabaseModule extends DynamicModule<DatabaseConfig> {
protected readonly configToken = DATABASE_CONFIG;
Expand Down
127 changes: 127 additions & 0 deletions docs/blog/2025-06-26-native-decorators-simpler-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: 'The jump to lightspeed'
authors: [evanion]
tags: [release, decorators, typescript, modules, breaking-changes]
description: "NexusDI now uses native decorator metadata, drops reflect-metadata, and unifies module configuration. Here's what's new, why it matters, and how to upgrade."
---

# 🚀 Native Decorators, Simpler Modules, and a Future-Proof NexusDI

NexusDI just made the jump to lightspeed! With our latest release, you can say goodbye to `reflect-metadata`, enjoy a cleaner module API, and rest easy knowing your DI setup is ready for the next era of TypeScript and JavaScript. Let's take a tour of what's new, why it matters, and how to upgrade.

<!--truncate-->

## ⚡ Native Decorator Metadata (No More reflect-metadata!)

NexusDI now uses the new [ECMAScript decorator metadata standard](https://github.com/tc39/proposal-decorator-metadata) via `Symbol.metadata`, as supported in TypeScript 5.2+ and the upcoming JavaScript standard.

- **No more `reflect-metadata`**: You don't need to install or import it—ever (unless your other dependencies require it).
- **Cleaner tsconfig**: No more `emitDecoratorMetadata`. Just enable `experimentalDecorators` and (optionally) `useDefineForClassFields` (the default in TS 5.2+).
- **Automatic polyfill**: NexusDI includes a tiny, built-in polyfill for `Symbol.metadata` if your runtime doesn't support it yet. No extra setup required.
- **Future-proof**: Your code is ready for the next generation of TypeScript and JavaScript.

> "It's like switching from a hyperdrive that needs constant repairs to one that just works."

### Example

```typescript
import { Service, Inject, Nexus, Token } from '@nexusdi/core';

const USER_SERVICE = new Token('UserService');

@Service()
class UserService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}

const container = new Nexus();
container.set(USER_SERVICE, UserService);

const userService = container.get(USER_SERVICE);
console.log(userService.getUsers()); // ['Alice', 'Bob', 'Charlie']
```

> **Note:** NexusDI v0.3+ uses native decorator metadata (TypeScript 5.2+). You do not need to install or import `reflect-metadata`, and you do not need `emitDecoratorMetadata` in your tsconfig. Only `experimentalDecorators` and `useDefineForClassFields` (default in TypeScript 5.2+) are required.

---

## 📦 Unified Module API: `services` and `providers` Merged

Modules are now simpler and less confusing! Instead of juggling both `services` and `providers` arrays, you now use a single `providers` array for everything:

**Before:**

```typescript
@Module({
services: [UserService],
providers: [{ token: USER_REPO, useClass: UserRepo }],
})
export class UserModule {}
```

**Now:**

```typescript
@Module({
providers: [UserService, { token: USER_REPO, useClass: UserRepo }],
})
export class UserModule {}
```

- **Less boilerplate**: One array, all your dependencies.
- **Clearer intent**: No more guessing where things go.

---

## 🎯 Why This Matters

- **Smaller bundles**: No more `reflect-metadata` means less code in your app.
- **Faster startup**: Native metadata is faster and more reliable.
- **Modern and future-proof**: Aligns with the latest TypeScript and ECMAScript standards.
- **Easier migration**: The new module API is simpler and more intuitive.

---

## 🔧 How to Upgrade

1. **Update TypeScript**: Make sure you're on TypeScript 5.2 or later.
2. **Update your tsconfig**:
- Cleanup `emitDecoratorMetadata`, if you don't need it for anything else
- Ensure `experimentalDecorators` is enabled
- (Optional) `useDefineForClassFields` should be enabled (default in TS 5.2+)
3. **Remove all imports of `reflect-metadata`**.
4. **Update your modules**: Merge `services` and `providers` into a single `providers` array.
5. **Enjoy a cleaner, faster, and more modern DI experience!**

---

## 🧑‍💻 Other Notable Improvements

- **Dynamic Modules**: More flexible module registration and configuration. (was ghost launched in 0.2)
- **Better Type Safety**: Improved generics and overloads for decorators and containers.
- **Performance**: Optimized for tree-shaking and minimal runtime overhead.
- **Polyfill Included**: No manual setup for `Symbol.metadata`—it just works.

---

## 🛣️ Next Steps

- [Getting Started Guide](../docs/getting-started)
- [Advanced Providers & Factories](../docs/advanced/advanced-providers-and-factories)
- [Best Practices](../docs/best-practices)
- [Roadmap](../docs/roadmap)

---

## ✨ Lets boldly go where few have gone before!

This release is a big step toward a simpler, more powerful, and future-ready dependency injection experience for TypeScript and JavaScript developers. We can't wait to see what you build with NexusDI!

Have questions, feedback, or want to contribute? [Check out our docs](https://nexus.js.org/), [join the discussion](https://github.com/NexusDI/core/discussions), or [open an issue](https://github.com/NexusDI/core/issues).

---

_Want to help improve NexusDI?_
Read our [contribution guidelines](/docs/contributing) and join the journey! 🚀
20 changes: 20 additions & 0 deletions docs/blog/tags.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ dependency-injection:
permalink: /dependency-injection
description: Articles about DI patterns and libraries

decorators:
label: Decorators
permalink: /decorators
description: Articles about decorators

typescript:
label: TypeScript
permalink: /typescript
description: Articles about TypeScript

modules:
label: Modules
permalink: /modules
description: Articles about modules

breaking-changes:
label: Breaking Changes
permalink: /breaking-changes
description: Articles about breaking changes

v0.1.0:
label: v0.1.0
permalink: /v0.1.0
Expand Down
8 changes: 4 additions & 4 deletions docs/docs/advanced/circular-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Learn how to identify, prevent, and resolve circular dependencies in NexusDI. Li

## What Are Circular Dependencies?

A circular dependency occurs when two or more services depend on each other, either directly or indirectly. This creates a dependency cycle that the container cannot resolve.
A circular dependency occurs when two or more providers depend on each other, either directly or indirectly. This creates a dependency cycle that the container cannot resolve.

```typescript
// ❌ Direct circular dependency
Expand Down Expand Up @@ -184,7 +184,7 @@ class EmailService {

## Prevention Strategies

- **Single Responsibility Principle**: Each service should have one reason to change
- **Single Responsibility Principle**: Each provider should have one reason to change
- **Dependency Inversion**: Depend on abstractions, not concretions
- **Interface Segregation**: Use small, focused interfaces
- **Event-Driven Architecture**: Use events for communication
Expand All @@ -209,7 +209,7 @@ describe('Circular Dependency Detection', () => {
}).toThrow('Circular dependency detected');
});

it('should resolve services without circular dependencies', () => {
it('should resolve providers without circular dependencies', () => {
container.set(USER_SERVICE, { useClass: UserService });
container.set(EMAIL_SERVICE, { useClass: EmailService });

Expand All @@ -226,4 +226,4 @@ describe('Circular Dependency Detection', () => {
- **[Performance Tuning](performance-tuning.md)** - Optimize container performance
- **[Testing](../testing.md)** - Test your dependency injection setup

Remember: Design your services with clear boundaries and loose coupling to avoid circular dependency traps! 🔄✨
Remember: Design your providers with clear boundaries and loose coupling to avoid circular dependency traps! 🔄✨
51 changes: 31 additions & 20 deletions docs/docs/advanced/module-inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Module inheritance lets you build on existing modules by subclassing them, but b

## Why Inherit Modules? 🤖

Module inheritance lets you extend existing modules with new services or configuration, making it easy to reuse and adapt functionality for different scenarios. It's like giving your modules an upgrade!
Module inheritance lets you extend existing modules with new providers or configuration, making it easy to reuse and adapt functionality for different scenarios. It's like giving your modules an upgrade!

## Basic Example: Parent and Subclass

Expand All @@ -34,25 +34,34 @@ class AuditingModule extends LoggingModule {}

## Pitfall: Metadata Inheritance

By default, TypeScript's `Reflect.getMetadata` will return metadata from the parent if the subclass isn't decorated. This can lead to subtle bugs:
By default, both TypeScript's `Reflect.getMetadata` and native `Symbol.metadata` (as accessed via NexusDI's `getMetadata`) will return metadata from the parent if the subclass isn't decorated. This can lead to subtle bugs:

```typescript
const parentMeta = Reflect.getMetadata('nexusdi:module', LoggingModule); // { providers: [LoggerService] }
const childMeta = Reflect.getMetadata('nexusdi:module', ExtendedLoggingModule); // { providers: [LoggerService] } (inherited!)
import { getMetadata, METADATA_KEYS } from '@nexusdi/core';

const parentMeta = getMetadata(LoggingModule, METADATA_KEYS.MODULE_METADATA); // { providers: [LoggerService] }
const childMeta = getMetadata(
ExtendedLoggingModule,
METADATA_KEYS.MODULE_METADATA
); // { providers: [LoggerService] } (inherited!)
```

This means **subclasses without `@Module` will appear to have the parent's metadata**, but NexusDI expects every module to be explicitly decorated. The real risk is that your subclass will only include the parent's providers and configuration — any new configuration added in the child will be ignored unless you decorate the subclass with `@Module`. To add new services or providers to a child module, you must decorate it as a module.
> **Note:** This inheritance behavior applies to both legacy `Reflect.getMetadata` and modern native `Symbol.metadata` (as used by NexusDI's `getMetadata`).

This means **subclasses without `@Module` will appear to have the parent's metadata**, but NexusDI expects every module to be explicitly decorated. The real risk is that your subclass will only include the parent's providers and configuration — any new configuration added in the child will be ignored unless you decorate the subclass with `@Module`. To add new providers or configuration to a child module, you must decorate it as a module.

## Best Practice: Always Decorate Subclasses

If you want a subclass to be a module, always decorate it:

```typescript
@Service()
class AuditService {}
class AuditService {
constructor(private readonly logger: LoggerService) {}
}

@Module({
providers: [],
providers: [AuditService],
})
class AuditingModule extends LoggingModule {}
```
Expand All @@ -62,29 +71,27 @@ class AuditingModule extends LoggingModule {}
Here's how you can test correct and incorrect inheritance:

```typescript
import { Module, Service } from '@nexusdi/core';
import { METADATA_KEYS } from '@nexusdi/core/src/types';
import { Module, Service, METADATA_KEYS, getMetadata } from '@nexusdi/core';

describe('Module inheritance', () => {
@Service()
class LoggerService {}

@Module({ providers: [LoggerService] })
class LoggingModule {}

class ExtendedLoggingModule extends LoggingModule {}

it('should not have metadata on subclass if not decorated', () => {
const metadata = Reflect.getMetadata(
METADATA_KEYS.MODULE_METADATA,
ExtendedLoggingModule
const metadata = getMetadata(
ExtendedLoggingModule,
METADATA_KEYS.MODULE_METADATA
);
expect(metadata).toBeUndefined();
});

it('should have metadata on parent if decorated', () => {
const metadata = Reflect.getMetadata(
METADATA_KEYS.MODULE_METADATA,
LoggingModule
);
const metadata = getMetadata(LoggingModule, METADATA_KEYS.MODULE_METADATA);
expect(metadata).toBeDefined();
});
});
Expand All @@ -94,15 +101,19 @@ describe('Module inheritance', () => {

```typescript
@Module({
services: [UserService],
providers: [{ token: USER_CONFIG, useValue: { feature: 'basic' } }],
providers: [
{ token: USER_SERVICE, useClass: UserService },
{ token: USER_CONFIG, useValue: { feature: 'basic' } },
],
})
class UserModule {}

// Add premium features by extending and redecorating
@Module({
services: [PremiumUserService],
providers: [{ token: USER_CONFIG, useValue: { feature: 'premium' } }],
providers: [
{ token: USER_SERVICE, useClass: PremiumUserService },
{ token: USER_CONFIG, useValue: { feature: 'premium' } },
],
})
class PremiumUserModule extends UserModule {}
```
Expand Down
3 changes: 1 addition & 2 deletions docs/docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ Keep your code organized and modular:

```typescript
@Module({
services: [UserService, UserRepository, UserValidator],
providers: [{ token: DATABASE, useClass: PostgresDatabase }],
providers: [UserService, UserRepository, UserValidator],
})
class UserModule {}
```
Expand Down
Loading
Loading