
Security News
Package Maintainers Call for Improvements to GitHub’s New npm Security Plan
Maintainers back GitHub’s npm security overhaul but raise concerns about CI/CD workflows, enterprise support, and token management.
@gitlab/needle
Advanced tools
`@gitlab/needle` is a lightweight dependency injection framework for TypeScript, using ES stage 3 decorators. It provides a type-safe solution for managing dependencies in your TypeScript projects.
@gitlab/needle
documentation@gitlab/needle
is a lightweight dependency injection framework for TypeScript, using ES stage 3 decorators. It provides a type-safe solution for managing dependencies in your TypeScript projects.
interface A {
field: string;
}
interface B {
hello(): string;
}
import { createInterfaceId } from '@gitlab/needle';
const A = createInterfaceId<A>('A');
const B = createInterfaceId<B>('B');
@gitlab/needle
provides multiple decorator options for defining services that can be registered with the container.
The @Implements
decorator identifier which interface(s) a class implements.
import { Implements } from '@gitlab/needle';
@Implements(A)
class AImpl implements A {}
The @Service
decorator defines a service and is required by the system to create a service descriptor. We can specify dependencies and lifetime in this decorator.
import { Service, ServiceLifetime } from '@gitlab/needle';
@Service({
dependencies: [B, C],
lifetime: ServiceLifetime.Singleton,
autoActivate: false
})
class AImpl implements A {
constructor(b: B, c: C) {}
}
The lifetime
parameter can be one of:
ServiceLifetime.Singleton
- The service is created once and reused for all requests.ServiceLifetime.Transient
- A new instance of the service is created for each time the service is requested.ServiceLifetime.Scoped
- A new instance is created for each scopeThe autoActivate
parameter controls whether the service should be resolved automatically when the container is
built. This is useful for services that act as an entry point, for example, initialization of a websocket listener.
The @Injectable
decorator is a convenience decorator that combines the functionality of @Implements
and @Service
. By default, it declares the service to have a lifetime of Singleton
import { Injectable } from '@gitlab/needle';
@Injectable(A, [])
class AImpl implements A {
field = 'value';
}
@Injectable(B, [A])
class BImpl implements B {
#a: A;
constructor(a: A) {
this.#a = a;
}
hello() {
return `B.hello says A.field = ${this.#a.field}`;
}
}
ServiceCollection
ServiceCollection
is responsible for:
ServiceProvider
that can resolve those servicesimport { ServiceCollection } from '@gitlab/needle';
const services = new ServiceCollection();
ServiceCollection
provides several ways to register services:
@Service
or @Injectable
import { ServiceCollection, Injectable, createInterfaceId } from '@gitlab/needle';
// Define interface and ID
interface Logger {
log(message: string): void;
}
const Logger = createInterfaceId<Logger>('Logger');
// Implement and decorate class
@Injectable(Logger, [])
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
// Register with ServiceCollection
const services = new ServiceCollection();
services.addClass(ConsoleLogger);
For more advanced scenarios, you can create and add service descriptors:
import {
createInstanceDescriptor,
createConstructorDescriptor,
ServiceLifetime
} from '@gitlab/needle';
// Add an existing instance
const config = new ConfigurationService();
services.add(
createInstanceDescriptor({
instance: config,
aliases: [Configuration]
})
);
// Add a constructor-based service
services.add(
createConstructorDescriptor({
implementation: DatabaseService,
aliases: [Database],
dependencies: [Configuration],
lifetime: ServiceLifetime.Scoped,
autoActivate: false
})
);
Registration methods return the ServiceCollection instance to support method chaining:
services
.addClass(UserService)
.addClass(AuthenticationService)
.add(
createInstanceDescriptor({
instance: new ConfigurationService(),
aliases: [Configuration],
}),
);
ServiceCollection
Before building a ServiceProvider, you can validate the service registrations to ensure there are no issues:
const validationResult = services.validate();
if (!validationResult.isValid) {
console.error('Service validation failed:');
validationResult.errors.forEach((error) => console.error(error.message));
}
The validation checks for:
ServiceProvider
After registering all services, you can build a ServiceProvider to start resolving services:
// This will validate services and throw an error if validation fails
const serviceProvider = services.build();
If you've set autoActivate: true
for any services, they will be automatically instantiated when the provider is built.
ServiceProvider
ServiceProvider
is responsible for:
ServiceProvider
To get a single instance of a service:
const logger = serviceProvider.getRequiredService(Logger);
logger.log('Hello, world!');
This method will:
ServiceNotRegisteredError
if no implementation is registeredAmbiguousMatchError
if multiple implementations are registeredTo get all registered implementations of a service:
const loggers = serviceProvider.getServices(Logger);
loggers.forEach((logger) => logger.log('Hello from all loggers'));
This method does not throw and returns an empty array if no services are registered for the interface.
The behavior of services depends on their registered lifetime:
copes provide isolation for services marked with ServiceLifetime.Scoped
. They are useful for request-scoped services or isolating services whose lifetime is distinct from the application lifetime.
// Root provider from ServiceCollection
const rootProvider = services.build();
// Create a scope
const scope = rootProvider.createScope();
try {
// Get a scoped service within this scope
const userService = scope.getRequiredService(UserService);
// Use the service...
await userService.processRequest();
} finally {
// Always dispose the scope when finished to prevent memory leaks
scope.dispose();
}
You can create nested scopes to establish hierarchy in your service resolution:
const rootProvider = services.build();
const parentScope = rootProvider.createScope();
try {
// Create a child scope
const childScope = parentScope.createScope();
try {
// Use services from the child scope
const service = childScope.getRequiredService(ScopedService);
// ...
} finally {
childScope.dispose();
}
} finally {
parentScope.dispose();
}
Properly disposing scopes is important to prevent memory leaks, especially for scoped services that implement the Disposable
interface:
interface Disposable {
dispose(): void;
}
@Implements(DataService)
@Service({
dependencies: [],
lifetime: ServiceLifetime.Scoped
})
class DatabaseConnection implements DataService, Disposable {
#connection: Connection;
constructor() {
this.#connection = createConnection();
}
// Methods...
dispose() {
// Clean up resources when the scope is disposed
this.#connection.close();
}
}
// Usage
const scope = rootProvider.createScope();
try {
const db = scope.getRequiredService(DataService);
// Use database...
} finally {
// This will call dispose() on all scoped services in this scope
scope.dispose();
}
import { Container } from '@gitlab/needle';
const container = new Container();
container.instantiate(AImpl, BImpl);
const b = container.get(B);
console.log(b.hello());
For dependencies that can't be provided by the container, you need to add them manually. Here's an example of adding an external logger dependency:
import { createInterfaceId, brandInstance } from '@gitlab/needle';
interface Logger {
log(...args: unknown[]): void;
}
const Logger = createInterfaceId<Logger>('Logger');
const customLogger: Logger = {
log: (...args) => console.log('custom logger', ...args),
};
container.addInstances(brandInstance(Logger, customLogger));
We had to add the Language Server connection to the container (we get it from the VS Code framework already created). For that we created an alias for the Connection
called LsConnection
(so we can create the InterfaceId
) and added it to the container.
When you need to inject all registered instances of a particular interface, you can use collection dependencies:
import { createInterfaceId, collection } from '@gitlab/needle';
// Define interface and create its ID
interface Plugin {
execute(): void;
}
const Plugin = createInterfaceId<Plugin>('Plugin');
// Implement multiple plugins
@Injectable(Plugin, [])
class Plugin1 implements Plugin {
execute() {
console.log('Plugin 1');
}
}
@Injectable(Plugin, [])
class Plugin2 implements Plugin {
execute() {
console.log('Plugin 2');
}
}
// Inject all plugins into a manager
@Injectable(PluginManager, [collection(Plugin)])
class PluginManager {
constructor(private plugins: Plugin[]) {}
executeAll() {
this.plugins.forEach((p) => p.execute());
}
}
// Initialize
const container = new Container();
container.instantiate(Plugin1, Plugin2, PluginManager);
// Use
const manager = container.get(PluginManager);
manager.executeAll(); // Outputs: "Plugin 1" "Plugin 2"
If the same collection is required in multiple places, a collectionId variable can be created:
import { createInterfaceId, createCollectionId } from '@gitlab/needle';
// Same Plugin setup as example above
const PluginsCollection = createCollectionId(Plugin);
@Injectable(PluginManager1, [PluginsCollection])
class PluginManager1 {
constructor(private plugins: Plugin[]) {}
}
@Injectable(PluginManager2, [PluginsCollection])
class PluginManager2 {
constructor(private plugins: Plugin[]) {}
}
createInterfaceId<T>(id: string): InterfaceId<T>
Creates a unique runtime identifier for an interface.
createCollectionId<T>(interfaceId: InterfaceId<T>): CollectionId<T>
Creates a runtime identifier for a collection of interfaces.
collection<T>(interfaceId: InterfaceId<T>): CollectionId<T>
Alias of createCollectionId
, intended for use directly within @Injectable when no intermediate variable is required.
@Injectable(id: InterfaceId<I>, dependencies: (InterfaceId<unknown> | CollectionId<unknown>)[])
Decorator for classes to mark them as injectable and specify their dependencies.
Container
instantiate(...classes: Class[]): void
: Initializes the specified classes.addInstances(...instances: BrandedInstance<object>[]): void
: Adds pre-initialized objects to the container.get<T>(id: InterfaceId<T>): T
: Retrieves an instance from the container.brandInstance<T>(id: InterfaceId<T>, instance: T): BrandedInstance<T>
Brands an instance for use with container.addInstances()
.
createInterfaceId
for each interface.@Injectable
.container.get()
sparingly, preferring constructor injection.ConfigService
(no constructor arguments)ConnectionService
(many constructor arguments)@Injectable
.InterfaceId
instances have the correct interface type and string identifier.@Injectable
decorator.To reduce boilerplate, consider adding this snippet to your VS Code TypeScript snippets:
{
"New DI Class": {
"prefix": "diclass",
"body": [
"import { Injectable, createInterfaceId } from '@gitlab/needle';",
"",
"export interface ${1:InterfaceName} {}",
"",
"export const ${1:InterfaceName} = createInterfaceId<${1:InterfaceName}>('${1:InterfaceName}');",
"",
"@Injectable(${1:InterfaceName}, [])",
"export class Default${1:InterfaceName} implements ${1:InterfaceName} {}"
],
"description": "Creates DI framework boilerplate"
}
}
You can't, because the ES Stage 3 decorators don't support decorating method/function parameters.
@Injectable
is a dark, write-only type magicContainer
implementation is a straightforward graph traversing and string validationLearn more in the blog post mentioned in the next section.
Current implementation works both for legacy experimental decorators and new TypeScript 5 decorators.
If the decorators are used in a legacy environment, you won't get errors explaining that you should only use Implements
and Service
decorators on classes.
FAQs
`@gitlab/needle` is a lightweight dependency injection framework for TypeScript, using ES stage 3 decorators. It provides a type-safe solution for managing dependencies in your TypeScript projects.
The npm package @gitlab/needle receives a total of 2,128 weekly downloads. As such, @gitlab/needle popularity was classified as popular.
We found that @gitlab/needle demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 7 open source maintainers 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
Maintainers back GitHub’s npm security overhaul but raise concerns about CI/CD workflows, enterprise support, and token management.
Product
Socket Firewall is a free tool that blocks malicious packages at install time, giving developers proactive protection against rising supply chain attacks.
Research
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.