
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
@mmstack/di
Advanced tools
A collection of dependency injection utilities for Angular that simplify working with InjectionTokens and provide type-safe patterns for creating injectable services.
A collection of dependency injection utilities for Angular that simplify working with InjectionTokens and provide type-safe patterns for creating injectable services.
npm install @mmstack/di
This library provides the following utilities:
injectable - Creates a typed InjectionToken with inject and provide helper functions for type-safe dependency injection.injectLazy - Defers the resolution and instantiation of a token until it is actually accessed.createRunInInjectionContext - Captures an injection context securely and returns a runner function, useful for inject() inside async callbacks.rootInjectable - Creates a lazily-initialized root-level injectable that maintains a singleton instance.createScope - Creates a dependency injection scope that caches singletons based on the factory function.Creates a typed InjectionToken with convenient, type-safe inject and provider functions, eliminating boilerplate and ensuring type safety throughout your dependency injection flow. It returns a tuple of [injectFn, provideFn] that work together seamlessly.
The injectable function supports three patterns:
T | null when not providedimport { Component, Injectable } from '@angular/core';
import { injectable } from '@mmstack/di';
// Create a typed injectable
const [injectLogger, provideLogger] = injectable<Logger>('Logger');
// Provide the value in a component or module
@Component({
selector: 'app-root',
providers: [
provideLogger({
log: (msg) => console.log(`[LOG]: ${msg}`),
error: (msg) => console.error(`[ERROR]: ${msg}`),
}),
],
})
export class AppComponent {}
// Inject it anywhere in the component tree
@Injectable()
export class DataService {
private logger = injectLogger(); // Logger | null
fetchData() {
this.logger?.log('Fetching data...');
}
}
import { HttpClient } from '@angular/common/http';
import { injectable } from '@mmstack/di';
interface ApiConfig {
baseUrl: string;
timeout: number;
}
const [injectApiConfig, provideApiConfig] = injectable<ApiConfig>('ApiConfig');
// Provide using a factory with dependencies
@Component({
providers: [
provideApiConfig(
(http: HttpClient) => ({
baseUrl: 'https://api.example.com',
timeout: 5000,
}),
[HttpClient], // Dependencies array
),
],
})
export class AppComponent {}
When you want to provide a default value instead of returning null:
import { injectable } from '@mmstack/di';
interface Theme {
primary: string;
secondary: string;
}
const [injectTheme, provideTheme] = injectable<Theme>('Theme', {
fallback: {
primary: '#007bff',
secondary: '#6c757d',
},
});
// or if you need inject/lazy evaluation
const [injectTheme, provideTheme] = injectable<Theme>('Theme', {
lazyFallback: () => {
return {
primary: inject(APP_PRIMARY),
secondary: '#6c757d',
},
}
});
@Injectable()
export class ThemeService {
// Always returns a Theme, never null
private theme = injectTheme();
getPrimaryColor() {
return this.theme.primary; // Safe to access
}
}
When you want to enforce that the value must be provided:
import { injectable } from '@mmstack/di';
const [injectApiKey, provideApiKey] = injectable<string>('ApiKey', {
errorMessage: 'API Key is required! Please provide it using provideApiKey().',
});
@Injectable()
export class ApiService {
// Throws error if not provided
private apiKey = injectApiKey();
makeRequest() {
// apiKey is guaranteed to exist here
return fetch(`https://api.example.com?key=${this.apiKey}`);
}
}
The provideFn correctly handles functions as values (not factories):
import { injectable } from '@mmstack/di';
type Validator = (value: string) => boolean;
const [injectValidator, provideValidator] = injectable<Validator>('Validator');
@Component({
providers: [
// Providing a function as a value (not a factory)
provideValidator((value: string) => value.length > 5),
],
})
export class FormComponent {
private validator = injectValidator();
validate(input: string) {
return this.validator?.(input) ?? false;
}
}
Create context-like dependency injection patterns:
import { Component, Injectable } from '@angular/core';
import { injectable } from '@mmstack/di';
interface FormContext {
formId: string;
isDirty: boolean;
submit: () => void;
}
const [injectFormContext, provideFormContext] = injectable<FormContext>('FormContext', {
errorMessage: 'FormContext must be provided by a parent form component',
});
@Component({
selector: 'app-form',
providers: [
provideFormContext({
formId: 'user-form',
isDirty: false,
submit: () => console.log('Submitting form...'),
}),
],
template: `
<form>
<app-form-field></app-form-field>
<app-form-actions></app-form-actions>
</form>
`,
})
export class FormComponent {}
@Component({
selector: 'app-form-field',
template: `<input [id]="formContext.formId + '-input'" />`,
})
export class FormFieldComponent {
// Automatically gets the context from parent
formContext = injectFormContext();
}
@Component({
selector: 'app-form-actions',
template: `<button (click)="formContext.submit()">Submit</button>`,
})
export class FormActionsComponent {
formContext = injectFormContext();
}
Defers the resolution and instantiation of an injection token until the returned getter function is actually called.
Angular's native inject() resolves and instantiates dependencies immediately during the construction phase. If a service is heavily resource-intensive but only needed conditionally (like an export service or a complex editor), injectLazy allows you to capture the injection context immediately while delaying instantiation. The resolved value is cached, acting as a standard scoped singleton on subsequent calls.
import { Component, HostListener } from '@angular/core';
import { injectLazy } from '@mmstack/di';
@Component({
selector: 'app-export-button',
template: `<button>Export Data</button>`,
})
export class ExportButtonComponent {
// Captures the Injector but does NOT instantiate HeavyExportService yet
private getExportService = injectLazy(HeavyExportService);
@HostListener('click')
export() {
// HeavyExportService is instantiated on the first click, then cached
const service = this.getExportService();
service.doExport();
}
}
It fully supports Angular's InjectOptions and guarantees correct return types (e.g., returning T | null when optional: true):
const getOptionalDep = injectLazy(MyToken, { optional: true });
// Later...
const dep = getOptionalDep(); // MyToken | null
Captures an injection context securely and returns a runner function.
This utility allows you to execute callbacks inside the captured context at a later time. It solves the common pain point of needing to use inject() inside asynchronous callbacks, RxJS streams, or external event listeners where the framework's implicit injection context has been lost.
import { Component, OnInit, inject } from '@angular/core';
import { createRunInInjectionContext } from '@mmstack/di';
@Component({
selector: 'app-dialog-trigger',
template: `<button>Open Dialog</button>`,
})
export class DialogTriggerComponent implements OnInit {
// Grabs the current injector during construction
private runInContext = createRunInInjectionContext();
ngOnInit() {
// someExternalLibrary is out of Angular's zone/context
someExternalLibrary.on('openEvent', () => {
this.runInContext(() => {
// We can safely use `inject()` here even though we are inside an async callback!
const dialog = inject(DialogService);
dialog.open();
});
});
}
}
You can also completely bypass the ambient capture and provide an Injector explicitly:
// Outside of normal injection context
const runner = createRunInInjectionContext(appRef.injector);
runner(() => {
const router = inject(Router);
router.navigate(['/home']);
});
Creates a lazily-initialized root-level injectable that maintains a singleton instance across your entire application. The factory function runs in the root injection context on first access, allowing you to inject other dependencies.
Important: This should only be used for pure singletons. If you need scoped instances, use regular @Injectable services with providedIn or component-level providers.
import { Injectable } from '@angular/core';
import { rootInjectable } from '@mmstack/di';
interface Logger {
log: (message: string) => void;
}
// Create a root-level injectable
const injectLogger = rootInjectable<Logger>(() => ({
log: (message) => console.log(`[${new Date().toISOString()}] ${message}`),
}));
@Injectable()
export class DataService {
private logger = injectLogger();
fetchData() {
this.logger.log('Fetching data...');
}
}
@Injectable()
export class UserService {
private logger = injectLogger(); // Same instance as above
saveUser() {
this.logger.log('Saving user...');
}
}
The factory function receives the root injector, allowing you to inject other services:
import { HttpClient } from '@angular/common/http';
import { rootInjectable } from '@mmstack/di';
interface ApiClient {
get: (url: string) => Promise<any>;
post: (url: string, data: any) => Promise<any>;
}
const injectApiClient = rootInjectable<ApiClient>((injector) => {
const http = injector.get(HttpClient); // or just inject(HttpClient)
return {
get: (url) => fetch(url).then((r) => r.json()),
post: (url, data) =>
fetch(url, {
method: 'POST',
body: JSON.stringify(data),
}).then((r) => r.json()),
};
});
@Injectable()
export class ProductService {
private api = injectApiClient(); // Singleton instance
loadProducts() {
return this.api.get('/api/products');
}
}
Create a simple global state manager:
import { signal, computed } from '@angular/core';
import { rootInjectable } from '@mmstack/di';
interface User {
id: string;
name: string;
email: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
}
const injectAuthStore = rootInjectable(() => {
const user = signal<User | null>(null);
const isAuthenticated = computed(() => user() !== null);
return {
user: user.asReadonly(),
isAuthenticated,
login: (userData: User) => user.set(userData),
logout: () => user.set(null),
};
});
@Injectable()
export class AuthService {
private store = injectAuthStore(); // Singleton across app
login(email: string, password: string) {
// Perform authentication...
this.store.login({
id: '123',
name: 'John Doe',
email,
});
}
logout() {
this.store.logout();
}
}
@Component({
selector: 'app-navbar',
template: `
@if (authStore.isAuthenticated()) {
<span>{{ authStore.user()?.name }}</span>
<button (click)="logout()">Logout</button>
}
`,
})
export class NavbarComponent {
authStore = injectAuthStore(); // Same instance
logout() {
this.authStore.logout();
}
}
Creates a dependency injection scope using a dynamic InjectionToken representing a caching registry. Factories executed within the scope run in the Angular injection context and their results are cached, effectively creating scoped singletons, that are destroyed when the scoped provider is. It returns a tuple of [injectable, provider].
import { Component, inject } from '@angular/core';
import { createScope } from '@mmstack/di';
// Create the scope
const [injectableFeatureItem, provideFeatureScope] = createScope('FeatureScope');
// Provide the scope at a specific component level boundary
@Component({
selector: 'app-feature',
providers: [provideFeatureScope()],
template: `<app-child></app-child>`,
})
export class FeatureComponent {}
// Use the scope to register an item factory
// The factory will run in the injection context so you can use inject()
const useFeatureItem = injectableFeatureItem(() => {
const someDep = inject(SomeDependency);
return {
id: Math.random(),
doWork: () => someDep.work(),
};
});
@Component({
selector: 'app-child',
template: `<div>Child Item ID: {{ item.id }}</div>`,
})
export class ChildComponent {
// Always returns the exact same instance for this specific scope provider boundary
item = useFeatureItem();
}
MIT © Miha Mulec
FAQs
A collection of dependency injection utilities for Angular that simplify working with InjectionTokens and provide type-safe patterns for creating injectable services.
The npm package @mmstack/di receives a total of 353 weekly downloads. As such, @mmstack/di popularity was classified as not popular.
We found that @mmstack/di demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.