Skip to content

Generated Code

Understand what the compiler produces.

Overview

NeoSyringe transforms your configuration into optimized TypeScript at build time. This page shows what your code looks like before and after compilation.

Before (Your Configuration)

typescript
// container.ts
import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neosyringe';

interface ILogger {
  log(msg: string): void;
}

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

class ApiService {
  constructor(
    private logger: ILogger,
    private apiUrl: string
  ) {}
}

const apiUrl = useProperty<string>(ApiService, 'apiUrl');

export const container = defineBuilderConfig({
  name: 'AppContainer',
  injections: [
    { token: useInterface<ILogger>(), provider: ConsoleLogger },
    { token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
    { token: ApiService }
  ]
});

After (Generated Code)

The build plugin replaces the entire file:

typescript
// container.ts (after build)
import * as Import_0 from './container';

export class NeoContainer {
  private instances = new Map<any, any>();

  // -- Factories --

  private create_ILogger(): any {
    return new Import_0.ConsoleLogger();
  }

  private create_ApiService_apiUrl(): any {
    const userFactory = () => process.env.API_URL ?? 'http://localhost';
    return userFactory(this);
  }

  private create_ApiService(): any {
    return new Import_0.ApiService(
      this.resolve("ILogger"),
      this.resolve("PropertyToken:ApiService.apiUrl")
    );
  }

  constructor(
    private parent?: any,
    private legacy?: any[],
    private name: string = 'AppContainer'
  ) {}

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

    // 2. Delegate to parent
    if (this.parent) {
      try {
        return this.parent.resolve(token);
      } catch (e) {
        // Continue to legacy
      }
    }

    // 3. Delegate to legacy containers
    if (this.legacy) {
      for (const legacyContainer of this.legacy) {
        try {
          if (legacyContainer.resolve) {
            return legacyContainer.resolve(token);
          }
        } catch (e) {
          // Try next
        }
      }
    }

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

  private resolveLocal(token: any): any {
    // Interface token (string-based)
    if (token === "ILogger") {
      if (!this.instances.has("ILogger")) {
        this.instances.set("ILogger", this.create_ILogger());
      }
      return this.instances.get("ILogger");
    }

    // Property token (string-based)
    if (token === "PropertyToken:ApiService.apiUrl") {
      if (!this.instances.has("PropertyToken:ApiService.apiUrl")) {
        this.instances.set("PropertyToken:ApiService.apiUrl", this.create_ApiService_apiUrl());
      }
      return this.instances.get("PropertyToken:ApiService.apiUrl");
    }

    // Class token (reference-based)
    if (token === Import_0.ApiService) {
      if (!this.instances.has(Import_0.ApiService)) {
        this.instances.set(Import_0.ApiService, this.create_ApiService());
      }
      return this.instances.get(Import_0.ApiService);
    }

    return undefined;
  }

  // For debugging
  public get _graph() {
    return ["ILogger", "PropertyToken:ApiService.apiUrl", "ApiService"];
  }
}

export const container = new NeoContainer();

Key Observations

Token Resolution

Token TypeResolution MethodExample
InterfaceString comparisontoken === "ILogger"
ClassReference comparisontoken === Import_0.ApiService
PropertyString comparisontoken === "PropertyToken:ApiService.apiUrl"

Singleton Pattern

Singletons use the instances Map and call private factory methods:

typescript
if (!this.instances.has("ILogger")) {
  this.instances.set("ILogger", this.create_ILogger());
}
return this.instances.get("ILogger");

Transient Pattern

Transients return directly without caching:

typescript
if (token === "RequestContext") {
  return this.create_RequestContext();  // No caching!
}

Dependency Injection

Dependencies are resolved recursively via this.resolve:

typescript
private create_ApiService(): any {
  return new Import_0.ApiService(
    this.resolve("ILogger"),          // Resolves ILogger first
    this.resolve("PropertyToken:...")  // Resolves property
  );
}

With Parent Container

When using useContainer:

typescript
// Configuration
const child = defineBuilderConfig({
  useContainer: parent,
  injections: [{ token: UserService }]
});
typescript
// Generated
import { parent } from './parent-container';

class NeoContainer {
  constructor(
    private parent: typeof parent = parent,  // 👈 Parent reference
    // ...
  ) {}

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

    // Delegate to parent
    if (this.parent) {
      return this.parent.resolve(token);
    }
    
    throw new Error('...');
  }
}

export const child = new NeoContainer(parent);

With Factory Provider

typescript
// Configuration
{
  token: useInterface<IDatabase>(),
  provider: (container) => {
    const config = container.resolve(useInterface<IConfig>());
    return new PostgresDatabase(config.connectionString);
  }
}
typescript
// Generated
private create_IDatabase(): any {
  const userFactory = (container) => {
    const config = container.resolve("IConfig");
    return new PostgresDatabase(config.connectionString);
  };
  return userFactory(this);
}

Multiple Containers per File

When you declare multiple containers in the same file, each gets its own unique class:

Before

typescript
// containers.ts
export const userContainer = defineBuilderConfig({
  name: 'UserModule',
  injections: [{ token: UserService }]
});

export const productContainer = defineBuilderConfig({
  name: 'ProductModule',
  injections: [{ token: ProductService }]
});

After

typescript
// containers.ts (after build)
import * as Import_0 from './containers';

// Class for UserModule
class NeoContainer_UserModule {
  private instances = new Map<any, any>();

  private create_UserService(): any {
    return new Import_0.UserService();
  }

  constructor(
    private parent?: any,
    private name: string = 'UserModule'
  ) {}

  public resolve(token: any): any {
    // ... resolution logic
  }
}

export const userContainer = new NeoContainer_UserModule();

// Class for ProductModule
class NeoContainer_ProductModule {
  private instances = new Map<any, any>();

  private create_ProductService(): any {
    return new Import_0.ProductService();
  }

  constructor(
    private parent?: any,
    private name: string = 'ProductModule'
  ) {}

  public resolve(token: any): any {
    // ... resolution logic
  }
}

export const productContainer = new NeoContainer_ProductModule();

Key points:

  • Each container gets a unique class name: NeoContainer_{name}
  • The name field determines the class name
  • If no name field, a hash is used: NeoContainer_a1b2c3d4

See Multiple Containers for more details.

Bundle Size Impact

AspectTraditional DINeoSyringe
Container library4-11 KB0 KB
reflect-metadata~3 KB0 KB
Generated codeN/A~50-200 lines
Total7-14 KB< 1 KB

The generated code is:

  • ✅ Tree-shakeable (unused services removed)
  • ✅ Minifiable (standard JavaScript)
  • ✅ No external dependencies

Debugging

The generated container includes a _graph getter for inspection:

typescript
console.log(container._graph);
// ["ILogger", "PropertyToken:ApiService.apiUrl", "ApiService"]

Each container also has a name for error messages:

typescript
// Error: [AppContainer] Service not found: UnknownToken

Released under the MIT License.