npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

mini-di-cocos

v1.0.0

Published

Lightweight DI container for Cocos Creator 3.x - inspired by VContainer and Zenject, optimized for Playable Ads

Readme

MiniDI

Lightweight Dependency Injection container for Cocos Creator 3.x, inspired by VContainer and Zenject.

Optimized for Playable Ads with minimal bundle size impact.

Features

  • 🚀 No reflect-metadata — uses static properties, works with any Cocos build
  • 🎮 Cocos Creator native — seamless integration with Components and Prefabs
  • 📦 Minimal footprint — optimized for playable ads (2-15MB limits)
  • 🔄 Lifecycle hooks — IInitializable, IStartable, ITickable, IDisposable
  • 🏭 VContainer-style instantiatecontainer.instantiate(prefab) with auto-injection
  • 🔍 Zenject-style scene injection — automatic injection across entire scene

Installation

npm install mini-di-cocos --legacy-peer-deps

Or add to your .npmrc:

legacy-peer-deps=true

Quick Start

1. Create Services

import { Injectable, IInitializable, IDisposable } from 'mini-di-cocos';

// Simple service
@Injectable([])
class ConfigService {
    readonly maxScore = 1000;
    readonly soundEnabled = true;
}

// Service with dependencies
@Injectable([ConfigService])
class AudioService implements IInitializable, IDisposable {
    constructor(private config: ConfigService) {}

    initialize(): void {
        console.log('AudioService initialized');
    }

    playSound(name: string): void {
        if (this.config.soundEnabled) {
            // play sound
        }
    }

    dispose(): void {
        console.log('AudioService disposed');
    }
}

@Injectable([AudioService, ConfigService])
class GameService {
    constructor(
        private audio: AudioService,
        private config: ConfigService
    ) {}

    onScore(): void {
        this.audio.playSound('score');
    }
}

2. Create LifetimeScope

import { _decorator } from 'cc';
import { LifetimeScope, ContainerBuilder } from 'mini-di-cocos';

const { ccclass } = _decorator;

@ccclass('GameScope')
export class GameScope extends LifetimeScope {
    protected configure(builder: ContainerBuilder): void {
        builder.register(ConfigService);
        builder.register(AudioService);
        builder.register(GameService);
    }
}

3. Inject into Cocos Components

import { _decorator, Component } from 'cc';
import { NeedInject, InjectProperty } from 'mini-di-cocos';

@NeedInject()
@ccclass('PlayerController')
export class PlayerController extends Component {
    @InjectProperty(GameService)
    private gameService!: GameService;

    @InjectProperty(AudioService)
    private audio!: AudioService;

    onCollectCoin(): void {
        this.gameService.onScore();
        this.audio.playSound('coin');
    }
}

4. Scene Setup

Scene Hierarchy:
├── GameScope (LifetimeScope)    ← Add your scope here
├── Player
│   └── PlayerController         ← @NeedInject components
├── UI
│   └── ScoreView                ← @NeedInject components
└── ...

API Reference

Decorators

@Injectable(dependencies: Token[])

Marks a class for constructor injection.

@Injectable([DepA, DepB])
class MyService {
    constructor(private depA: DepA, private depB: DepB) {}
}

@NeedInject()

Marks a Cocos Component for property injection.

@NeedInject()
@ccclass('MyComponent')
class MyComponent extends Component {}

@InjectProperty(token: Token)

Injects a dependency into a property.

@InjectProperty(AudioService)
private audio!: AudioService;

Container

// Registration
container.register(Token, options?)
container.registerSingleton(Token, useClass?)
container.registerTransient(Token, useClass?)
container.registerInstance(Token, instance)
container.registerFactory(Token, factory, lifetime?)

// Resolution
container.resolve<T>(Token): T
container.tryResolve<T>(Token): T | null
container.isRegistered(Token): boolean

// Cocos Integration
container.injectComponent(component)
container.injectNode(node)
container.instantiate(prefab, parent?): Node
container.instantiateAndGet<T>(prefab, type, parent?): T

// Lifecycle
container.initializeAll()
container.startAll()
container.disposeAll()
container.getAllSingletons(): any[]

ContainerBuilder

const builder = new ContainerBuilder();

builder
    .register(ServiceA)
    .register(ServiceB, { useClass: ServiceBImpl })
    .registerSingleton(ServiceC)
    .registerTransient(ServiceD)
    .registerInstance(Config, configInstance)
    .registerFactory(ServiceE, (c) => new ServiceE(c.resolve(ServiceA)));

const container = builder.build();

LifetimeScope

| Property | Default | Description | |----------|---------|-------------| | autoInjectScene | true | Auto-inject all @NeedInject components in scene | | autoInitialize | true | Auto-call IInitializable.initialize() | | autoStart | true | Auto-call IStartable.start() | | enableTickManager | false | Enable ITickable support |

@ccclass('GameScope')
class GameScope extends LifetimeScope {
    protected configure(builder: ContainerBuilder): void {
        // Register your services here
    }
}

// Static access
const scope = LifetimeScope.findInScene();
const service = scope?.resolve(MyService);

InjectionToken

For interface-based injection (TypeScript interfaces are erased at runtime):

interface IAudioService {
    playSound(name: string): void;
}

const IAudioService = new InjectionToken<IAudioService>('IAudioService');

// Registration
builder.register(IAudioService, {
    useFactory: (c) => new AudioServiceImpl(c.resolve(ConfigService))
});

// Injection
@InjectProperty(IAudioService)
private audio!: IAudioService;

Lifecycle Interfaces

interface IInitializable {
    initialize(): void | Promise<void>;
}

interface IStartable {
    start(): void;
}

interface ITickable {
    tick(deltaTime: number): void;
}

interface IFixedTickable {
    fixedTick(fixedDeltaTime: number): void;
}

interface ILateTickable {
    lateTick(deltaTime: number): void;
}

interface IDisposable {
    dispose(): void;
}

Execution Order:

onLoad()
  └── Container.build()
  └── IInitializable.initialize() (registration order)
  └── Scene injection

start()
  └── IStartable.start() (registration order)

update(dt)
  └── ITickable.tick(dt)
  └── IFixedTickable.fixedTick(fixedDt) (fixed timestep)

lateUpdate(dt)
  └── ILateTickable.lateTick(dt)

onDestroy()
  └── IDisposable.dispose() (reverse order)

Dynamic Instantiation

@Injectable([Container])
class EnemySpawner {
    @property(Prefab)
    private enemyPrefab!: Prefab;

    constructor(private container: Container) {}

    spawn(parent: Node): Enemy {
        // Instantiate with auto-injection
        return this.container.instantiateAndGet(
            this.enemyPrefab,
            Enemy,
            parent
        );
    }
}

Lifetime

| Type | Description | |------|-------------| | Lifetime.Singleton | One instance per container (default) | | Lifetime.Transient | New instance on each resolve |

Comparison

| Feature | MiniDI | VContainer | Zenject | |---------|--------|------------|---------| | Platform | Cocos Creator | Unity | Unity | | reflect-metadata | ❌ Not required | ✅ Required | ✅ Required | | Bundle size | ~5KB | ~50KB | ~200KB | | Constructor injection | ✅ | ✅ | ✅ | | Property injection | ✅ | ✅ | ✅ | | Scene injection | ✅ | ❌ | ✅ | | Prefab instantiate | ✅ | ✅ | ✅ | | Lifecycle hooks | ✅ | ✅ | ✅ | | Scoped containers | ❌ | ✅ | ✅ |

Best Practices

1. Service vs Component

// ✅ Services — constructor injection
@Injectable([ConfigService])
class AudioService {
    constructor(private config: ConfigService) {}
}

// ✅ Cocos Components — property injection
@NeedInject()
@ccclass('PlayerController')
class PlayerController extends Component {
    @InjectProperty(AudioService)
    private audio!: AudioService;
}

2. Interface Segregation

// Define interfaces
interface IAudioService {
    playSound(name: string): void;
}

// Create tokens
const IAudioService = new InjectionToken<IAudioService>('IAudioService');

// Register implementation
builder.register(IAudioService, { useClass: AudioServiceImpl });

3. Factory Registration

builder.registerFactory(ComplexService, (container) => {
    const config = container.resolve(ConfigService);
    const audio = container.resolve(AudioService);
    
    return new ComplexService(config, audio, {
        debug: true,
        maxRetries: 3
    });
});

Troubleshooting

"X is not registered"

Ensure the dependency is registered before resolving:

protected configure(builder: ContainerBuilder): void {
    builder.register(DependencyFirst);  // Register dependencies first
    builder.register(ServiceThatNeedsIt);
}

"Circular dependency detected"

Break the cycle with factory or lazy resolution:

@Injectable([])
class ServiceA {
    private _serviceB: ServiceB | null = null;
    
    setServiceB(b: ServiceB): void {
        this._serviceB = b;
    }
}

// In configure:
builder.registerFactory(ServiceA, (c) => {
    const a = new ServiceA();
    const b = c.resolve(ServiceB);
    a.setServiceB(b);
    return a;
});

Component not injected

  1. Check @NeedInject() decorator is present
  2. Check component is in scene when LifetimeScope loads
  3. For dynamic objects, use container.instantiate()

License

MIT