@bosker-labs/di-assemble
v0.1.0
Published
Composable DI modules and providers with an Inversify-backed container; optional React bindings (@bosker/di-assemble/react).
Maintainers
Readme
di-assemble
Composable dependency injection for TypeScript apps: module-style @Module, explicit providers (useClass, useValue, useFactory, useExisting), and an Inversify container behind a small API. Optional React bindings live under @bosker/di-assemble/react.
Install
yarn add @bosker/di-assemble inversify reflect-metadata
# or: npm install @bosker/di-assemble inversify reflect-metadataIf you use createAppProvider / useInjection, also install react (peer dependency).
TypeScript
Enable legacy decorators for @Module, @Injectable, and @Inject (you do not need emitDecoratorMetadata if every injected constructor parameter has @Inject(token)):
{
"compilerOptions": {
"experimentalDecorators": true
}
}If you prefer Inversify’s reflection-based parameter types, set emitDecoratorMetadata: true and ensure reflect-metadata is loaded (see Inversify docs). The supported path in this library is explicit @Inject(createInjectionToken(...)) on each parameter.
App entry
Import reflect-metadata once before any module that uses @Module or @Injectable / @Inject (e.g. your root index.js or layout). The library’s injection entry also imports it, but a single top-level import keeps load order predictable.
Package exports
| Import | Purpose |
|--------|---------|
| @bosker/di-assemble | Module, bootstrapApplication, Injectable, Inject, providers, createInjectionToken, … |
| @bosker/di-assemble/react | createAppProvider, useInjection, useApplicationContext |
Quick start (core only)
import 'reflect-metadata';
import { Module, bootstrapApplication, createInjectionToken } from '@bosker/di-assemble';
const GREETING = createInjectionToken<string>('greeting');
@Module({
providers: [{ provide: GREETING, useValue: 'hello' }],
exports: [GREETING],
})
class AppModule {}
const app = bootstrapApplication({ modules: [AppModule] });
app.get(GREETING); // 'hello'createInjectionToken<T>() keeps TypeScript inference for app.get(...) and useInjection(...).
Constructor injection (useClass)
useClass is bound with Inversify’s to(Class): the container constructs the class and resolves constructor dependencies.
- Use
@Injectable()on the class and@Inject(TOKEN)on each injected constructor parameter. - Zero-argument classes do not require
@Injectable().
import 'reflect-metadata';
import {
Inject,
Injectable,
Module,
bootstrapApplication,
createInjectionToken,
} from '@bosker/di-assemble';
const DEP = createInjectionToken<string>('dep');
const CONSUMER = createInjectionToken<{ msg: string }>('consumer');
@Injectable()
class ConsumerImpl {
constructor(@Inject(DEP) readonly msg: string) {}
}
@Module({
providers: [
{ provide: DEP, useValue: 'wired' },
{ provide: CONSUMER, useClass: ConsumerImpl },
],
exports: [CONSUMER],
})
class AppModule {}
const app = bootstrapApplication({ modules: [AppModule] });
app.get(CONSUMER).msg; // 'wired'React (optional)
import { Slot } from 'expo-router';
import { createAppProvider } from '@bosker/di-assemble/react';
import { AppModule } from './app.module';
const { AppProvider } = createAppProvider({ modules: [AppModule] });
export default function RootLayout() {
return (
<AppProvider>
<Slot />
</AppProvider>
);
}import { useInjection } from '@bosker/di-assemble/react';
import { FEATURE_TOKENS } from './feature.tokens';
// Type inferred when FEATURE_TOKENS.SomeUseCase is createInjectionToken<SomeUseCase>(...)
const useCase = useInjection(FEATURE_TOKENS.SomeUseCase);Testing / storybook
Pass a pre-built context to avoid double bootstrap:
import { bootstrapApplication } from '@bosker/di-assemble';
import { createAppProvider } from '@bosker/di-assemble/react';
const app = bootstrapApplication({ modules: [AppModule], overrides: [...] });
const { AppProvider } = createAppProvider({ modules: [AppModule] });
<AppProvider app={app}>{children}</AppProvider>;Providers
useClass— Inversify instantiates the class (see Constructor injection above). Default singleton; usescope: 'transient'for a new instance perget.useValue— constant.useFactory—(injector) => Twith access toinjector.get(...).useExisting— alias another token.
Module rules
imports— other module classes.providers— bindings registered when the app boots (child-first order by the internal graph).exports— tokens that must be provided locally or re-exported from an import; invalid exports throw at bootstrap.
CLI (module scaffold)
After install, di-assemble is on PATH via node_modules/.bin.
npx di-assemble generate module <kebab-name> [options]
# or: yarn di-assemble generate module billing| Option | Default | Meaning |
|--------|---------|--------|
| --out-dir | ./src/features | Directory that will contain <kebab-name>/ |
| --import-di | @bosker/di-assemble | Import path for Module, createInjectionToken, Inject, … |
| --no-presentation | off | Skip presentation/hooks and hook export |
| --force | off | Overwrite existing files |
| --dry-run | off | Print paths only |
Generated layout matches the sample feature: services/ (request, response, contract, impl), models/ (presentation types + mapper stubs), usecases/, *.module.ts, *.tokens.ts, *.types.ts, index.ts. Replace throw new Error('… not implemented') stubs with real types and logic, then register \*Module in your app module imports.
Agent skills (skills CLI)
This package includes an Agent Skill at skills/di-assemble/SKILL.md (name: di-assemble). It is discoverable from the repo root via the open-ecosystem skills tool (vercel-labs/skills), which can install into Cursor, Claude Code, Codex, and many other agents.
After yarn add @bosker/di-assemble (or npm install @bosker/di-assemble):
# List the skill(s) shipped in the package
npx skills add ./node_modules/@bosker/di-assemble --list
# Install into Cursor (project scope; non-interactive)
npx skills add ./node_modules/@bosker/di-assemble --skill di-assemble -a cursor -y
# Or global install for your user
npx skills add ./node_modules/@bosker/di-assemble --skill di-assemble -a cursor -g -yFrom a Git clone or monorepo path (this library or a fork):
npx skills add /path/to/di-assemble --skill di-assemble -a cursor -yFrom GitHub (once published there):
npx skills add https://github.com/<org>/di-assemble --skill di-assemble -a cursor -yThe CLI symlinks or copies SKILL.md into each agent’s configured skills directory (for Cursor, typically .agents/skills/ in the project, or ~/.cursor/skills/ when using -g). You can still copy skills/di-assemble/ manually if you prefer not to use the CLI.
Monorepo / this repository
Library source lives under src/:
src/
index.ts # public entry (re-exports di)
react.ts # public entry for @bosker/di-assemble/react
di/
index.ts # core + tokens barrel
core/ # bootstrap, container, decorators, injection, …
tokens/
example/sample/ # optional reference layout (not published)
services/ # *.request.ts, *.response.ts, service contract + impl
models/ # presentation-oriented types + mappers (use case boundary)
usecases/
presentation/
bin/ # di-assemble CLI (published)
skills/di-assemble/ # Agent SKILL.md (skills CLI + Cursor-compatible)The published package includes dist/, bin/, skills/, README.md, LICENSE (run yarn build before publish).
Develop & test (contributors)
yarn install
yarn test # Vitest
yarn build # tsdown → dist/Limits (today)
- No request scope, async lifecycle hooks, or auto file scanning.
- Circular module imports are rejected; circular service graphs depend on Inversify behavior.
License
MIT
