Skip to content

Legacy Migration ​

Bridge existing DI containers (tsyringe, InversifyJS) while migrating to NeoSyringe.

Overview ​

You don't have to migrate everything at once. NeoSyringe can delegate resolution to any container that has a resolve() method.

typescript
// Bridge your existing container
export const container = defineBuilderConfig({
  useContainer: legacyContainer,  // Delegate to legacy
  injections: [
    { token: NewService }  // New services in NeoSyringe
  ]
});

With tsyringe ​

Step 1: Keep Your Existing Setup ​

typescript
// legacy-container.ts (existing tsyringe code)
import 'reflect-metadata';
import { container, injectable } from 'tsyringe';

@injectable()
export class AuthService {
  validateToken(token: string) { return true; }
}

@injectable()
export class LegacyUserRepository {
  findById(id: string) { return { id, name: 'John' }; }
}

// Register in tsyringe
container.registerSingleton(AuthService);
container.registerSingleton(LegacyUserRepository);

export { container as legacyContainer };

Step 2: Declare Legacy Tokens ​

Use declareContainerTokens for type-safety:

typescript
// container.ts
import { defineBuilderConfig, declareContainerTokens, useInterface } from '@djodjonx/neosyringe';
import { legacyContainer, AuthService, LegacyUserRepository } from './legacy-container';

// Declare what the legacy container provides
const legacy = declareContainerTokens<{
  AuthService: AuthService;
  LegacyUserRepository: LegacyUserRepository;
}>(legacyContainer);

Step 3: Bridge and Extend ​

typescript
// New services using NeoSyringe
interface ILogger {
  log(msg: string): void;
}

class ConsoleLogger implements ILogger {
  log(msg: string) { console.log(msg); }
}

class UserService {
  constructor(
    private auth: AuthService,           // From legacy!
    private repo: LegacyUserRepository,  // From legacy!
    private logger: ILogger              // From neo-syringe
  ) {}
}

export const appContainer = defineBuilderConfig({
  name: 'AppContainer',
  useContainer: legacy,  // 👈 Bridge to legacy
  injections: [
    { token: useInterface<ILogger>(), provider: ConsoleLogger },
    { token: UserService }
  ]
});

Step 4: Use It ​

typescript
// main.ts
import { appContainer } from './container';

const userService = appContainer.resolve(UserService);
// ✅ AuthService and LegacyUserRepository come from tsyringe
// ✅ ILogger comes from NeoSyringe

With InversifyJS ​

typescript
// legacy-inversify.ts
import 'reflect-metadata';
import { Container, injectable } from 'inversify';

@injectable()
class DatabaseConnection {
  query(sql: string) { return []; }
}

const inversifyContainer = new Container();
inversifyContainer.bind(DatabaseConnection).toSelf().inSingletonScope();

export { inversifyContainer, DatabaseConnection };
typescript
// container.ts
import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neosyringe';
import { inversifyContainer, DatabaseConnection } from './legacy-inversify';

const legacy = declareContainerTokens<{
  DatabaseConnection: DatabaseConnection;
}>(inversifyContainer);

class ReportService {
  constructor(private db: DatabaseConnection) {}
}

export const container = defineBuilderConfig({
  useContainer: legacy,
  injections: [
    { token: ReportService }
  ]
});

With Awilix ​

typescript
// legacy-awilix.ts
import { createContainer, asClass } from 'awilix';

class EmailService {
  send(to: string, subject: string) { /* ... */ }
}

const awilixContainer = createContainer();
awilixContainer.register({
  emailService: asClass(EmailService).singleton()
});

// Awilix uses different API, create wrapper
export const legacyContainer = {
  resolve(token: any) {
    return awilixContainer.resolve(token.name ?? token);
  }
};
typescript
// container.ts
import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neosyringe';
import { legacyContainer, EmailService } from './legacy-awilix';

const legacy = declareContainerTokens<{
  EmailService: EmailService;
}>(legacyContainer);

export const container = defineBuilderConfig({
  useContainer: legacy,
  injections: [
    { token: NotificationService }  // Uses EmailService from Awilix
  ]
});

How It Works ​

At Compile-Time ​

  1. declareContainerTokens<T>() is analyzed
  2. Type T properties are extracted (e.g., { AuthService, UserRepo })
  3. These tokens are added to parentProvidedTokens
  4. GraphValidator accepts them as valid dependencies
  5. Generator outputs: new NeoContainer([legacyContainer])

At Runtime ​

typescript
// Generated code (simplified)
class NeoContainer {
  constructor(
    private legacy?: any[],       // ← Your tsyringe/inversify container ends up here
    private name: string = 'NeoContainer'
  ) {}

  resolve(token: any): any {
    // 1. Try local resolution
    const local = this.resolveLocal(token);
    if (local !== undefined) return local;

    // 2. Delegate to legacy containers (this is also where a NeoSyringe
    // useContainer parent ends up — see the Parent Container guide)
    if (this.legacy) {
      for (const container of this.legacy) {
        try { if (container.resolve) return container.resolve(token); }  // ← Calls tsyringe!
        catch (e: any) {
          if (!(e instanceof Error && e.name === 'NeoServiceNotFoundError')) throw e;
        }
      }
    }

    throw new NeoServiceNotFoundError(`[${this.name}] Service not found or token not registered: ${token}`);
  }
}

Validation ​

NeoSyringe validates legacy bindings at compile-time:

CheckDescription
✅ Missing bindingError if dependency not in local OR legacy container
✅ Duplicate detectionError if token already registered in legacy
✅ Type safetydeclareContainerTokens<T>() provides TypeScript types

Migration Strategy ​

Phase 1: Bridge Everything ​

typescript
const legacy = declareContainerTokens<{
  ServiceA: ServiceA;
  ServiceB: ServiceB;
  ServiceC: ServiceC;
  // ... all services
}>(tsyringeContainer);

export const container = defineBuilderConfig({
  useContainer: legacy,
  injections: []  // Nothing new yet
});

Phase 2: New Services in NeoSyringe ​

typescript
export const container = defineBuilderConfig({
  useContainer: legacy,
  injections: [
    { token: NewServiceD },
    { token: NewServiceE }
  ]
});

Phase 3: Migrate One at a Time ​

typescript
// Remove ServiceA from legacy declaration
const legacy = declareContainerTokens<{
  ServiceB: ServiceB;
  ServiceC: ServiceC;
}>(tsyringeContainer);

// Add to NeoSyringe
export const container = defineBuilderConfig({
  useContainer: legacy,
  injections: [
    { token: ServiceA },  // Migrated!
    { token: NewServiceD },
    { token: NewServiceE }
  ]
});

Phase 4: Complete Migration ​

typescript
// No more legacy!
export const container = defineBuilderConfig({
  injections: [
    { token: ServiceA },
    { token: ServiceB },
    { token: ServiceC },
    { token: NewServiceD },
    { token: NewServiceE }
  ]
});

Tips ​

Keep Legacy Container Isolated ​

Put legacy code in a separate file that you can eventually delete:

src/
├── legacy/
│   └── container.ts      # Will be deleted later
├── container.ts          # NeoSyringe
└── services/
    ├── legacy/           # To be migrated
    └── new/              # Pure TypeScript

Test Both Paths ​

Ensure services work whether resolved from legacy or NeoSyringe:

typescript
describe('UserService', () => {
  it('works from legacy container', () => {
    const service = legacyContainer.resolve(UserService);
    expect(service).toBeDefined();
  });
  
  it('works from NeoSyringe container', () => {
    const service = container.resolve(UserService);
    expect(service).toBeDefined();
  });
});

Released under the MIT License.