@half0wl/container 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BaseService: () => BaseService,
24
+ Container: () => Container,
25
+ Inject: () => Inject,
26
+ Service: () => Service,
27
+ getPropertyDescriptorFromChain: () => getPropertyDescriptorFromChain,
28
+ wrapWithTracing: () => wrapWithTracing
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/base.ts
33
+ var BaseService = class {
34
+ /** The user-defined dependencies (everything except `registry`). */
35
+ deps;
36
+ /** The container instance, used internally by {@link Inject} to resolve other services. */
37
+ registry;
38
+ constructor(dependencies) {
39
+ const { registry, ...rest } = dependencies;
40
+ this.deps = rest;
41
+ this.registry = registry;
42
+ }
43
+ };
44
+
45
+ // src/decorators.ts
46
+ var TRACED_MARKER = /* @__PURE__ */ Symbol("__service_traced__");
47
+ function Service(options) {
48
+ return (target) => {
49
+ if (options?.trace) {
50
+ target[TRACED_MARKER] = true;
51
+ }
52
+ return target;
53
+ };
54
+ }
55
+ function Inject(factory) {
56
+ return (_target, propertyKey) => {
57
+ const cacheKey = /* @__PURE__ */ Symbol(`__inject_${String(propertyKey)}`);
58
+ Object.defineProperty(_target, propertyKey, {
59
+ get() {
60
+ const cached = this[cacheKey];
61
+ if (cached !== void 0) return cached;
62
+ const resolved = this.registry.get(factory());
63
+ this[cacheKey] = resolved;
64
+ return resolved;
65
+ },
66
+ enumerable: true,
67
+ configurable: true
68
+ });
69
+ };
70
+ }
71
+
72
+ // src/helpers.ts
73
+ function getPropertyDescriptorFromChain(proto, key, stopAt) {
74
+ let current = proto;
75
+ while (current && current !== stopAt) {
76
+ const desc = Object.getOwnPropertyDescriptor(current, key);
77
+ if (desc) return desc;
78
+ current = Object.getPrototypeOf(current);
79
+ }
80
+ return void 0;
81
+ }
82
+ function wrapWithTracing(instance, trace, stopAt) {
83
+ const className = instance.constructor.name;
84
+ const seen = /* @__PURE__ */ new Set();
85
+ let proto = Object.getPrototypeOf(instance);
86
+ while (proto && proto !== stopAt) {
87
+ for (const key of Object.getOwnPropertyNames(proto)) {
88
+ if (key === "constructor" || seen.has(key)) continue;
89
+ seen.add(key);
90
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
91
+ if (!desc || !desc.value || typeof desc.value !== "function") continue;
92
+ const original = desc.value;
93
+ const spanName = `${className}.${key}`;
94
+ instance[key] = function(...args) {
95
+ return trace(spanName, () => original.apply(this, args));
96
+ };
97
+ }
98
+ proto = Object.getPrototypeOf(proto);
99
+ }
100
+ }
101
+
102
+ // src/container.ts
103
+ var Container = class _Container {
104
+ static globalInstance;
105
+ instances = /* @__PURE__ */ new Map();
106
+ deps;
107
+ config;
108
+ constructor(config = {}) {
109
+ this.config = config;
110
+ this.deps = config.deps;
111
+ }
112
+ /**
113
+ * Create a new container instance.
114
+ *
115
+ * Accepts either a plain deps object or a {@link ContainerConfig} with
116
+ * additional options like `trace`.
117
+ * When called with no arguments, the container defers resolution
118
+ * until {@link setDeps} is called.
119
+ *
120
+ * @typeParam TDeps - User-defined dependency interface
121
+ * @param depsOrConfig - Dependencies or full configuration object
122
+ * @returns A new isolated container instance
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * // Simple — pass deps directly
127
+ * const container = Container.create<ContainerDeps>({ db, logger });
128
+ *
129
+ * // With config — pass options alongside deps
130
+ * const container = Container.create<ContainerDeps>({
131
+ * deps: { db, logger },
132
+ * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),
133
+ * });
134
+ *
135
+ * // Lazy — set deps later
136
+ * const container = Container.create<ContainerDeps>();
137
+ * container.setDeps({ db, logger });
138
+ * ```
139
+ */
140
+ static create(depsOrConfig) {
141
+ if (depsOrConfig && typeof depsOrConfig === "object" && ("deps" in depsOrConfig || "trace" in depsOrConfig)) {
142
+ return new _Container(depsOrConfig);
143
+ }
144
+ return new _Container({
145
+ deps: depsOrConfig
146
+ });
147
+ }
148
+ /**
149
+ * Get the global singleton container instance.
150
+ *
151
+ * Creates one on first call. Useful for module-level exports that
152
+ * need a container reference before deps are available.
153
+ *
154
+ * @typeParam TDeps - User-defined dependency interface
155
+ * @returns The global container instance
156
+ */
157
+ static getGlobalInstance() {
158
+ if (!_Container.globalInstance) {
159
+ _Container.globalInstance = new _Container();
160
+ }
161
+ return _Container.globalInstance;
162
+ }
163
+ /**
164
+ * Reset the global singleton container.
165
+ *
166
+ * Clears all cached instances and removes the global reference.
167
+ * Primarily used for test cleanup.
168
+ */
169
+ static resetGlobal() {
170
+ _Container.globalInstance?.clear();
171
+ _Container.globalInstance = void 0;
172
+ }
173
+ /**
174
+ * Set or replace the dependencies after container creation.
175
+ *
176
+ * Useful for lazy initialization patterns where the container
177
+ * is created before deps are available.
178
+ *
179
+ * @param deps - The user-defined dependencies
180
+ */
181
+ setDeps(deps) {
182
+ this.deps = deps;
183
+ }
184
+ /**
185
+ * Resolve a service by its class constructor.
186
+ *
187
+ * On first call, lazily constructs the service with `{ ...deps, registry: this }`
188
+ * and caches the singleton. Subsequent calls return the cached instance.
189
+ *
190
+ * If the service was decorated with `@Service({ trace: true })`, all its methods
191
+ * are automatically wrapped with the configured `trace` function.
192
+ *
193
+ * @typeParam T - The service type
194
+ * @param serviceClass - The class constructor to resolve
195
+ * @returns The singleton instance
196
+ * @throws If deps have not been set
197
+ * @throws If the service has tracing enabled but no `trace` function was configured
198
+ */
199
+ get(serviceClass) {
200
+ const existing = this.instances.get(serviceClass);
201
+ if (existing) return existing;
202
+ if (!this.deps) {
203
+ throw new Error(
204
+ "Container deps not set. Call setDeps() or pass deps to Container.create()."
205
+ );
206
+ }
207
+ const serviceDeps = {
208
+ ...this.deps,
209
+ registry: this
210
+ };
211
+ const instance = new serviceClass(serviceDeps);
212
+ if (serviceClass[TRACED_MARKER]) {
213
+ if (!this.config.trace) {
214
+ throw new Error(
215
+ `Service "${serviceClass.name}" has trace enabled but no trace function was provided. Pass { trace } to Container.create().`
216
+ );
217
+ }
218
+ wrapWithTracing(
219
+ instance,
220
+ this.config.trace,
221
+ BaseService.prototype
222
+ );
223
+ }
224
+ this.instances.set(serviceClass, instance);
225
+ return instance;
226
+ }
227
+ /**
228
+ * Register a pre-built instance for a service class.
229
+ *
230
+ * The registered instance is returned by {@link get} without constructing.
231
+ * Primarily used for injecting test mocks.
232
+ *
233
+ * @typeParam T - The service type
234
+ * @param serviceClass - The class constructor to associate with the instance
235
+ * @param instance - The pre-built instance to register
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });
240
+ * testContainer.register(UserService, mockUserService);
241
+ * const auth = testContainer.get(AuthService); // uses mockUserService via @Inject
242
+ * ```
243
+ */
244
+ register(serviceClass, instance) {
245
+ this.instances.set(serviceClass, instance);
246
+ }
247
+ /**
248
+ * Clear all cached service instances.
249
+ *
250
+ * After calling this, the next {@link get} call for any service
251
+ * will construct a fresh instance. Does not clear deps or config.
252
+ */
253
+ clear() {
254
+ this.instances.clear();
255
+ }
256
+ };
257
+ // Annotate the CommonJS export names for ESM import in node:
258
+ 0 && (module.exports = {
259
+ BaseService,
260
+ Container,
261
+ Inject,
262
+ Service,
263
+ getPropertyDescriptorFromChain,
264
+ wrapWithTracing
265
+ });
266
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/base.ts","../src/decorators.ts","../src/helpers.ts","../src/container.ts"],"sourcesContent":["export {\n BaseService,\n type Constructor,\n type ContainerConfig,\n type IContainer,\n type ServiceDependencies,\n type TraceFn,\n} from \"./base.js\";\nexport { Container } from \"./container.js\";\nexport { Inject, Service, type ServiceOptions } from \"./decorators.js\";\nexport { getPropertyDescriptorFromChain, wrapWithTracing } from \"./helpers.js\";\n","/** A class constructor type. */\n// biome-ignore lint/suspicious/noExplicitAny: Constructor must accept any args to be generic over all classes\nexport type Constructor<T = unknown> = new (...args: any[]) => T;\n\n/** A pluggable tracing function signature. */\nexport type TraceFn = (spanName: string, fn: () => unknown) => unknown;\n\n/**\n * Minimal interface representing a DI container.\n *\n * Services receive this as `this.registry` to resolve other services\n * without coupling to the full {@link Container} implementation.\n */\nexport interface IContainer {\n /**\n * Resolve a service by its class constructor.\n *\n * @typeParam T - The service type\n * @param serviceClass - The class constructor to resolve\n * @returns The singleton instance of the service\n */\n get<T>(serviceClass: Constructor<T>): T;\n}\n\n/**\n * The full set of dependencies passed to a service constructor.\n *\n * Combines the user-defined `TDeps` with the container's `registry`,\n * which is injected automatically by the container.\n *\n * @typeParam TDeps - User-defined dependency interface\n */\nexport type ServiceDependencies<TDeps> = TDeps & { registry: IContainer };\n\n/**\n * Configuration object for {@link Container.create}.\n *\n * @typeParam TDeps - User-defined dependency interface\n */\nexport interface ContainerConfig<TDeps> {\n /** The user-defined dependencies to inject into services. */\n deps?: TDeps;\n\n /**\n * Optional tracing function for services decorated with `@Service({ trace: true })`.\n * When a traced service is resolved, all its methods are wrapped with this function.\n *\n * @param spanName - The span name in `ClassName.methodName` format\n * @param fn - The original method to execute within the span\n * @returns The return value of `fn`\n *\n * @example\n * ```ts\n * const container = Container.create<ContainerDeps>({\n * deps: { db, logger },\n * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),\n * });\n * ```\n */\n trace?: TraceFn;\n}\n\n/**\n * Abstract base class that all container-managed services extend.\n *\n * Receives injected dependencies via its constructor and exposes them\n * as `this.deps` (user-defined) and `this.registry` (the container).\n *\n * @typeParam TDeps - User-defined dependency interface\n *\n * @example\n * ```ts\n * interface ContainerDeps {\n * db: DatabaseClient;\n * logger: Logger;\n * }\n *\n * @Service()\n * class UserService extends BaseService<ContainerDeps> {\n * findById(id: string) {\n * return this.deps.db.users.findUnique({ where: { id } });\n * }\n * }\n * ```\n */\nexport abstract class BaseService<TDeps = unknown> {\n /** The user-defined dependencies (everything except `registry`). */\n protected readonly deps: TDeps;\n\n /** The container instance, used internally by {@link Inject} to resolve other services. */\n protected readonly registry: IContainer;\n\n constructor(dependencies: ServiceDependencies<TDeps>) {\n const { registry, ...rest } = dependencies as ServiceDependencies<TDeps>;\n this.deps = rest as unknown as TDeps;\n this.registry = registry;\n }\n}\n","import type { Constructor, IContainer } from \"./base.js\";\n\n/** @internal Symbol used to mark classes decorated with `@Service({ trace: true })`. */\nexport const TRACED_MARKER = Symbol(\"__service_traced__\");\n\n/**\n * Options for the {@link Service} decorator.\n */\nexport interface ServiceOptions {\n /**\n * When `true`, all methods on this service are automatically wrapped\n * with the container's `trace` function after construction.\n *\n * Requires a `trace` function in the {@link ContainerConfig}.\n *\n * @defaultValue `false`\n */\n trace?: boolean;\n}\n\n/**\n * Class decorator that marks a class as container-managed.\n *\n * When `trace` is enabled, the container will wrap all methods\n * with tracing spans using the configured trace function.\n *\n * Requires `experimentalDecorators: true` in tsconfig.json.\n *\n * @param options - Optional configuration for the service\n * @returns A class decorator\n *\n * @example\n * ```ts\n * @Service()\n * class UserService extends BaseService<ContainerDeps> {\n * findById(id: string) {\n * return this.deps.db.users.findUnique({ where: { id } });\n * }\n * }\n *\n * @Service({ trace: true })\n * class TracedService extends BaseService<ContainerDeps> {\n * // All methods auto-traced as \"TracedService.methodName\"\n * process() { ... }\n * }\n * ```\n */\nexport function Service(options?: ServiceOptions): ClassDecorator {\n return (target) => {\n if (options?.trace) {\n (target as unknown as Record<symbol, boolean>)[TRACED_MARKER] = true;\n }\n return target;\n };\n}\n\ntype InjectTarget<T> = { registry: IContainer } & Record<symbol, T>;\n\n/**\n * Property decorator for declarative inter-service dependency injection.\n *\n * Defines a lazy getter that resolves the dependency from the same container\n * on first access and caches it per-instance. Uses a factory function\n * (`() => X` instead of `X` directly) to avoid circular import issues\n * between service files.\n *\n * Requires `experimentalDecorators: true` in tsconfig.json.\n *\n * @typeParam T - The injected service type\n * @param factory - A factory function returning the service class constructor.\n * Called lazily on first property access.\n * @returns A property decorator\n *\n * @example\n * ```ts\n * @Service()\n * class AuthService extends BaseService<ContainerDeps> {\n * @Inject(() => UserService)\n * declare readonly userService: UserService;\n *\n * @Inject(() => TokenService)\n * declare readonly tokenService: TokenService;\n *\n * authenticate(token: string) {\n * const payload = this.tokenService.verify(token);\n * return this.userService.findById(payload.userId);\n * }\n * }\n * ```\n */\nexport function Inject<T>(factory: () => Constructor<T>) {\n return (_target: object, propertyKey: string | symbol): void => {\n const cacheKey = Symbol(`__inject_${String(propertyKey)}`);\n\n Object.defineProperty(_target, propertyKey, {\n get(this: InjectTarget<T>) {\n const cached = this[cacheKey];\n if (cached !== undefined) return cached;\n const resolved = this.registry.get(factory());\n this[cacheKey] = resolved;\n return resolved;\n },\n enumerable: true,\n configurable: true,\n });\n };\n}\n","import type { TraceFn } from \"./base.js\";\n\n/**\n * Walk the prototype chain from `proto` up to (but not including) `stopAt`,\n * looking for a property descriptor with the given key.\n *\n * @param proto - The prototype to start searching from\n * @param key - The property name to look for\n * @param stopAt - The prototype to stop at (exclusive)\n * @returns The property descriptor if found, or `undefined`\n */\nexport function getPropertyDescriptorFromChain(\n proto: object,\n key: string,\n stopAt: object,\n): PropertyDescriptor | undefined {\n let current: object | null = proto;\n while (current && current !== stopAt) {\n const desc = Object.getOwnPropertyDescriptor(current, key);\n if (desc) return desc;\n current = Object.getPrototypeOf(current);\n }\n return undefined;\n}\n\n/**\n * Wrap all methods on an instance with a tracing function.\n *\n * Walks the prototype chain from the instance up to `stopAt`, wrapping\n * every method (functions with a `value` descriptor) with the `trace` callback.\n * Getters, setters, and the constructor are skipped. When a method exists on\n * multiple prototypes in the chain, the closest (most derived) version wins.\n *\n * Span names follow the `ClassName.methodName` format.\n *\n * @param instance - The object instance whose methods will be wrapped\n * @param trace - The tracing function that wraps each method call\n * @param stopAt - The prototype to stop walking at (typically `BaseService.prototype`)\n *\n * @example\n * ```ts\n * wrapWithTracing(instance, (spanName, fn) => {\n * console.log(`>> ${spanName}`);\n * const result = fn();\n * console.log(`<< ${spanName}`);\n * return result;\n * }, BaseService.prototype);\n * ```\n */\nexport function wrapWithTracing(\n instance: object,\n trace: TraceFn,\n stopAt: object,\n): void {\n const className = instance.constructor.name;\n const seen = new Set<string>();\n let proto: object | null = Object.getPrototypeOf(instance);\n\n while (proto && proto !== stopAt) {\n for (const key of Object.getOwnPropertyNames(proto)) {\n if (key === \"constructor\" || seen.has(key)) continue;\n seen.add(key);\n\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (!desc || !desc.value || typeof desc.value !== \"function\") continue;\n\n const original = desc.value as (...args: unknown[]) => unknown;\n const spanName = `${className}.${key}`;\n\n (instance as Record<string, unknown>)[key] = function (\n this: unknown,\n ...args: unknown[]\n ) {\n return trace(spanName, () => original.apply(this, args));\n };\n }\n proto = Object.getPrototypeOf(proto);\n }\n}\n","import {\n BaseService,\n type Constructor,\n type ContainerConfig,\n type IContainer,\n type ServiceDependencies,\n} from \"./base.js\";\nimport { TRACED_MARKER } from \"./decorators.js\";\nimport { wrapWithTracing } from \"./helpers.js\";\n\n/**\n * A dependency injection container that lazily creates and caches singleton service instances.\n *\n * Supports both a global singleton (via {@link Container.getGlobalInstance}) and\n * isolated instances (via {@link Container.create}) for test environments.\n *\n * @typeParam TDeps - User-defined dependency interface\n *\n * @example\n * ```ts\n * // Production usage\n * const container = Container.create<ContainerDeps>({ db, logger });\n * const auth = container.get(AuthService);\n *\n * // Test usage (isolated)\n * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });\n * testContainer.register(UserService, mockUserService);\n * const auth = testContainer.get(AuthService);\n * ```\n */\nexport class Container<TDeps = unknown> implements IContainer {\n private static globalInstance: Container<unknown> | undefined;\n\n private instances = new Map<Constructor, unknown>();\n private deps: TDeps | undefined;\n private config: ContainerConfig<TDeps>;\n\n private constructor(config: ContainerConfig<TDeps> = {}) {\n this.config = config;\n this.deps = config.deps;\n }\n\n /**\n * Create a new container instance.\n *\n * Accepts either a plain deps object or a {@link ContainerConfig} with\n * additional options like `trace`.\n * When called with no arguments, the container defers resolution\n * until {@link setDeps} is called.\n *\n * @typeParam TDeps - User-defined dependency interface\n * @param depsOrConfig - Dependencies or full configuration object\n * @returns A new isolated container instance\n *\n * @example\n * ```ts\n * // Simple — pass deps directly\n * const container = Container.create<ContainerDeps>({ db, logger });\n *\n * // With config — pass options alongside deps\n * const container = Container.create<ContainerDeps>({\n * deps: { db, logger },\n * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),\n * });\n *\n * // Lazy — set deps later\n * const container = Container.create<ContainerDeps>();\n * container.setDeps({ db, logger });\n * ```\n */\n static create<TDeps>(\n depsOrConfig?: TDeps | ContainerConfig<TDeps>,\n ): Container<TDeps> {\n if (\n depsOrConfig &&\n typeof depsOrConfig === \"object\" &&\n (\"deps\" in depsOrConfig || \"trace\" in depsOrConfig)\n ) {\n return new Container<TDeps>(depsOrConfig as ContainerConfig<TDeps>);\n }\n return new Container<TDeps>({\n deps: depsOrConfig as TDeps | undefined,\n });\n }\n\n /**\n * Get the global singleton container instance.\n *\n * Creates one on first call. Useful for module-level exports that\n * need a container reference before deps are available.\n *\n * @typeParam TDeps - User-defined dependency interface\n * @returns The global container instance\n */\n static getGlobalInstance<TDeps>(): Container<TDeps> {\n if (!Container.globalInstance) {\n Container.globalInstance = new Container<TDeps>();\n }\n return Container.globalInstance as Container<TDeps>;\n }\n\n /**\n * Reset the global singleton container.\n *\n * Clears all cached instances and removes the global reference.\n * Primarily used for test cleanup.\n */\n static resetGlobal(): void {\n Container.globalInstance?.clear();\n Container.globalInstance = undefined;\n }\n\n /**\n * Set or replace the dependencies after container creation.\n *\n * Useful for lazy initialization patterns where the container\n * is created before deps are available.\n *\n * @param deps - The user-defined dependencies\n */\n setDeps(deps: TDeps): void {\n this.deps = deps;\n }\n\n /**\n * Resolve a service by its class constructor.\n *\n * On first call, lazily constructs the service with `{ ...deps, registry: this }`\n * and caches the singleton. Subsequent calls return the cached instance.\n *\n * If the service was decorated with `@Service({ trace: true })`, all its methods\n * are automatically wrapped with the configured `trace` function.\n *\n * @typeParam T - The service type\n * @param serviceClass - The class constructor to resolve\n * @returns The singleton instance\n * @throws If deps have not been set\n * @throws If the service has tracing enabled but no `trace` function was configured\n */\n get<T>(serviceClass: Constructor<T>): T {\n const existing = this.instances.get(serviceClass);\n if (existing) return existing as T;\n\n if (!this.deps) {\n throw new Error(\n \"Container deps not set. Call setDeps() or pass deps to Container.create().\",\n );\n }\n\n const serviceDeps: ServiceDependencies<TDeps> = {\n ...this.deps,\n registry: this,\n };\n\n const instance = new serviceClass(serviceDeps);\n\n // Apply tracing if @Service({ trace: true }) was used\n if ((serviceClass as unknown as Record<symbol, unknown>)[TRACED_MARKER]) {\n if (!this.config.trace) {\n throw new Error(\n `Service \"${serviceClass.name}\" has trace enabled but no trace function was provided. Pass { trace } to Container.create().`,\n );\n }\n wrapWithTracing(\n instance as object,\n this.config.trace,\n BaseService.prototype,\n );\n }\n\n this.instances.set(serviceClass, instance);\n return instance;\n }\n\n /**\n * Register a pre-built instance for a service class.\n *\n * The registered instance is returned by {@link get} without constructing.\n * Primarily used for injecting test mocks.\n *\n * @typeParam T - The service type\n * @param serviceClass - The class constructor to associate with the instance\n * @param instance - The pre-built instance to register\n *\n * @example\n * ```ts\n * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });\n * testContainer.register(UserService, mockUserService);\n * const auth = testContainer.get(AuthService); // uses mockUserService via @Inject\n * ```\n */\n register<T>(serviceClass: Constructor<T>, instance: T): void {\n this.instances.set(serviceClass, instance);\n }\n\n /**\n * Clear all cached service instances.\n *\n * After calling this, the next {@link get} call for any service\n * will construct a fresh instance. Does not clear deps or config.\n */\n clear(): void {\n this.instances.clear();\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,325 @@
1
+ /** A class constructor type. */
2
+ type Constructor<T = unknown> = new (...args: any[]) => T;
3
+ /** A pluggable tracing function signature. */
4
+ type TraceFn = (spanName: string, fn: () => unknown) => unknown;
5
+ /**
6
+ * Minimal interface representing a DI container.
7
+ *
8
+ * Services receive this as `this.registry` to resolve other services
9
+ * without coupling to the full {@link Container} implementation.
10
+ */
11
+ interface IContainer {
12
+ /**
13
+ * Resolve a service by its class constructor.
14
+ *
15
+ * @typeParam T - The service type
16
+ * @param serviceClass - The class constructor to resolve
17
+ * @returns The singleton instance of the service
18
+ */
19
+ get<T>(serviceClass: Constructor<T>): T;
20
+ }
21
+ /**
22
+ * The full set of dependencies passed to a service constructor.
23
+ *
24
+ * Combines the user-defined `TDeps` with the container's `registry`,
25
+ * which is injected automatically by the container.
26
+ *
27
+ * @typeParam TDeps - User-defined dependency interface
28
+ */
29
+ type ServiceDependencies<TDeps> = TDeps & {
30
+ registry: IContainer;
31
+ };
32
+ /**
33
+ * Configuration object for {@link Container.create}.
34
+ *
35
+ * @typeParam TDeps - User-defined dependency interface
36
+ */
37
+ interface ContainerConfig<TDeps> {
38
+ /** The user-defined dependencies to inject into services. */
39
+ deps?: TDeps;
40
+ /**
41
+ * Optional tracing function for services decorated with `@Service({ trace: true })`.
42
+ * When a traced service is resolved, all its methods are wrapped with this function.
43
+ *
44
+ * @param spanName - The span name in `ClassName.methodName` format
45
+ * @param fn - The original method to execute within the span
46
+ * @returns The return value of `fn`
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const container = Container.create<ContainerDeps>({
51
+ * deps: { db, logger },
52
+ * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),
53
+ * });
54
+ * ```
55
+ */
56
+ trace?: TraceFn;
57
+ }
58
+ /**
59
+ * Abstract base class that all container-managed services extend.
60
+ *
61
+ * Receives injected dependencies via its constructor and exposes them
62
+ * as `this.deps` (user-defined) and `this.registry` (the container).
63
+ *
64
+ * @typeParam TDeps - User-defined dependency interface
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * interface ContainerDeps {
69
+ * db: DatabaseClient;
70
+ * logger: Logger;
71
+ * }
72
+ *
73
+ * @Service()
74
+ * class UserService extends BaseService<ContainerDeps> {
75
+ * findById(id: string) {
76
+ * return this.deps.db.users.findUnique({ where: { id } });
77
+ * }
78
+ * }
79
+ * ```
80
+ */
81
+ declare abstract class BaseService<TDeps = unknown> {
82
+ /** The user-defined dependencies (everything except `registry`). */
83
+ protected readonly deps: TDeps;
84
+ /** The container instance, used internally by {@link Inject} to resolve other services. */
85
+ protected readonly registry: IContainer;
86
+ constructor(dependencies: ServiceDependencies<TDeps>);
87
+ }
88
+
89
+ /**
90
+ * A dependency injection container that lazily creates and caches singleton service instances.
91
+ *
92
+ * Supports both a global singleton (via {@link Container.getGlobalInstance}) and
93
+ * isolated instances (via {@link Container.create}) for test environments.
94
+ *
95
+ * @typeParam TDeps - User-defined dependency interface
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * // Production usage
100
+ * const container = Container.create<ContainerDeps>({ db, logger });
101
+ * const auth = container.get(AuthService);
102
+ *
103
+ * // Test usage (isolated)
104
+ * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });
105
+ * testContainer.register(UserService, mockUserService);
106
+ * const auth = testContainer.get(AuthService);
107
+ * ```
108
+ */
109
+ declare class Container<TDeps = unknown> implements IContainer {
110
+ private static globalInstance;
111
+ private instances;
112
+ private deps;
113
+ private config;
114
+ private constructor();
115
+ /**
116
+ * Create a new container instance.
117
+ *
118
+ * Accepts either a plain deps object or a {@link ContainerConfig} with
119
+ * additional options like `trace`.
120
+ * When called with no arguments, the container defers resolution
121
+ * until {@link setDeps} is called.
122
+ *
123
+ * @typeParam TDeps - User-defined dependency interface
124
+ * @param depsOrConfig - Dependencies or full configuration object
125
+ * @returns A new isolated container instance
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * // Simple — pass deps directly
130
+ * const container = Container.create<ContainerDeps>({ db, logger });
131
+ *
132
+ * // With config — pass options alongside deps
133
+ * const container = Container.create<ContainerDeps>({
134
+ * deps: { db, logger },
135
+ * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),
136
+ * });
137
+ *
138
+ * // Lazy — set deps later
139
+ * const container = Container.create<ContainerDeps>();
140
+ * container.setDeps({ db, logger });
141
+ * ```
142
+ */
143
+ static create<TDeps>(depsOrConfig?: TDeps | ContainerConfig<TDeps>): Container<TDeps>;
144
+ /**
145
+ * Get the global singleton container instance.
146
+ *
147
+ * Creates one on first call. Useful for module-level exports that
148
+ * need a container reference before deps are available.
149
+ *
150
+ * @typeParam TDeps - User-defined dependency interface
151
+ * @returns The global container instance
152
+ */
153
+ static getGlobalInstance<TDeps>(): Container<TDeps>;
154
+ /**
155
+ * Reset the global singleton container.
156
+ *
157
+ * Clears all cached instances and removes the global reference.
158
+ * Primarily used for test cleanup.
159
+ */
160
+ static resetGlobal(): void;
161
+ /**
162
+ * Set or replace the dependencies after container creation.
163
+ *
164
+ * Useful for lazy initialization patterns where the container
165
+ * is created before deps are available.
166
+ *
167
+ * @param deps - The user-defined dependencies
168
+ */
169
+ setDeps(deps: TDeps): void;
170
+ /**
171
+ * Resolve a service by its class constructor.
172
+ *
173
+ * On first call, lazily constructs the service with `{ ...deps, registry: this }`
174
+ * and caches the singleton. Subsequent calls return the cached instance.
175
+ *
176
+ * If the service was decorated with `@Service({ trace: true })`, all its methods
177
+ * are automatically wrapped with the configured `trace` function.
178
+ *
179
+ * @typeParam T - The service type
180
+ * @param serviceClass - The class constructor to resolve
181
+ * @returns The singleton instance
182
+ * @throws If deps have not been set
183
+ * @throws If the service has tracing enabled but no `trace` function was configured
184
+ */
185
+ get<T>(serviceClass: Constructor<T>): T;
186
+ /**
187
+ * Register a pre-built instance for a service class.
188
+ *
189
+ * The registered instance is returned by {@link get} without constructing.
190
+ * Primarily used for injecting test mocks.
191
+ *
192
+ * @typeParam T - The service type
193
+ * @param serviceClass - The class constructor to associate with the instance
194
+ * @param instance - The pre-built instance to register
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });
199
+ * testContainer.register(UserService, mockUserService);
200
+ * const auth = testContainer.get(AuthService); // uses mockUserService via @Inject
201
+ * ```
202
+ */
203
+ register<T>(serviceClass: Constructor<T>, instance: T): void;
204
+ /**
205
+ * Clear all cached service instances.
206
+ *
207
+ * After calling this, the next {@link get} call for any service
208
+ * will construct a fresh instance. Does not clear deps or config.
209
+ */
210
+ clear(): void;
211
+ }
212
+
213
+ /**
214
+ * Options for the {@link Service} decorator.
215
+ */
216
+ interface ServiceOptions {
217
+ /**
218
+ * When `true`, all methods on this service are automatically wrapped
219
+ * with the container's `trace` function after construction.
220
+ *
221
+ * Requires a `trace` function in the {@link ContainerConfig}.
222
+ *
223
+ * @defaultValue `false`
224
+ */
225
+ trace?: boolean;
226
+ }
227
+ /**
228
+ * Class decorator that marks a class as container-managed.
229
+ *
230
+ * When `trace` is enabled, the container will wrap all methods
231
+ * with tracing spans using the configured trace function.
232
+ *
233
+ * Requires `experimentalDecorators: true` in tsconfig.json.
234
+ *
235
+ * @param options - Optional configuration for the service
236
+ * @returns A class decorator
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * @Service()
241
+ * class UserService extends BaseService<ContainerDeps> {
242
+ * findById(id: string) {
243
+ * return this.deps.db.users.findUnique({ where: { id } });
244
+ * }
245
+ * }
246
+ *
247
+ * @Service({ trace: true })
248
+ * class TracedService extends BaseService<ContainerDeps> {
249
+ * // All methods auto-traced as "TracedService.methodName"
250
+ * process() { ... }
251
+ * }
252
+ * ```
253
+ */
254
+ declare function Service(options?: ServiceOptions): ClassDecorator;
255
+ /**
256
+ * Property decorator for declarative inter-service dependency injection.
257
+ *
258
+ * Defines a lazy getter that resolves the dependency from the same container
259
+ * on first access and caches it per-instance. Uses a factory function
260
+ * (`() => X` instead of `X` directly) to avoid circular import issues
261
+ * between service files.
262
+ *
263
+ * Requires `experimentalDecorators: true` in tsconfig.json.
264
+ *
265
+ * @typeParam T - The injected service type
266
+ * @param factory - A factory function returning the service class constructor.
267
+ * Called lazily on first property access.
268
+ * @returns A property decorator
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * @Service()
273
+ * class AuthService extends BaseService<ContainerDeps> {
274
+ * @Inject(() => UserService)
275
+ * declare readonly userService: UserService;
276
+ *
277
+ * @Inject(() => TokenService)
278
+ * declare readonly tokenService: TokenService;
279
+ *
280
+ * authenticate(token: string) {
281
+ * const payload = this.tokenService.verify(token);
282
+ * return this.userService.findById(payload.userId);
283
+ * }
284
+ * }
285
+ * ```
286
+ */
287
+ declare function Inject<T>(factory: () => Constructor<T>): (_target: object, propertyKey: string | symbol) => void;
288
+
289
+ /**
290
+ * Walk the prototype chain from `proto` up to (but not including) `stopAt`,
291
+ * looking for a property descriptor with the given key.
292
+ *
293
+ * @param proto - The prototype to start searching from
294
+ * @param key - The property name to look for
295
+ * @param stopAt - The prototype to stop at (exclusive)
296
+ * @returns The property descriptor if found, or `undefined`
297
+ */
298
+ declare function getPropertyDescriptorFromChain(proto: object, key: string, stopAt: object): PropertyDescriptor | undefined;
299
+ /**
300
+ * Wrap all methods on an instance with a tracing function.
301
+ *
302
+ * Walks the prototype chain from the instance up to `stopAt`, wrapping
303
+ * every method (functions with a `value` descriptor) with the `trace` callback.
304
+ * Getters, setters, and the constructor are skipped. When a method exists on
305
+ * multiple prototypes in the chain, the closest (most derived) version wins.
306
+ *
307
+ * Span names follow the `ClassName.methodName` format.
308
+ *
309
+ * @param instance - The object instance whose methods will be wrapped
310
+ * @param trace - The tracing function that wraps each method call
311
+ * @param stopAt - The prototype to stop walking at (typically `BaseService.prototype`)
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * wrapWithTracing(instance, (spanName, fn) => {
316
+ * console.log(`>> ${spanName}`);
317
+ * const result = fn();
318
+ * console.log(`<< ${spanName}`);
319
+ * return result;
320
+ * }, BaseService.prototype);
321
+ * ```
322
+ */
323
+ declare function wrapWithTracing(instance: object, trace: TraceFn, stopAt: object): void;
324
+
325
+ export { BaseService, type Constructor, Container, type ContainerConfig, type IContainer, Inject, Service, type ServiceDependencies, type ServiceOptions, type TraceFn, getPropertyDescriptorFromChain, wrapWithTracing };
@@ -0,0 +1,325 @@
1
+ /** A class constructor type. */
2
+ type Constructor<T = unknown> = new (...args: any[]) => T;
3
+ /** A pluggable tracing function signature. */
4
+ type TraceFn = (spanName: string, fn: () => unknown) => unknown;
5
+ /**
6
+ * Minimal interface representing a DI container.
7
+ *
8
+ * Services receive this as `this.registry` to resolve other services
9
+ * without coupling to the full {@link Container} implementation.
10
+ */
11
+ interface IContainer {
12
+ /**
13
+ * Resolve a service by its class constructor.
14
+ *
15
+ * @typeParam T - The service type
16
+ * @param serviceClass - The class constructor to resolve
17
+ * @returns The singleton instance of the service
18
+ */
19
+ get<T>(serviceClass: Constructor<T>): T;
20
+ }
21
+ /**
22
+ * The full set of dependencies passed to a service constructor.
23
+ *
24
+ * Combines the user-defined `TDeps` with the container's `registry`,
25
+ * which is injected automatically by the container.
26
+ *
27
+ * @typeParam TDeps - User-defined dependency interface
28
+ */
29
+ type ServiceDependencies<TDeps> = TDeps & {
30
+ registry: IContainer;
31
+ };
32
+ /**
33
+ * Configuration object for {@link Container.create}.
34
+ *
35
+ * @typeParam TDeps - User-defined dependency interface
36
+ */
37
+ interface ContainerConfig<TDeps> {
38
+ /** The user-defined dependencies to inject into services. */
39
+ deps?: TDeps;
40
+ /**
41
+ * Optional tracing function for services decorated with `@Service({ trace: true })`.
42
+ * When a traced service is resolved, all its methods are wrapped with this function.
43
+ *
44
+ * @param spanName - The span name in `ClassName.methodName` format
45
+ * @param fn - The original method to execute within the span
46
+ * @returns The return value of `fn`
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const container = Container.create<ContainerDeps>({
51
+ * deps: { db, logger },
52
+ * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),
53
+ * });
54
+ * ```
55
+ */
56
+ trace?: TraceFn;
57
+ }
58
+ /**
59
+ * Abstract base class that all container-managed services extend.
60
+ *
61
+ * Receives injected dependencies via its constructor and exposes them
62
+ * as `this.deps` (user-defined) and `this.registry` (the container).
63
+ *
64
+ * @typeParam TDeps - User-defined dependency interface
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * interface ContainerDeps {
69
+ * db: DatabaseClient;
70
+ * logger: Logger;
71
+ * }
72
+ *
73
+ * @Service()
74
+ * class UserService extends BaseService<ContainerDeps> {
75
+ * findById(id: string) {
76
+ * return this.deps.db.users.findUnique({ where: { id } });
77
+ * }
78
+ * }
79
+ * ```
80
+ */
81
+ declare abstract class BaseService<TDeps = unknown> {
82
+ /** The user-defined dependencies (everything except `registry`). */
83
+ protected readonly deps: TDeps;
84
+ /** The container instance, used internally by {@link Inject} to resolve other services. */
85
+ protected readonly registry: IContainer;
86
+ constructor(dependencies: ServiceDependencies<TDeps>);
87
+ }
88
+
89
+ /**
90
+ * A dependency injection container that lazily creates and caches singleton service instances.
91
+ *
92
+ * Supports both a global singleton (via {@link Container.getGlobalInstance}) and
93
+ * isolated instances (via {@link Container.create}) for test environments.
94
+ *
95
+ * @typeParam TDeps - User-defined dependency interface
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * // Production usage
100
+ * const container = Container.create<ContainerDeps>({ db, logger });
101
+ * const auth = container.get(AuthService);
102
+ *
103
+ * // Test usage (isolated)
104
+ * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });
105
+ * testContainer.register(UserService, mockUserService);
106
+ * const auth = testContainer.get(AuthService);
107
+ * ```
108
+ */
109
+ declare class Container<TDeps = unknown> implements IContainer {
110
+ private static globalInstance;
111
+ private instances;
112
+ private deps;
113
+ private config;
114
+ private constructor();
115
+ /**
116
+ * Create a new container instance.
117
+ *
118
+ * Accepts either a plain deps object or a {@link ContainerConfig} with
119
+ * additional options like `trace`.
120
+ * When called with no arguments, the container defers resolution
121
+ * until {@link setDeps} is called.
122
+ *
123
+ * @typeParam TDeps - User-defined dependency interface
124
+ * @param depsOrConfig - Dependencies or full configuration object
125
+ * @returns A new isolated container instance
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * // Simple — pass deps directly
130
+ * const container = Container.create<ContainerDeps>({ db, logger });
131
+ *
132
+ * // With config — pass options alongside deps
133
+ * const container = Container.create<ContainerDeps>({
134
+ * deps: { db, logger },
135
+ * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),
136
+ * });
137
+ *
138
+ * // Lazy — set deps later
139
+ * const container = Container.create<ContainerDeps>();
140
+ * container.setDeps({ db, logger });
141
+ * ```
142
+ */
143
+ static create<TDeps>(depsOrConfig?: TDeps | ContainerConfig<TDeps>): Container<TDeps>;
144
+ /**
145
+ * Get the global singleton container instance.
146
+ *
147
+ * Creates one on first call. Useful for module-level exports that
148
+ * need a container reference before deps are available.
149
+ *
150
+ * @typeParam TDeps - User-defined dependency interface
151
+ * @returns The global container instance
152
+ */
153
+ static getGlobalInstance<TDeps>(): Container<TDeps>;
154
+ /**
155
+ * Reset the global singleton container.
156
+ *
157
+ * Clears all cached instances and removes the global reference.
158
+ * Primarily used for test cleanup.
159
+ */
160
+ static resetGlobal(): void;
161
+ /**
162
+ * Set or replace the dependencies after container creation.
163
+ *
164
+ * Useful for lazy initialization patterns where the container
165
+ * is created before deps are available.
166
+ *
167
+ * @param deps - The user-defined dependencies
168
+ */
169
+ setDeps(deps: TDeps): void;
170
+ /**
171
+ * Resolve a service by its class constructor.
172
+ *
173
+ * On first call, lazily constructs the service with `{ ...deps, registry: this }`
174
+ * and caches the singleton. Subsequent calls return the cached instance.
175
+ *
176
+ * If the service was decorated with `@Service({ trace: true })`, all its methods
177
+ * are automatically wrapped with the configured `trace` function.
178
+ *
179
+ * @typeParam T - The service type
180
+ * @param serviceClass - The class constructor to resolve
181
+ * @returns The singleton instance
182
+ * @throws If deps have not been set
183
+ * @throws If the service has tracing enabled but no `trace` function was configured
184
+ */
185
+ get<T>(serviceClass: Constructor<T>): T;
186
+ /**
187
+ * Register a pre-built instance for a service class.
188
+ *
189
+ * The registered instance is returned by {@link get} without constructing.
190
+ * Primarily used for injecting test mocks.
191
+ *
192
+ * @typeParam T - The service type
193
+ * @param serviceClass - The class constructor to associate with the instance
194
+ * @param instance - The pre-built instance to register
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });
199
+ * testContainer.register(UserService, mockUserService);
200
+ * const auth = testContainer.get(AuthService); // uses mockUserService via @Inject
201
+ * ```
202
+ */
203
+ register<T>(serviceClass: Constructor<T>, instance: T): void;
204
+ /**
205
+ * Clear all cached service instances.
206
+ *
207
+ * After calling this, the next {@link get} call for any service
208
+ * will construct a fresh instance. Does not clear deps or config.
209
+ */
210
+ clear(): void;
211
+ }
212
+
213
+ /**
214
+ * Options for the {@link Service} decorator.
215
+ */
216
+ interface ServiceOptions {
217
+ /**
218
+ * When `true`, all methods on this service are automatically wrapped
219
+ * with the container's `trace` function after construction.
220
+ *
221
+ * Requires a `trace` function in the {@link ContainerConfig}.
222
+ *
223
+ * @defaultValue `false`
224
+ */
225
+ trace?: boolean;
226
+ }
227
+ /**
228
+ * Class decorator that marks a class as container-managed.
229
+ *
230
+ * When `trace` is enabled, the container will wrap all methods
231
+ * with tracing spans using the configured trace function.
232
+ *
233
+ * Requires `experimentalDecorators: true` in tsconfig.json.
234
+ *
235
+ * @param options - Optional configuration for the service
236
+ * @returns A class decorator
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * @Service()
241
+ * class UserService extends BaseService<ContainerDeps> {
242
+ * findById(id: string) {
243
+ * return this.deps.db.users.findUnique({ where: { id } });
244
+ * }
245
+ * }
246
+ *
247
+ * @Service({ trace: true })
248
+ * class TracedService extends BaseService<ContainerDeps> {
249
+ * // All methods auto-traced as "TracedService.methodName"
250
+ * process() { ... }
251
+ * }
252
+ * ```
253
+ */
254
+ declare function Service(options?: ServiceOptions): ClassDecorator;
255
+ /**
256
+ * Property decorator for declarative inter-service dependency injection.
257
+ *
258
+ * Defines a lazy getter that resolves the dependency from the same container
259
+ * on first access and caches it per-instance. Uses a factory function
260
+ * (`() => X` instead of `X` directly) to avoid circular import issues
261
+ * between service files.
262
+ *
263
+ * Requires `experimentalDecorators: true` in tsconfig.json.
264
+ *
265
+ * @typeParam T - The injected service type
266
+ * @param factory - A factory function returning the service class constructor.
267
+ * Called lazily on first property access.
268
+ * @returns A property decorator
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * @Service()
273
+ * class AuthService extends BaseService<ContainerDeps> {
274
+ * @Inject(() => UserService)
275
+ * declare readonly userService: UserService;
276
+ *
277
+ * @Inject(() => TokenService)
278
+ * declare readonly tokenService: TokenService;
279
+ *
280
+ * authenticate(token: string) {
281
+ * const payload = this.tokenService.verify(token);
282
+ * return this.userService.findById(payload.userId);
283
+ * }
284
+ * }
285
+ * ```
286
+ */
287
+ declare function Inject<T>(factory: () => Constructor<T>): (_target: object, propertyKey: string | symbol) => void;
288
+
289
+ /**
290
+ * Walk the prototype chain from `proto` up to (but not including) `stopAt`,
291
+ * looking for a property descriptor with the given key.
292
+ *
293
+ * @param proto - The prototype to start searching from
294
+ * @param key - The property name to look for
295
+ * @param stopAt - The prototype to stop at (exclusive)
296
+ * @returns The property descriptor if found, or `undefined`
297
+ */
298
+ declare function getPropertyDescriptorFromChain(proto: object, key: string, stopAt: object): PropertyDescriptor | undefined;
299
+ /**
300
+ * Wrap all methods on an instance with a tracing function.
301
+ *
302
+ * Walks the prototype chain from the instance up to `stopAt`, wrapping
303
+ * every method (functions with a `value` descriptor) with the `trace` callback.
304
+ * Getters, setters, and the constructor are skipped. When a method exists on
305
+ * multiple prototypes in the chain, the closest (most derived) version wins.
306
+ *
307
+ * Span names follow the `ClassName.methodName` format.
308
+ *
309
+ * @param instance - The object instance whose methods will be wrapped
310
+ * @param trace - The tracing function that wraps each method call
311
+ * @param stopAt - The prototype to stop walking at (typically `BaseService.prototype`)
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * wrapWithTracing(instance, (spanName, fn) => {
316
+ * console.log(`>> ${spanName}`);
317
+ * const result = fn();
318
+ * console.log(`<< ${spanName}`);
319
+ * return result;
320
+ * }, BaseService.prototype);
321
+ * ```
322
+ */
323
+ declare function wrapWithTracing(instance: object, trace: TraceFn, stopAt: object): void;
324
+
325
+ export { BaseService, type Constructor, Container, type ContainerConfig, type IContainer, Inject, Service, type ServiceDependencies, type ServiceOptions, type TraceFn, getPropertyDescriptorFromChain, wrapWithTracing };
package/dist/index.js ADDED
@@ -0,0 +1,234 @@
1
+ // src/base.ts
2
+ var BaseService = class {
3
+ /** The user-defined dependencies (everything except `registry`). */
4
+ deps;
5
+ /** The container instance, used internally by {@link Inject} to resolve other services. */
6
+ registry;
7
+ constructor(dependencies) {
8
+ const { registry, ...rest } = dependencies;
9
+ this.deps = rest;
10
+ this.registry = registry;
11
+ }
12
+ };
13
+
14
+ // src/decorators.ts
15
+ var TRACED_MARKER = /* @__PURE__ */ Symbol("__service_traced__");
16
+ function Service(options) {
17
+ return (target) => {
18
+ if (options?.trace) {
19
+ target[TRACED_MARKER] = true;
20
+ }
21
+ return target;
22
+ };
23
+ }
24
+ function Inject(factory) {
25
+ return (_target, propertyKey) => {
26
+ const cacheKey = /* @__PURE__ */ Symbol(`__inject_${String(propertyKey)}`);
27
+ Object.defineProperty(_target, propertyKey, {
28
+ get() {
29
+ const cached = this[cacheKey];
30
+ if (cached !== void 0) return cached;
31
+ const resolved = this.registry.get(factory());
32
+ this[cacheKey] = resolved;
33
+ return resolved;
34
+ },
35
+ enumerable: true,
36
+ configurable: true
37
+ });
38
+ };
39
+ }
40
+
41
+ // src/helpers.ts
42
+ function getPropertyDescriptorFromChain(proto, key, stopAt) {
43
+ let current = proto;
44
+ while (current && current !== stopAt) {
45
+ const desc = Object.getOwnPropertyDescriptor(current, key);
46
+ if (desc) return desc;
47
+ current = Object.getPrototypeOf(current);
48
+ }
49
+ return void 0;
50
+ }
51
+ function wrapWithTracing(instance, trace, stopAt) {
52
+ const className = instance.constructor.name;
53
+ const seen = /* @__PURE__ */ new Set();
54
+ let proto = Object.getPrototypeOf(instance);
55
+ while (proto && proto !== stopAt) {
56
+ for (const key of Object.getOwnPropertyNames(proto)) {
57
+ if (key === "constructor" || seen.has(key)) continue;
58
+ seen.add(key);
59
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
60
+ if (!desc || !desc.value || typeof desc.value !== "function") continue;
61
+ const original = desc.value;
62
+ const spanName = `${className}.${key}`;
63
+ instance[key] = function(...args) {
64
+ return trace(spanName, () => original.apply(this, args));
65
+ };
66
+ }
67
+ proto = Object.getPrototypeOf(proto);
68
+ }
69
+ }
70
+
71
+ // src/container.ts
72
+ var Container = class _Container {
73
+ static globalInstance;
74
+ instances = /* @__PURE__ */ new Map();
75
+ deps;
76
+ config;
77
+ constructor(config = {}) {
78
+ this.config = config;
79
+ this.deps = config.deps;
80
+ }
81
+ /**
82
+ * Create a new container instance.
83
+ *
84
+ * Accepts either a plain deps object or a {@link ContainerConfig} with
85
+ * additional options like `trace`.
86
+ * When called with no arguments, the container defers resolution
87
+ * until {@link setDeps} is called.
88
+ *
89
+ * @typeParam TDeps - User-defined dependency interface
90
+ * @param depsOrConfig - Dependencies or full configuration object
91
+ * @returns A new isolated container instance
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * // Simple — pass deps directly
96
+ * const container = Container.create<ContainerDeps>({ db, logger });
97
+ *
98
+ * // With config — pass options alongside deps
99
+ * const container = Container.create<ContainerDeps>({
100
+ * deps: { db, logger },
101
+ * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),
102
+ * });
103
+ *
104
+ * // Lazy — set deps later
105
+ * const container = Container.create<ContainerDeps>();
106
+ * container.setDeps({ db, logger });
107
+ * ```
108
+ */
109
+ static create(depsOrConfig) {
110
+ if (depsOrConfig && typeof depsOrConfig === "object" && ("deps" in depsOrConfig || "trace" in depsOrConfig)) {
111
+ return new _Container(depsOrConfig);
112
+ }
113
+ return new _Container({
114
+ deps: depsOrConfig
115
+ });
116
+ }
117
+ /**
118
+ * Get the global singleton container instance.
119
+ *
120
+ * Creates one on first call. Useful for module-level exports that
121
+ * need a container reference before deps are available.
122
+ *
123
+ * @typeParam TDeps - User-defined dependency interface
124
+ * @returns The global container instance
125
+ */
126
+ static getGlobalInstance() {
127
+ if (!_Container.globalInstance) {
128
+ _Container.globalInstance = new _Container();
129
+ }
130
+ return _Container.globalInstance;
131
+ }
132
+ /**
133
+ * Reset the global singleton container.
134
+ *
135
+ * Clears all cached instances and removes the global reference.
136
+ * Primarily used for test cleanup.
137
+ */
138
+ static resetGlobal() {
139
+ _Container.globalInstance?.clear();
140
+ _Container.globalInstance = void 0;
141
+ }
142
+ /**
143
+ * Set or replace the dependencies after container creation.
144
+ *
145
+ * Useful for lazy initialization patterns where the container
146
+ * is created before deps are available.
147
+ *
148
+ * @param deps - The user-defined dependencies
149
+ */
150
+ setDeps(deps) {
151
+ this.deps = deps;
152
+ }
153
+ /**
154
+ * Resolve a service by its class constructor.
155
+ *
156
+ * On first call, lazily constructs the service with `{ ...deps, registry: this }`
157
+ * and caches the singleton. Subsequent calls return the cached instance.
158
+ *
159
+ * If the service was decorated with `@Service({ trace: true })`, all its methods
160
+ * are automatically wrapped with the configured `trace` function.
161
+ *
162
+ * @typeParam T - The service type
163
+ * @param serviceClass - The class constructor to resolve
164
+ * @returns The singleton instance
165
+ * @throws If deps have not been set
166
+ * @throws If the service has tracing enabled but no `trace` function was configured
167
+ */
168
+ get(serviceClass) {
169
+ const existing = this.instances.get(serviceClass);
170
+ if (existing) return existing;
171
+ if (!this.deps) {
172
+ throw new Error(
173
+ "Container deps not set. Call setDeps() or pass deps to Container.create()."
174
+ );
175
+ }
176
+ const serviceDeps = {
177
+ ...this.deps,
178
+ registry: this
179
+ };
180
+ const instance = new serviceClass(serviceDeps);
181
+ if (serviceClass[TRACED_MARKER]) {
182
+ if (!this.config.trace) {
183
+ throw new Error(
184
+ `Service "${serviceClass.name}" has trace enabled but no trace function was provided. Pass { trace } to Container.create().`
185
+ );
186
+ }
187
+ wrapWithTracing(
188
+ instance,
189
+ this.config.trace,
190
+ BaseService.prototype
191
+ );
192
+ }
193
+ this.instances.set(serviceClass, instance);
194
+ return instance;
195
+ }
196
+ /**
197
+ * Register a pre-built instance for a service class.
198
+ *
199
+ * The registered instance is returned by {@link get} without constructing.
200
+ * Primarily used for injecting test mocks.
201
+ *
202
+ * @typeParam T - The service type
203
+ * @param serviceClass - The class constructor to associate with the instance
204
+ * @param instance - The pre-built instance to register
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });
209
+ * testContainer.register(UserService, mockUserService);
210
+ * const auth = testContainer.get(AuthService); // uses mockUserService via @Inject
211
+ * ```
212
+ */
213
+ register(serviceClass, instance) {
214
+ this.instances.set(serviceClass, instance);
215
+ }
216
+ /**
217
+ * Clear all cached service instances.
218
+ *
219
+ * After calling this, the next {@link get} call for any service
220
+ * will construct a fresh instance. Does not clear deps or config.
221
+ */
222
+ clear() {
223
+ this.instances.clear();
224
+ }
225
+ };
226
+ export {
227
+ BaseService,
228
+ Container,
229
+ Inject,
230
+ Service,
231
+ getPropertyDescriptorFromChain,
232
+ wrapWithTracing
233
+ };
234
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/base.ts","../src/decorators.ts","../src/helpers.ts","../src/container.ts"],"sourcesContent":["/** A class constructor type. */\n// biome-ignore lint/suspicious/noExplicitAny: Constructor must accept any args to be generic over all classes\nexport type Constructor<T = unknown> = new (...args: any[]) => T;\n\n/** A pluggable tracing function signature. */\nexport type TraceFn = (spanName: string, fn: () => unknown) => unknown;\n\n/**\n * Minimal interface representing a DI container.\n *\n * Services receive this as `this.registry` to resolve other services\n * without coupling to the full {@link Container} implementation.\n */\nexport interface IContainer {\n /**\n * Resolve a service by its class constructor.\n *\n * @typeParam T - The service type\n * @param serviceClass - The class constructor to resolve\n * @returns The singleton instance of the service\n */\n get<T>(serviceClass: Constructor<T>): T;\n}\n\n/**\n * The full set of dependencies passed to a service constructor.\n *\n * Combines the user-defined `TDeps` with the container's `registry`,\n * which is injected automatically by the container.\n *\n * @typeParam TDeps - User-defined dependency interface\n */\nexport type ServiceDependencies<TDeps> = TDeps & { registry: IContainer };\n\n/**\n * Configuration object for {@link Container.create}.\n *\n * @typeParam TDeps - User-defined dependency interface\n */\nexport interface ContainerConfig<TDeps> {\n /** The user-defined dependencies to inject into services. */\n deps?: TDeps;\n\n /**\n * Optional tracing function for services decorated with `@Service({ trace: true })`.\n * When a traced service is resolved, all its methods are wrapped with this function.\n *\n * @param spanName - The span name in `ClassName.methodName` format\n * @param fn - The original method to execute within the span\n * @returns The return value of `fn`\n *\n * @example\n * ```ts\n * const container = Container.create<ContainerDeps>({\n * deps: { db, logger },\n * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),\n * });\n * ```\n */\n trace?: TraceFn;\n}\n\n/**\n * Abstract base class that all container-managed services extend.\n *\n * Receives injected dependencies via its constructor and exposes them\n * as `this.deps` (user-defined) and `this.registry` (the container).\n *\n * @typeParam TDeps - User-defined dependency interface\n *\n * @example\n * ```ts\n * interface ContainerDeps {\n * db: DatabaseClient;\n * logger: Logger;\n * }\n *\n * @Service()\n * class UserService extends BaseService<ContainerDeps> {\n * findById(id: string) {\n * return this.deps.db.users.findUnique({ where: { id } });\n * }\n * }\n * ```\n */\nexport abstract class BaseService<TDeps = unknown> {\n /** The user-defined dependencies (everything except `registry`). */\n protected readonly deps: TDeps;\n\n /** The container instance, used internally by {@link Inject} to resolve other services. */\n protected readonly registry: IContainer;\n\n constructor(dependencies: ServiceDependencies<TDeps>) {\n const { registry, ...rest } = dependencies as ServiceDependencies<TDeps>;\n this.deps = rest as unknown as TDeps;\n this.registry = registry;\n }\n}\n","import type { Constructor, IContainer } from \"./base.js\";\n\n/** @internal Symbol used to mark classes decorated with `@Service({ trace: true })`. */\nexport const TRACED_MARKER = Symbol(\"__service_traced__\");\n\n/**\n * Options for the {@link Service} decorator.\n */\nexport interface ServiceOptions {\n /**\n * When `true`, all methods on this service are automatically wrapped\n * with the container's `trace` function after construction.\n *\n * Requires a `trace` function in the {@link ContainerConfig}.\n *\n * @defaultValue `false`\n */\n trace?: boolean;\n}\n\n/**\n * Class decorator that marks a class as container-managed.\n *\n * When `trace` is enabled, the container will wrap all methods\n * with tracing spans using the configured trace function.\n *\n * Requires `experimentalDecorators: true` in tsconfig.json.\n *\n * @param options - Optional configuration for the service\n * @returns A class decorator\n *\n * @example\n * ```ts\n * @Service()\n * class UserService extends BaseService<ContainerDeps> {\n * findById(id: string) {\n * return this.deps.db.users.findUnique({ where: { id } });\n * }\n * }\n *\n * @Service({ trace: true })\n * class TracedService extends BaseService<ContainerDeps> {\n * // All methods auto-traced as \"TracedService.methodName\"\n * process() { ... }\n * }\n * ```\n */\nexport function Service(options?: ServiceOptions): ClassDecorator {\n return (target) => {\n if (options?.trace) {\n (target as unknown as Record<symbol, boolean>)[TRACED_MARKER] = true;\n }\n return target;\n };\n}\n\ntype InjectTarget<T> = { registry: IContainer } & Record<symbol, T>;\n\n/**\n * Property decorator for declarative inter-service dependency injection.\n *\n * Defines a lazy getter that resolves the dependency from the same container\n * on first access and caches it per-instance. Uses a factory function\n * (`() => X` instead of `X` directly) to avoid circular import issues\n * between service files.\n *\n * Requires `experimentalDecorators: true` in tsconfig.json.\n *\n * @typeParam T - The injected service type\n * @param factory - A factory function returning the service class constructor.\n * Called lazily on first property access.\n * @returns A property decorator\n *\n * @example\n * ```ts\n * @Service()\n * class AuthService extends BaseService<ContainerDeps> {\n * @Inject(() => UserService)\n * declare readonly userService: UserService;\n *\n * @Inject(() => TokenService)\n * declare readonly tokenService: TokenService;\n *\n * authenticate(token: string) {\n * const payload = this.tokenService.verify(token);\n * return this.userService.findById(payload.userId);\n * }\n * }\n * ```\n */\nexport function Inject<T>(factory: () => Constructor<T>) {\n return (_target: object, propertyKey: string | symbol): void => {\n const cacheKey = Symbol(`__inject_${String(propertyKey)}`);\n\n Object.defineProperty(_target, propertyKey, {\n get(this: InjectTarget<T>) {\n const cached = this[cacheKey];\n if (cached !== undefined) return cached;\n const resolved = this.registry.get(factory());\n this[cacheKey] = resolved;\n return resolved;\n },\n enumerable: true,\n configurable: true,\n });\n };\n}\n","import type { TraceFn } from \"./base.js\";\n\n/**\n * Walk the prototype chain from `proto` up to (but not including) `stopAt`,\n * looking for a property descriptor with the given key.\n *\n * @param proto - The prototype to start searching from\n * @param key - The property name to look for\n * @param stopAt - The prototype to stop at (exclusive)\n * @returns The property descriptor if found, or `undefined`\n */\nexport function getPropertyDescriptorFromChain(\n proto: object,\n key: string,\n stopAt: object,\n): PropertyDescriptor | undefined {\n let current: object | null = proto;\n while (current && current !== stopAt) {\n const desc = Object.getOwnPropertyDescriptor(current, key);\n if (desc) return desc;\n current = Object.getPrototypeOf(current);\n }\n return undefined;\n}\n\n/**\n * Wrap all methods on an instance with a tracing function.\n *\n * Walks the prototype chain from the instance up to `stopAt`, wrapping\n * every method (functions with a `value` descriptor) with the `trace` callback.\n * Getters, setters, and the constructor are skipped. When a method exists on\n * multiple prototypes in the chain, the closest (most derived) version wins.\n *\n * Span names follow the `ClassName.methodName` format.\n *\n * @param instance - The object instance whose methods will be wrapped\n * @param trace - The tracing function that wraps each method call\n * @param stopAt - The prototype to stop walking at (typically `BaseService.prototype`)\n *\n * @example\n * ```ts\n * wrapWithTracing(instance, (spanName, fn) => {\n * console.log(`>> ${spanName}`);\n * const result = fn();\n * console.log(`<< ${spanName}`);\n * return result;\n * }, BaseService.prototype);\n * ```\n */\nexport function wrapWithTracing(\n instance: object,\n trace: TraceFn,\n stopAt: object,\n): void {\n const className = instance.constructor.name;\n const seen = new Set<string>();\n let proto: object | null = Object.getPrototypeOf(instance);\n\n while (proto && proto !== stopAt) {\n for (const key of Object.getOwnPropertyNames(proto)) {\n if (key === \"constructor\" || seen.has(key)) continue;\n seen.add(key);\n\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (!desc || !desc.value || typeof desc.value !== \"function\") continue;\n\n const original = desc.value as (...args: unknown[]) => unknown;\n const spanName = `${className}.${key}`;\n\n (instance as Record<string, unknown>)[key] = function (\n this: unknown,\n ...args: unknown[]\n ) {\n return trace(spanName, () => original.apply(this, args));\n };\n }\n proto = Object.getPrototypeOf(proto);\n }\n}\n","import {\n BaseService,\n type Constructor,\n type ContainerConfig,\n type IContainer,\n type ServiceDependencies,\n} from \"./base.js\";\nimport { TRACED_MARKER } from \"./decorators.js\";\nimport { wrapWithTracing } from \"./helpers.js\";\n\n/**\n * A dependency injection container that lazily creates and caches singleton service instances.\n *\n * Supports both a global singleton (via {@link Container.getGlobalInstance}) and\n * isolated instances (via {@link Container.create}) for test environments.\n *\n * @typeParam TDeps - User-defined dependency interface\n *\n * @example\n * ```ts\n * // Production usage\n * const container = Container.create<ContainerDeps>({ db, logger });\n * const auth = container.get(AuthService);\n *\n * // Test usage (isolated)\n * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });\n * testContainer.register(UserService, mockUserService);\n * const auth = testContainer.get(AuthService);\n * ```\n */\nexport class Container<TDeps = unknown> implements IContainer {\n private static globalInstance: Container<unknown> | undefined;\n\n private instances = new Map<Constructor, unknown>();\n private deps: TDeps | undefined;\n private config: ContainerConfig<TDeps>;\n\n private constructor(config: ContainerConfig<TDeps> = {}) {\n this.config = config;\n this.deps = config.deps;\n }\n\n /**\n * Create a new container instance.\n *\n * Accepts either a plain deps object or a {@link ContainerConfig} with\n * additional options like `trace`.\n * When called with no arguments, the container defers resolution\n * until {@link setDeps} is called.\n *\n * @typeParam TDeps - User-defined dependency interface\n * @param depsOrConfig - Dependencies or full configuration object\n * @returns A new isolated container instance\n *\n * @example\n * ```ts\n * // Simple — pass deps directly\n * const container = Container.create<ContainerDeps>({ db, logger });\n *\n * // With config — pass options alongside deps\n * const container = Container.create<ContainerDeps>({\n * deps: { db, logger },\n * trace: (spanName, fn) => tracer.startActiveSpan(spanName, () => fn()),\n * });\n *\n * // Lazy — set deps later\n * const container = Container.create<ContainerDeps>();\n * container.setDeps({ db, logger });\n * ```\n */\n static create<TDeps>(\n depsOrConfig?: TDeps | ContainerConfig<TDeps>,\n ): Container<TDeps> {\n if (\n depsOrConfig &&\n typeof depsOrConfig === \"object\" &&\n (\"deps\" in depsOrConfig || \"trace\" in depsOrConfig)\n ) {\n return new Container<TDeps>(depsOrConfig as ContainerConfig<TDeps>);\n }\n return new Container<TDeps>({\n deps: depsOrConfig as TDeps | undefined,\n });\n }\n\n /**\n * Get the global singleton container instance.\n *\n * Creates one on first call. Useful for module-level exports that\n * need a container reference before deps are available.\n *\n * @typeParam TDeps - User-defined dependency interface\n * @returns The global container instance\n */\n static getGlobalInstance<TDeps>(): Container<TDeps> {\n if (!Container.globalInstance) {\n Container.globalInstance = new Container<TDeps>();\n }\n return Container.globalInstance as Container<TDeps>;\n }\n\n /**\n * Reset the global singleton container.\n *\n * Clears all cached instances and removes the global reference.\n * Primarily used for test cleanup.\n */\n static resetGlobal(): void {\n Container.globalInstance?.clear();\n Container.globalInstance = undefined;\n }\n\n /**\n * Set or replace the dependencies after container creation.\n *\n * Useful for lazy initialization patterns where the container\n * is created before deps are available.\n *\n * @param deps - The user-defined dependencies\n */\n setDeps(deps: TDeps): void {\n this.deps = deps;\n }\n\n /**\n * Resolve a service by its class constructor.\n *\n * On first call, lazily constructs the service with `{ ...deps, registry: this }`\n * and caches the singleton. Subsequent calls return the cached instance.\n *\n * If the service was decorated with `@Service({ trace: true })`, all its methods\n * are automatically wrapped with the configured `trace` function.\n *\n * @typeParam T - The service type\n * @param serviceClass - The class constructor to resolve\n * @returns The singleton instance\n * @throws If deps have not been set\n * @throws If the service has tracing enabled but no `trace` function was configured\n */\n get<T>(serviceClass: Constructor<T>): T {\n const existing = this.instances.get(serviceClass);\n if (existing) return existing as T;\n\n if (!this.deps) {\n throw new Error(\n \"Container deps not set. Call setDeps() or pass deps to Container.create().\",\n );\n }\n\n const serviceDeps: ServiceDependencies<TDeps> = {\n ...this.deps,\n registry: this,\n };\n\n const instance = new serviceClass(serviceDeps);\n\n // Apply tracing if @Service({ trace: true }) was used\n if ((serviceClass as unknown as Record<symbol, unknown>)[TRACED_MARKER]) {\n if (!this.config.trace) {\n throw new Error(\n `Service \"${serviceClass.name}\" has trace enabled but no trace function was provided. Pass { trace } to Container.create().`,\n );\n }\n wrapWithTracing(\n instance as object,\n this.config.trace,\n BaseService.prototype,\n );\n }\n\n this.instances.set(serviceClass, instance);\n return instance;\n }\n\n /**\n * Register a pre-built instance for a service class.\n *\n * The registered instance is returned by {@link get} without constructing.\n * Primarily used for injecting test mocks.\n *\n * @typeParam T - The service type\n * @param serviceClass - The class constructor to associate with the instance\n * @param instance - The pre-built instance to register\n *\n * @example\n * ```ts\n * const testContainer = Container.create<ContainerDeps>({ db: mockDb, logger: mockLogger });\n * testContainer.register(UserService, mockUserService);\n * const auth = testContainer.get(AuthService); // uses mockUserService via @Inject\n * ```\n */\n register<T>(serviceClass: Constructor<T>, instance: T): void {\n this.instances.set(serviceClass, instance);\n }\n\n /**\n * Clear all cached service instances.\n *\n * After calling this, the next {@link get} call for any service\n * will construct a fresh instance. Does not clear deps or config.\n */\n clear(): void {\n this.instances.clear();\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@half0wl/container",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight decorator-based dependency injection container for TypeScript services",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "typecheck": "tsc -p tsconfig.check.json",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "dependency-injection",
33
+ "di",
34
+ "container",
35
+ "typescript",
36
+ "decorators",
37
+ "singleton"
38
+ ],
39
+ "license": "MIT",
40
+ "devDependencies": {
41
+ "tsup": "^8.0.0",
42
+ "typescript": "^5.4.0",
43
+ "vitest": "^3.0.0"
44
+ }
45
+ }