vite-plugin-di-auto-register
v0.1.1
Published
Auto-register injection-js dependencies for Vite-based DDD projects. Scans @Injectable() classes and generates a register file at dev/build time.
Maintainers
Readme
vite-plugin-di-auto-register
A Vite plugin that auto-registers
injection-jsdependencies for DDD-style Vue / Vite projects. Scans@Injectable()classes at dev/build time and generates a strongly-typedregister.ts.
中文用户:本 README 以英文为主,关键章节附中文注释。
Why
In a typical DDD frontend project using injection-js, every new @Injectable() class needs to be registered manually in a central provider list. That list grows fast and is easy to forget.
This plugin:
- 🔍 Scans configured directories for
@Injectable()classes - 🔗 Binds classes to
InjectionTokens declared intokens.ts(or anywhere) when theyimplementsan interface or use@Injectable({ as }) - 📝 Generates an
app/di/register.tswith all providers wired up - ⚡ Reacts to file add / change / unlink in dev mode (debounced)
- 🛡 Skips writing when content unchanged (no HMR loops)
- 🧪 CLI included for one-shot regeneration in CI / pre-commit
Install
npm i -D vite-plugin-di-auto-register
# peer
npm i injection-js reflect-metadataQuick start
1. Configure Vite
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteDiPlugin } from 'vite-plugin-di-auto-register'
export default defineConfig({
plugins: [viteDiPlugin(), vue()],
})2. Project conventions
Put three files under src/app/di/ (locations are configurable, see Options):
// src/app/di/scan.ts — directories to scan, like Spring's @ComponentScan
export const scanDirs = ['features', 'core', 'app']// src/app/di/tokens.ts — InjectionToken definitions
import { InjectionToken } from 'injection-js'
import type { AuthRepository } from '../../features/auth/domain/AuthRepository'
export const AUTH_REPOSITORY = new InjectionToken<AuthRepository>('AuthRepository')// src/app/di/index.ts — your DI public surface
import { type Injector, type Provider } from 'injection-js'
export * from 'injection-js'
export let di: Injector
export function setInjector(i: Injector): void { di = i }
// Hand-written providers (useValue / useFactory / useExisting)
export const customProviders: Provider[] = []3. Mark services with @Injectable()
// features/auth/infrastructure/AuthRepositoryImpl.ts
import { Injectable } from 'injection-js'
import type { AuthRepository } from '../domain/AuthRepository'
@Injectable()
export class AuthRepositoryImpl implements AuthRepository {
async login() { return 'token' }
}// features/auth/application/LoginUseCase.ts
import { Inject, Injectable } from 'injection-js'
import { AUTH_REPOSITORY } from '../../../app/di/tokens'
@Injectable()
export class LoginUseCase {
constructor(@Inject(AUTH_REPOSITORY) private repo: AuthRepository) {}
}4. Bootstrap
// src/main.ts
import 'reflect-metadata'
import { createApp } from 'vue'
import { registerDependencies } from './app/di/register'
import App from './App.vue'
registerDependencies()
createApp(App).mount('#app')register.ts is fully auto-generated — never edit it by hand.
How matching works
When a class is decorated with @Injectable(), the plugin registers it according to:
| Source | Bound as |
|---|---|
| @Injectable() class X implements Foo and a new InjectionToken<Foo>(...) exists | { provide: TOKEN, useClass: X } |
| @Injectable({ as: AbstractCls }) class X extends AbstractCls | { provide: AbstractCls, useClass: X } |
| Otherwise | X (class itself as token, no @Inject needed for consumers) |
Tokens are picked up from:
- The configured
tokensFile(defaultapp/di/tokens.ts) - Any
*.tsfile under the scanned directories that declaresexport const X = new InjectionToken<Y>('...')
CLI
A di-auto-register binary is exposed for one-shot regeneration (CI, pre-commit hooks, scripts):
npx di-auto-register --src=srcFlags:
| Flag | Default | Description |
|---|---|---|
| --src <dir> | 'src' | Source root, relative to cwd |
| --register-file <path> | 'app/di/register.ts' | Generated file (relative to --src) |
| --tokens-file <path> | 'app/di/tokens.ts' | Token definitions (relative to --src) |
| --scan-config <path> | 'app/di/scan.ts' | Scan config (relative to --src) |
| --silent | false | Suppress warnings |
| -h, --help | | Show help |
Add it to your scripts:
{
"scripts": {
"generate:di": "di-auto-register"
}
}Options
viteDiPlugin({
srcDir: 'src', // 默认 'src', relative to vite root
registerFile: 'app/di/register.ts',
tokensFile: 'app/di/tokens.ts',
scanConfigFile: 'app/di/scan.ts',
silent: false,
debounceMs: 50, // dev-mode watcher debounce window
})| Option | Type | Default | Notes |
|---|---|---|---|
| srcDir | string | 'src' | Relative to Vite root |
| registerFile | string | 'app/di/register.ts' | Relative to srcDir |
| tokensFile | string | 'app/di/tokens.ts' | Relative to srcDir |
| scanConfigFile | string | 'app/di/scan.ts' | Relative to srcDir |
| silent | boolean | false | Mute plugin & generator console output |
| debounceMs | number | 50 | Watcher debounce in dev |
Programmatic API
import { generateDi, resolvePaths, DEFAULT_PATHS } from 'vite-plugin-di-auto-register'
const result = generateDi({ srcDir: '/abs/path/src' })
console.log(result.tokenBindings, result.standaloneClasses, result.warnings)Exported types: ViteDiPluginOptions, GenerateDiOptions, GenerateDiResult, DiPaths, TokenBinding, StandaloneClass, ResolvedPaths.
How HMR is handled
- The plugin writes
register.tsonly when content actually differs → no churn when nothing changed. - Changes to
register.tsitself are ignored by the watcher → no infinite loop. - Rapid file events are debounced (
debounceMs).
tsconfig
Make sure your app tsconfig.json has decorator metadata enabled:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}And import 'reflect-metadata' before anything else in main.ts.
FAQ
Q: SSR support?
The plugin only does file generation; the generated register.ts is plain TS that any bundler / runtime can consume.
Q: Multiple Vite roots / monorepo?
Use one viteDiPlugin() per Vite config. Each instance has its own srcDir.
Q: It generated a binding I didn't expect.
Check result.warnings (or run npx di-auto-register) — unresolvable @Injectable({ as }) imports are logged with the offending file path.
Q: Can I commit the generated register.ts?
Yes, recommended — it's deterministic and gives a clean diff when DI graph changes.
