getbox 0.2.0 → 1.1.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/CONTEXT.md ADDED
@@ -0,0 +1,123 @@
1
+ # getbox/context
2
+
3
+ `getbox/context` provides an alternative pattern where the box is available implicitly via `AsyncLocalStorage`. Classes can resolve dependencies directly in their constructors without needing `static init` methods or passing the box around.
4
+
5
+ ## Setup
6
+
7
+ Wrap your application entry point with `withBox` to create a scoped box.
8
+
9
+ ```ts
10
+ import { withBox } from "getbox/context";
11
+
12
+ withBox(() => {
13
+ // All code in this scope can resolve dependencies
14
+ const app = resolve(App);
15
+ app.start();
16
+ });
17
+ ```
18
+
19
+ You can also pass an existing box to the scope.
20
+
21
+ ```ts
22
+ import { Box } from "getbox";
23
+ import { withBox } from "getbox/context";
24
+
25
+ const box = new Box();
26
+ Box.mock(box, LoggerFactory, new TestLogger());
27
+
28
+ withBox(box, () => {
29
+ const app = resolve(App);
30
+ app.start();
31
+ });
32
+ ```
33
+
34
+ ## Resolving dependencies
35
+
36
+ With the context pattern, classes can use `resolve` directly as field initializers. No `static init` method is needed.
37
+
38
+ ```ts
39
+ import { resolve } from "getbox/context";
40
+
41
+ class UserService {
42
+ public db = resolve(Database);
43
+ public logger = resolve(LoggerFactory);
44
+
45
+ createUser(name: string) {
46
+ this.logger.log(`Creating user: ${name}`);
47
+ return this.db.query("INSERT INTO users ...");
48
+ }
49
+ }
50
+ ```
51
+
52
+ Compare this with the base pattern which requires `static init`:
53
+
54
+ ```ts
55
+ import { Box } from "getbox";
56
+
57
+ class UserService {
58
+ constructor(private db: Database, private logger: Logger) {}
59
+
60
+ static init(box: Box) {
61
+ return box.for(UserService).get(Database, LoggerFactory);
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Resolving multiple dependencies
67
+
68
+ Use `resolveAll` to resolve multiple constructors at once. Accepts an object or array of constructors.
69
+
70
+ ```ts
71
+ withBox(() => {
72
+ // Object form
73
+ const { db, logger } = resolveAll({ db: Database, logger: LoggerFactory });
74
+
75
+ // Array form
76
+ const [db2, logger2] = resolveAll([Database, LoggerFactory]);
77
+
78
+ console.log(db === db2); // true (cached)
79
+ console.log(logger === logger2); // true (cached)
80
+ });
81
+ ```
82
+
83
+ ## Resolving constructor parameters
84
+
85
+ Use `construct` to create a class instance with resolved constructor parameters.
86
+
87
+ ```ts
88
+ class UserService {
89
+ constructor(private db: Database, private logger: Logger) {}
90
+ }
91
+
92
+ withBox(() => {
93
+ const service = construct(UserService).get(Database, LoggerFactory);
94
+ service.createUser("Alice");
95
+ });
96
+ ```
97
+
98
+ ## Accessing the box
99
+
100
+ Use `useBox()` to get the current box from the scope. This is useful when you need full access to the box API.
101
+
102
+ ```ts
103
+ withBox(() => {
104
+ const box = useBox();
105
+ const db = box.new(Database);
106
+ });
107
+ ```
108
+
109
+ ## Nested and concurrent scopes
110
+
111
+ Each `withBox` call creates an independent scope. Nested scopes do not share the parent box, and concurrent async scopes are isolated from each other.
112
+
113
+ ```ts
114
+ withBox(() => {
115
+ const db = resolve(Database);
116
+
117
+ // Inner scope gets a fresh box
118
+ withBox(() => {
119
+ const db2 = resolve(Database);
120
+ console.log(db === db2); // false (different scope)
121
+ });
122
+ });
123
+ ```
package/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  ### Lightweight dependency injection for TypeScript.
4
4
 
5
- `getbox` provides a simple way of managing dependencies in TypeScript applications. It uses classes and factory functions to define dependencies and automatically handles instance caching.
5
+ `getbox` provides a simple way of managing dependencies in TypeScript applications. Dependencies are defined as constructors that act as interfaces for the values they resolve.
6
6
 
7
- The main advantage of `getbox` is removing the need to manually pass references around when instantiating classes that depend on one another.
7
+ Callers know the type of the value they need, but not how it will be derived. The box resolves constructors lazily and caches instances automatically.
8
+
9
+ For an alternative pattern using AsyncLocalStorage where classes can resolve dependencies directly in their constructors, see [getbox/context](./CONTEXT.md).
8
10
 
9
11
  ## Installation
10
12
 
@@ -22,8 +24,6 @@ Classes are instantiated once and cached. Subsequent calls return the cached ins
22
24
 
23
25
  ```ts
24
26
  // printer.ts
25
- import { Box } from "getbox";
26
-
27
27
  export class Printer {
28
28
  print(text: string): string {
29
29
  return text.toUpperCase();
@@ -77,29 +77,21 @@ console.log(office.printer === printer); // true
77
77
  Use `box.new()` to create a new instance each time without caching. This is useful for instances that should not be shared.
78
78
 
79
79
  ```ts
80
- // printer.ts
80
+ // main.ts
81
81
  import { Box } from "getbox";
82
82
 
83
- export class Printer {
84
- id = Math.random();
85
-
86
- print(text: string): string {
87
- return text.toUpperCase();
83
+ class Database {
84
+ connect() {
85
+ /* ... */
88
86
  }
89
87
  }
90
- ```
91
-
92
- ```ts
93
- // main.ts
94
- import { Box } from "getbox";
95
- import { Printer } from "./printer";
96
88
 
97
89
  const box = new Box();
98
90
 
99
- const printer1 = box.new(Printer);
100
- const printer2 = box.new(Printer);
91
+ const db1 = box.new(Database);
92
+ const db2 = box.new(Database);
101
93
 
102
- console.log(printer1 === printer2); // false
94
+ console.log(db1 === db2); // false
103
95
  ```
104
96
 
105
97
  ## Factory functions
@@ -146,7 +138,7 @@ export class UserService {
146
138
 
147
139
  ## Constants
148
140
 
149
- Use the `constant` helper to register constant values without needing a factory or class.
141
+ Use the `constant` helper to register constant values without needing a factory or class. Constant values are never cached since they are already fixed.
150
142
 
151
143
  ```ts
152
144
  import { Box, constant } from "getbox";
@@ -154,7 +146,7 @@ import { Box, constant } from "getbox";
154
146
  const ApiUrl = constant("https://api.example.com");
155
147
  const Port = constant(3000);
156
148
  const Config = constant({
157
- apiUrl: "https://api.example.com",
149
+ baseUrl: "https://example.com",
158
150
  timeout: 5000,
159
151
  });
160
152
 
@@ -169,39 +161,54 @@ console.log(port); // 3000
169
161
  console.log(config.timeout); // 5000
170
162
  ```
171
163
 
172
- ## Constructing classes with dependencies
164
+ Since constructors act as interfaces, a `constant` can later be replaced with a `factory` without changing any callers.
173
165
 
174
- Use `box.for()` for a convenient way to create instances of classes that take other constructors as dependencies. The instance created with `box.for()` is not cached, but dependencies resolved with `.get()` are cached.
166
+ ```ts
167
+ const ApiUrl = factory((box: Box) => {
168
+ const config = box.get(Config);
169
+ return `${config.baseUrl}/api`;
170
+ });
171
+
172
+ const apiUrl = box.get(ApiUrl); // "https://example.com/api"
173
+ ```
174
+
175
+ ## Resolving multiple constructors
176
+
177
+ Use `box.all.get()` to resolve multiple constructors at once. Pass an object to get an object of instances, or an array to get an array of instances.
175
178
 
176
179
  ```ts
177
- // database.ts
178
- export class Database {
179
- connect() { /* ... */ }
180
- }
180
+ import { Box } from "getbox";
181
181
 
182
- // logger.ts
183
- import { Box, factory } from "getbox";
182
+ const box = new Box();
184
183
 
185
- export interface Logger {
186
- log(message: string): void;
187
- }
184
+ // Object form
185
+ const { db, logger } = box.all.get({ db: Database, logger: LoggerFactory });
188
186
 
189
- export const LoggerFactory = factory((box: Box): Logger => {
190
- return console;
191
- });
187
+ // Array form
188
+ const [db2, logger2] = box.all.get([Database, LoggerFactory]);
189
+
190
+ console.log(db === db2); // true (cached)
191
+ console.log(logger === logger2); // true (cached)
192
192
  ```
193
193
 
194
+ Use `box.all.new()` to resolve multiple constructors as transient instances.
195
+
194
196
  ```ts
195
- // service.ts
196
- import { Box } from "getbox";
197
- import { Database } from "./database";
198
- import { Logger, LoggerFactory } from "./logger";
197
+ const { db } = box.all.new({ db: Database });
198
+ const [db2] = box.all.new([Database]);
199
199
 
200
- export class UserService {
201
- constructor(
202
- private db: Database,
203
- private logger: Logger
204
- ) {}
200
+ console.log(db === db2); // false (transient)
201
+ ```
202
+
203
+ ## Class constructors
204
+
205
+ Use `box.for()` inside a class's `static init` method to resolve constructor dependencies automatically. The instance returned by the builder is cached or transient depending on whether the class is retrieved via `box.get()` or `box.new()`.
206
+
207
+ ```ts
208
+ import { Box, factory } from "getbox";
209
+
210
+ class UserService {
211
+ constructor(private db: Database, private logger: Logger) {}
205
212
 
206
213
  static init(box: Box) {
207
214
  // Create new instance with cached dependencies
@@ -210,18 +217,10 @@ export class UserService {
210
217
 
211
218
  createUser(name: string) {
212
219
  this.logger.log(`Creating user: ${name}`);
213
- // Use db to save user
214
220
  }
215
221
  }
216
- ```
217
-
218
- ```ts
219
- // main.ts
220
- import { Box } from "getbox";
221
- import { UserService } from "./service";
222
222
 
223
223
  const box = new Box();
224
-
225
224
  const service = box.get(UserService);
226
225
  service.createUser("Alice");
227
226
  ```
@@ -245,7 +244,8 @@ class MockLogger implements Logger {
245
244
  }
246
245
 
247
246
  const box = new Box();
248
- Box.mock(box, LoggerFactory, new MockLogger());
247
+ const mockLogger = new MockLogger();
248
+ Box.mock(box, LoggerFactory, mockLogger);
249
249
 
250
250
  const service = box.get(UserService);
251
251
  service.createUser("Alice");
@@ -253,6 +253,23 @@ service.createUser("Alice");
253
253
  console.log(mockLogger.messages); // ["Creating user: Alice"]
254
254
  ```
255
255
 
256
+ ## Clearing the cache
257
+
258
+ Use `Box.clear` to remove cached instances. Pass a specific constructor to clear a single entry, or omit it to clear all cached instances.
259
+
260
+ ```ts
261
+ const box = new Box();
262
+
263
+ const db = box.get(Database);
264
+
265
+ // Clear a specific constructor
266
+ Box.clear(box, Database);
267
+ console.log(box.get(Database) === db); // false (new instance)
268
+
269
+ // Clear all cached instances
270
+ Box.clear(box);
271
+ ```
272
+
256
273
  ## Circular dependencies
257
274
 
258
275
  `getbox` does not prevent circular dependencies. You should structure your code to avoid circular imports between modules.
@@ -0,0 +1,42 @@
1
+ const require_index = require('./index.cjs');
2
+ let node_async_hooks = require("node:async_hooks");
3
+
4
+ //#region src/context.ts
5
+ const storage = new node_async_hooks.AsyncLocalStorage();
6
+ function withBox(boxOrFn, fn) {
7
+ if (typeof boxOrFn === "function") return storage.run(new require_index.Box(), boxOrFn);
8
+ return storage.run(boxOrFn, fn);
9
+ }
10
+ /**
11
+ * Returns the current {@link Box} from the active {@link withBox} scope.
12
+ * Throws if called outside a scope.
13
+ */
14
+ function useBox() {
15
+ const box = storage.getStore();
16
+ if (!box) throw new Error("useBox() must be called within a withBox() scope");
17
+ return box;
18
+ }
19
+ /**
20
+ * Resolves a cached instance from the current {@link withBox} scope.
21
+ * Shorthand for `useBox().get(constructor)`.
22
+ */
23
+ function resolve(constructor) {
24
+ return useBox().get(constructor);
25
+ }
26
+ function resolveAll(constructors) {
27
+ return useBox().all.get(constructors);
28
+ }
29
+ /**
30
+ * Returns a builder for creating a class instance with resolved constructor
31
+ * parameters from the current {@link withBox} scope. Shorthand for `useBox().for(constructor)`.
32
+ */
33
+ function construct(constructor) {
34
+ return useBox().for(constructor);
35
+ }
36
+
37
+ //#endregion
38
+ exports.construct = construct;
39
+ exports.resolve = resolve;
40
+ exports.resolveAll = resolveAll;
41
+ exports.useBox = useBox;
42
+ exports.withBox = withBox;
@@ -0,0 +1,33 @@
1
+ import { Box, Construct, Constructor, ConstructorInstanceType } from "./index.cjs";
2
+
3
+ //#region src/context.d.ts
4
+
5
+ /**
6
+ * Runs a function within a scoped {@link Box} context using AsyncLocalStorage.
7
+ * Creates a fresh Box if none is provided.
8
+ */
9
+ declare function withBox<T>(fn: () => T): T;
10
+ declare function withBox<T>(box: Box, fn: () => T): T;
11
+ /**
12
+ * Returns the current {@link Box} from the active {@link withBox} scope.
13
+ * Throws if called outside a scope.
14
+ */
15
+ declare function useBox(): Box;
16
+ /**
17
+ * Resolves a cached instance from the current {@link withBox} scope.
18
+ * Shorthand for `useBox().get(constructor)`.
19
+ */
20
+ declare function resolve<T>(constructor: Constructor<T>): T;
21
+ /**
22
+ * Resolves multiple cached instances from the current {@link withBox} scope.
23
+ * Shorthand for `useBox().all.get(constructors)`.
24
+ */
25
+ declare function resolveAll<const T extends Constructor<any>[]>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
26
+ declare function resolveAll<T extends Record<string, Constructor<any>>>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
27
+ /**
28
+ * Returns a builder for creating a class instance with resolved constructor
29
+ * parameters from the current {@link withBox} scope. Shorthand for `useBox().for(constructor)`.
30
+ */
31
+ declare function construct<T extends new (...args: any) => any>(constructor: T): Construct<T>;
32
+ //#endregion
33
+ export { construct, resolve, resolveAll, useBox, withBox };
@@ -0,0 +1,33 @@
1
+ import { Box, Construct, Constructor, ConstructorInstanceType } from "./index.mjs";
2
+
3
+ //#region src/context.d.ts
4
+
5
+ /**
6
+ * Runs a function within a scoped {@link Box} context using AsyncLocalStorage.
7
+ * Creates a fresh Box if none is provided.
8
+ */
9
+ declare function withBox<T>(fn: () => T): T;
10
+ declare function withBox<T>(box: Box, fn: () => T): T;
11
+ /**
12
+ * Returns the current {@link Box} from the active {@link withBox} scope.
13
+ * Throws if called outside a scope.
14
+ */
15
+ declare function useBox(): Box;
16
+ /**
17
+ * Resolves a cached instance from the current {@link withBox} scope.
18
+ * Shorthand for `useBox().get(constructor)`.
19
+ */
20
+ declare function resolve<T>(constructor: Constructor<T>): T;
21
+ /**
22
+ * Resolves multiple cached instances from the current {@link withBox} scope.
23
+ * Shorthand for `useBox().all.get(constructors)`.
24
+ */
25
+ declare function resolveAll<const T extends Constructor<any>[]>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
26
+ declare function resolveAll<T extends Record<string, Constructor<any>>>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
27
+ /**
28
+ * Returns a builder for creating a class instance with resolved constructor
29
+ * parameters from the current {@link withBox} scope. Shorthand for `useBox().for(constructor)`.
30
+ */
31
+ declare function construct<T extends new (...args: any) => any>(constructor: T): Construct<T>;
32
+ //#endregion
33
+ export { construct, resolve, resolveAll, useBox, withBox };
@@ -0,0 +1,38 @@
1
+ import { Box } from "./index.mjs";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+
4
+ //#region src/context.ts
5
+ const storage = new AsyncLocalStorage();
6
+ function withBox(boxOrFn, fn) {
7
+ if (typeof boxOrFn === "function") return storage.run(new Box(), boxOrFn);
8
+ return storage.run(boxOrFn, fn);
9
+ }
10
+ /**
11
+ * Returns the current {@link Box} from the active {@link withBox} scope.
12
+ * Throws if called outside a scope.
13
+ */
14
+ function useBox() {
15
+ const box = storage.getStore();
16
+ if (!box) throw new Error("useBox() must be called within a withBox() scope");
17
+ return box;
18
+ }
19
+ /**
20
+ * Resolves a cached instance from the current {@link withBox} scope.
21
+ * Shorthand for `useBox().get(constructor)`.
22
+ */
23
+ function resolve(constructor) {
24
+ return useBox().get(constructor);
25
+ }
26
+ function resolveAll(constructors) {
27
+ return useBox().all.get(constructors);
28
+ }
29
+ /**
30
+ * Returns a builder for creating a class instance with resolved constructor
31
+ * parameters from the current {@link withBox} scope. Shorthand for `useBox().for(constructor)`.
32
+ */
33
+ function construct(constructor) {
34
+ return useBox().for(constructor);
35
+ }
36
+
37
+ //#endregion
38
+ export { construct, resolve, resolveAll, useBox, withBox };
package/dist/index.cjs CHANGED
@@ -1,38 +1,178 @@
1
1
 
2
2
  //#region src/index.ts
3
+ /**
4
+ * Creates a {@link Constructor} from a factory function.
5
+ * The factory receives the box as an argument, allowing it to resolve other dependencies.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const LoggerFactory = factory((box: Box): Logger => {
10
+ * return new ConsoleLogger();
11
+ * });
12
+ *
13
+ * const logger = box.get(LoggerFactory);
14
+ * ```
15
+ */
3
16
  function factory(init) {
4
17
  return { init };
5
18
  }
19
+ const constantSymbol = Symbol("Box constant");
20
+ /**
21
+ * Creates a {@link Constructor} that always resolves to the given constant value.
22
+ * Constant values are never cached since they are already fixed.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const ApiUrl = constant("https://api.example.com");
27
+ * const port = box.get(ApiUrl); // "https://api.example.com"
28
+ * ```
29
+ */
6
30
  function constant(value) {
7
- return { init: () => value };
31
+ return {
32
+ init: () => value,
33
+ [constantSymbol]: true
34
+ };
8
35
  }
36
+ /**
37
+ * Dependency injection container that resolves and caches instances from
38
+ * a {@link Constructor}.
39
+ *
40
+ * A constructor is an object with an `init(box: Box)` method,
41
+ * a class with a `static init(box: Box)` method, or a class with a
42
+ * no-argument constructor. The {@link factory} and {@link constant} helpers
43
+ * create constructors from functions and values respectively.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * class Database {
48
+ * connect() {}
49
+ * }
50
+ *
51
+ * class UserService {
52
+ * constructor(private db: Database) {}
53
+ * static init(box: Box) {
54
+ * return new UserService(box.get(Database));
55
+ * }
56
+ * }
57
+ *
58
+ * const box = new Box();
59
+ * const service = box.get(UserService);
60
+ * const db = box.get(Database);
61
+ *
62
+ * console.log(service.db === db); // true (cached)
63
+ * console.log(box.new(Database) === db); // false (transient)
64
+ * ```
65
+ */
9
66
  var Box = class {
10
67
  cache = /* @__PURE__ */ new Map();
68
+ /**
69
+ * Creates a new (transient) instance without caching. Useful for instances
70
+ * that should not be shared.
71
+ */
11
72
  new(constructor) {
12
73
  return "init" in constructor ? constructor.init(this) : new constructor();
13
74
  }
75
+ /**
76
+ * Resolves an instance from the cache, or creates and caches a new one.
77
+ * Subsequent calls with the same constructor return the cached instance.
78
+ */
14
79
  get(constructor) {
15
80
  if (this.cache.has(constructor)) return this.cache.get(constructor);
16
81
  const value = this.new(constructor);
17
- this.cache.set(constructor, value);
82
+ if (!(constantSymbol in constructor && constructor[constantSymbol])) this.cache.set(constructor, value);
18
83
  return value;
19
84
  }
85
+ /** Resolves multiple constructors at once. */
86
+ all = new BoxAll(this);
87
+ /**
88
+ * Returns a {@link Construct} builder for creating class instances by
89
+ * resolving constructors for each constructor parameter.
90
+ *
91
+ * Intended to be used inside a class's `static init` method. The instance
92
+ * returned by the builder is never cached itself, but can be cached when
93
+ * the class is retrieved via {@link Box.get} or kept transient via {@link Box.new}.
94
+ */
20
95
  for(constructor) {
21
96
  return new Construct(this, constructor);
22
97
  }
98
+ /**
99
+ * Registers a mock value in the box's cache for a given constructor.
100
+ * Useful for replacing dependencies in tests.
101
+ */
23
102
  static mock(box, constructor, value) {
24
103
  box.cache.set(constructor, value);
25
104
  }
105
+ /**
106
+ * Removes the instance from the box's cache for a given constructor.
107
+ * Removes all instances if no constructor is provided.
108
+ */
109
+ static clear(box, constructor) {
110
+ if (!constructor) return box.cache.clear();
111
+ box.cache.delete(constructor);
112
+ }
113
+ };
114
+ /** Resolves multiple constructors at once from a {@link Box}. */
115
+ var BoxAll = class {
116
+ constructor(box) {
117
+ this.box = box;
118
+ }
119
+ get(constructors) {
120
+ if (Array.isArray(constructors)) return constructors.map((c) => this.box.get(c));
121
+ const result = {};
122
+ for (const [key, constructor] of Object.entries(constructors)) result[key] = this.box.get(constructor);
123
+ return result;
124
+ }
125
+ new(constructors) {
126
+ if (Array.isArray(constructors)) return constructors.map((c) => this.box.new(c));
127
+ const result = {};
128
+ for (const [key, constructor] of Object.entries(constructors)) result[key] = this.box.new(constructor);
129
+ return result;
130
+ }
26
131
  };
132
+ /** Builder for creating class instances with constructor dependencies resolved from a {@link Box}. */
27
133
  var Construct = class {
28
134
  constructor(box, construct) {
29
135
  this.box = box;
30
136
  this.construct = construct;
31
137
  }
138
+ /**
139
+ * Resolves each dependency as a new transient instance via {@link Box.new},
140
+ * meaning dependencies are not cached or shared.
141
+ *
142
+ * The returned instance is cached or transient depending on whether the
143
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * class UserService {
148
+ * constructor(private db: Database, private logger: Logger) {}
149
+ * static init(box: Box) {
150
+ * return box.for(UserService).new(Database, LoggerFactory);
151
+ * }
152
+ * }
153
+ * ```
154
+ */
32
155
  new(...args) {
33
156
  const instances = args.map((arg) => this.box.new(arg));
34
157
  return new this.construct(...instances);
35
158
  }
159
+ /**
160
+ * Resolves each dependency as a cached instance via {@link Box.get},
161
+ * meaning dependencies are shared across the box.
162
+ *
163
+ * The returned instance is cached or transient depending on whether the
164
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * class UserService {
169
+ * constructor(private db: Database, private logger: Logger) {}
170
+ * static init(box: Box) {
171
+ * return box.for(UserService).get(Database, LoggerFactory);
172
+ * }
173
+ * }
174
+ * ```
175
+ */
36
176
  get(...args) {
37
177
  const instances = args.map((arg) => this.box.get(arg));
38
178
  return new this.construct(...instances);
@@ -41,5 +181,6 @@ var Construct = class {
41
181
 
42
182
  //#endregion
43
183
  exports.Box = Box;
184
+ exports.Construct = Construct;
44
185
  exports.constant = constant;
45
186
  exports.factory = factory;
package/dist/index.d.cts CHANGED
@@ -1,29 +1,169 @@
1
1
  //#region src/index.d.ts
2
+ /**
3
+ * A type that can be resolved by a {@link Box}. Either an object with an
4
+ * `init(box: Box)` method, a class with a `static init(box: Box)` method
5
+ * that returns an instance, or a class with a no-argument constructor.
6
+ */
2
7
  type Constructor<T> = {
3
8
  init(box: Box): T;
4
9
  } | {
5
10
  new (): T;
6
11
  };
12
+ /** Extracts the instance type from a {@link Constructor}. */
7
13
  type ConstructorInstanceType<T> = T extends Constructor<infer U> ? U : never;
14
+ /**
15
+ * Creates a {@link Constructor} from a factory function.
16
+ * The factory receives the box as an argument, allowing it to resolve other dependencies.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const LoggerFactory = factory((box: Box): Logger => {
21
+ * return new ConsoleLogger();
22
+ * });
23
+ *
24
+ * const logger = box.get(LoggerFactory);
25
+ * ```
26
+ */
8
27
  declare function factory<T>(init: (box: Box) => T): Constructor<T>;
28
+ /**
29
+ * Creates a {@link Constructor} that always resolves to the given constant value.
30
+ * Constant values are never cached since they are already fixed.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const ApiUrl = constant("https://api.example.com");
35
+ * const port = box.get(ApiUrl); // "https://api.example.com"
36
+ * ```
37
+ */
9
38
  declare function constant<const T>(value: T): Constructor<T>;
39
+ /**
40
+ * Dependency injection container that resolves and caches instances from
41
+ * a {@link Constructor}.
42
+ *
43
+ * A constructor is an object with an `init(box: Box)` method,
44
+ * a class with a `static init(box: Box)` method, or a class with a
45
+ * no-argument constructor. The {@link factory} and {@link constant} helpers
46
+ * create constructors from functions and values respectively.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * class Database {
51
+ * connect() {}
52
+ * }
53
+ *
54
+ * class UserService {
55
+ * constructor(private db: Database) {}
56
+ * static init(box: Box) {
57
+ * return new UserService(box.get(Database));
58
+ * }
59
+ * }
60
+ *
61
+ * const box = new Box();
62
+ * const service = box.get(UserService);
63
+ * const db = box.get(Database);
64
+ *
65
+ * console.log(service.db === db); // true (cached)
66
+ * console.log(box.new(Database) === db); // false (transient)
67
+ * ```
68
+ */
10
69
  declare class Box {
11
70
  private cache;
71
+ /**
72
+ * Creates a new (transient) instance without caching. Useful for instances
73
+ * that should not be shared.
74
+ */
12
75
  new<T>(constructor: Constructor<T>): T;
76
+ /**
77
+ * Resolves an instance from the cache, or creates and caches a new one.
78
+ * Subsequent calls with the same constructor return the cached instance.
79
+ */
13
80
  get<T>(constructor: Constructor<T>): T;
81
+ /** Resolves multiple constructors at once. */
82
+ readonly all: BoxAll;
83
+ /**
84
+ * Returns a {@link Construct} builder for creating class instances by
85
+ * resolving constructors for each constructor parameter.
86
+ *
87
+ * Intended to be used inside a class's `static init` method. The instance
88
+ * returned by the builder is never cached itself, but can be cached when
89
+ * the class is retrieved via {@link Box.get} or kept transient via {@link Box.new}.
90
+ */
14
91
  for<T extends ClassConstructor<any>>(constructor: T): Construct<T>;
92
+ /**
93
+ * Registers a mock value in the box's cache for a given constructor.
94
+ * Useful for replacing dependencies in tests.
95
+ */
15
96
  static mock<T, V extends T = T>(box: Box, constructor: Constructor<T>, value: V): void;
97
+ /**
98
+ * Removes the instance from the box's cache for a given constructor.
99
+ * Removes all instances if no constructor is provided.
100
+ */
101
+ static clear<T>(box: Box, constructor?: Constructor<T>): void;
16
102
  }
103
+ /** Resolves multiple constructors at once from a {@link Box}. */
104
+ declare class BoxAll {
105
+ private box;
106
+ constructor(box: Box);
107
+ /**
108
+ * Resolves each constructor as a cached instance via {@link Box.get}.
109
+ * Accepts an array or object of constructors and returns instances in the same shape.
110
+ */
111
+ get<const T extends Constructor<any>[]>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
112
+ get<T extends Record<string, Constructor<any>>>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
113
+ /**
114
+ * Resolves each constructor as a new transient instance via {@link Box.new}.
115
+ * Accepts an array or object of constructors and returns instances in the same shape.
116
+ */
117
+ new<const T extends Constructor<any>[]>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
118
+ new<T extends Record<string, Constructor<any>>>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
119
+ }
120
+ /** Builder for creating class instances with constructor dependencies resolved from a {@link Box}. */
17
121
  declare class Construct<T extends ClassConstructor<any>> {
18
122
  private box;
19
123
  private construct;
20
124
  constructor(box: Box, construct: T);
125
+ /**
126
+ * Resolves each dependency as a new transient instance via {@link Box.new},
127
+ * meaning dependencies are not cached or shared.
128
+ *
129
+ * The returned instance is cached or transient depending on whether the
130
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * class UserService {
135
+ * constructor(private db: Database, private logger: Logger) {}
136
+ * static init(box: Box) {
137
+ * return box.for(UserService).new(Database, LoggerFactory);
138
+ * }
139
+ * }
140
+ * ```
141
+ */
21
142
  new(...args: ClassConstructorArgs<T>): InstanceType<T>;
143
+ /**
144
+ * Resolves each dependency as a cached instance via {@link Box.get},
145
+ * meaning dependencies are shared across the box.
146
+ *
147
+ * The returned instance is cached or transient depending on whether the
148
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * class UserService {
153
+ * constructor(private db: Database, private logger: Logger) {}
154
+ * static init(box: Box) {
155
+ * return box.for(UserService).get(Database, LoggerFactory);
156
+ * }
157
+ * }
158
+ * ```
159
+ */
22
160
  get(...args: ClassConstructorArgs<T>): InstanceType<T>;
23
161
  }
162
+ /** A class with any constructor signature. */
24
163
  type ClassConstructor<T> = {
25
164
  new (...args: any): T;
26
165
  };
166
+ /** Maps each constructor parameter to its corresponding {@link Constructor} type. */
27
167
  type ClassConstructorArgs<T extends ClassConstructor<any>, Args = ConstructorParameters<T>> = { [K in keyof Args]: Constructor<Args[K]> };
28
168
  //#endregion
29
- export { Box, Constructor, ConstructorInstanceType, constant, factory };
169
+ export { Box, Construct, Constructor, ConstructorInstanceType, constant, factory };
package/dist/index.d.mts CHANGED
@@ -1,29 +1,169 @@
1
1
  //#region src/index.d.ts
2
+ /**
3
+ * A type that can be resolved by a {@link Box}. Either an object with an
4
+ * `init(box: Box)` method, a class with a `static init(box: Box)` method
5
+ * that returns an instance, or a class with a no-argument constructor.
6
+ */
2
7
  type Constructor<T> = {
3
8
  init(box: Box): T;
4
9
  } | {
5
10
  new (): T;
6
11
  };
12
+ /** Extracts the instance type from a {@link Constructor}. */
7
13
  type ConstructorInstanceType<T> = T extends Constructor<infer U> ? U : never;
14
+ /**
15
+ * Creates a {@link Constructor} from a factory function.
16
+ * The factory receives the box as an argument, allowing it to resolve other dependencies.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const LoggerFactory = factory((box: Box): Logger => {
21
+ * return new ConsoleLogger();
22
+ * });
23
+ *
24
+ * const logger = box.get(LoggerFactory);
25
+ * ```
26
+ */
8
27
  declare function factory<T>(init: (box: Box) => T): Constructor<T>;
28
+ /**
29
+ * Creates a {@link Constructor} that always resolves to the given constant value.
30
+ * Constant values are never cached since they are already fixed.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const ApiUrl = constant("https://api.example.com");
35
+ * const port = box.get(ApiUrl); // "https://api.example.com"
36
+ * ```
37
+ */
9
38
  declare function constant<const T>(value: T): Constructor<T>;
39
+ /**
40
+ * Dependency injection container that resolves and caches instances from
41
+ * a {@link Constructor}.
42
+ *
43
+ * A constructor is an object with an `init(box: Box)` method,
44
+ * a class with a `static init(box: Box)` method, or a class with a
45
+ * no-argument constructor. The {@link factory} and {@link constant} helpers
46
+ * create constructors from functions and values respectively.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * class Database {
51
+ * connect() {}
52
+ * }
53
+ *
54
+ * class UserService {
55
+ * constructor(private db: Database) {}
56
+ * static init(box: Box) {
57
+ * return new UserService(box.get(Database));
58
+ * }
59
+ * }
60
+ *
61
+ * const box = new Box();
62
+ * const service = box.get(UserService);
63
+ * const db = box.get(Database);
64
+ *
65
+ * console.log(service.db === db); // true (cached)
66
+ * console.log(box.new(Database) === db); // false (transient)
67
+ * ```
68
+ */
10
69
  declare class Box {
11
70
  private cache;
71
+ /**
72
+ * Creates a new (transient) instance without caching. Useful for instances
73
+ * that should not be shared.
74
+ */
12
75
  new<T>(constructor: Constructor<T>): T;
76
+ /**
77
+ * Resolves an instance from the cache, or creates and caches a new one.
78
+ * Subsequent calls with the same constructor return the cached instance.
79
+ */
13
80
  get<T>(constructor: Constructor<T>): T;
81
+ /** Resolves multiple constructors at once. */
82
+ readonly all: BoxAll;
83
+ /**
84
+ * Returns a {@link Construct} builder for creating class instances by
85
+ * resolving constructors for each constructor parameter.
86
+ *
87
+ * Intended to be used inside a class's `static init` method. The instance
88
+ * returned by the builder is never cached itself, but can be cached when
89
+ * the class is retrieved via {@link Box.get} or kept transient via {@link Box.new}.
90
+ */
14
91
  for<T extends ClassConstructor<any>>(constructor: T): Construct<T>;
92
+ /**
93
+ * Registers a mock value in the box's cache for a given constructor.
94
+ * Useful for replacing dependencies in tests.
95
+ */
15
96
  static mock<T, V extends T = T>(box: Box, constructor: Constructor<T>, value: V): void;
97
+ /**
98
+ * Removes the instance from the box's cache for a given constructor.
99
+ * Removes all instances if no constructor is provided.
100
+ */
101
+ static clear<T>(box: Box, constructor?: Constructor<T>): void;
16
102
  }
103
+ /** Resolves multiple constructors at once from a {@link Box}. */
104
+ declare class BoxAll {
105
+ private box;
106
+ constructor(box: Box);
107
+ /**
108
+ * Resolves each constructor as a cached instance via {@link Box.get}.
109
+ * Accepts an array or object of constructors and returns instances in the same shape.
110
+ */
111
+ get<const T extends Constructor<any>[]>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
112
+ get<T extends Record<string, Constructor<any>>>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
113
+ /**
114
+ * Resolves each constructor as a new transient instance via {@link Box.new}.
115
+ * Accepts an array or object of constructors and returns instances in the same shape.
116
+ */
117
+ new<const T extends Constructor<any>[]>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
118
+ new<T extends Record<string, Constructor<any>>>(constructors: T): { [K in keyof T]: ConstructorInstanceType<T[K]> };
119
+ }
120
+ /** Builder for creating class instances with constructor dependencies resolved from a {@link Box}. */
17
121
  declare class Construct<T extends ClassConstructor<any>> {
18
122
  private box;
19
123
  private construct;
20
124
  constructor(box: Box, construct: T);
125
+ /**
126
+ * Resolves each dependency as a new transient instance via {@link Box.new},
127
+ * meaning dependencies are not cached or shared.
128
+ *
129
+ * The returned instance is cached or transient depending on whether the
130
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * class UserService {
135
+ * constructor(private db: Database, private logger: Logger) {}
136
+ * static init(box: Box) {
137
+ * return box.for(UserService).new(Database, LoggerFactory);
138
+ * }
139
+ * }
140
+ * ```
141
+ */
21
142
  new(...args: ClassConstructorArgs<T>): InstanceType<T>;
143
+ /**
144
+ * Resolves each dependency as a cached instance via {@link Box.get},
145
+ * meaning dependencies are shared across the box.
146
+ *
147
+ * The returned instance is cached or transient depending on whether the
148
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * class UserService {
153
+ * constructor(private db: Database, private logger: Logger) {}
154
+ * static init(box: Box) {
155
+ * return box.for(UserService).get(Database, LoggerFactory);
156
+ * }
157
+ * }
158
+ * ```
159
+ */
22
160
  get(...args: ClassConstructorArgs<T>): InstanceType<T>;
23
161
  }
162
+ /** A class with any constructor signature. */
24
163
  type ClassConstructor<T> = {
25
164
  new (...args: any): T;
26
165
  };
166
+ /** Maps each constructor parameter to its corresponding {@link Constructor} type. */
27
167
  type ClassConstructorArgs<T extends ClassConstructor<any>, Args = ConstructorParameters<T>> = { [K in keyof Args]: Constructor<Args[K]> };
28
168
  //#endregion
29
- export { Box, Constructor, ConstructorInstanceType, constant, factory };
169
+ export { Box, Construct, Constructor, ConstructorInstanceType, constant, factory };
package/dist/index.mjs CHANGED
@@ -1,37 +1,177 @@
1
1
  //#region src/index.ts
2
+ /**
3
+ * Creates a {@link Constructor} from a factory function.
4
+ * The factory receives the box as an argument, allowing it to resolve other dependencies.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const LoggerFactory = factory((box: Box): Logger => {
9
+ * return new ConsoleLogger();
10
+ * });
11
+ *
12
+ * const logger = box.get(LoggerFactory);
13
+ * ```
14
+ */
2
15
  function factory(init) {
3
16
  return { init };
4
17
  }
18
+ const constantSymbol = Symbol("Box constant");
19
+ /**
20
+ * Creates a {@link Constructor} that always resolves to the given constant value.
21
+ * Constant values are never cached since they are already fixed.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const ApiUrl = constant("https://api.example.com");
26
+ * const port = box.get(ApiUrl); // "https://api.example.com"
27
+ * ```
28
+ */
5
29
  function constant(value) {
6
- return { init: () => value };
30
+ return {
31
+ init: () => value,
32
+ [constantSymbol]: true
33
+ };
7
34
  }
35
+ /**
36
+ * Dependency injection container that resolves and caches instances from
37
+ * a {@link Constructor}.
38
+ *
39
+ * A constructor is an object with an `init(box: Box)` method,
40
+ * a class with a `static init(box: Box)` method, or a class with a
41
+ * no-argument constructor. The {@link factory} and {@link constant} helpers
42
+ * create constructors from functions and values respectively.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * class Database {
47
+ * connect() {}
48
+ * }
49
+ *
50
+ * class UserService {
51
+ * constructor(private db: Database) {}
52
+ * static init(box: Box) {
53
+ * return new UserService(box.get(Database));
54
+ * }
55
+ * }
56
+ *
57
+ * const box = new Box();
58
+ * const service = box.get(UserService);
59
+ * const db = box.get(Database);
60
+ *
61
+ * console.log(service.db === db); // true (cached)
62
+ * console.log(box.new(Database) === db); // false (transient)
63
+ * ```
64
+ */
8
65
  var Box = class {
9
66
  cache = /* @__PURE__ */ new Map();
67
+ /**
68
+ * Creates a new (transient) instance without caching. Useful for instances
69
+ * that should not be shared.
70
+ */
10
71
  new(constructor) {
11
72
  return "init" in constructor ? constructor.init(this) : new constructor();
12
73
  }
74
+ /**
75
+ * Resolves an instance from the cache, or creates and caches a new one.
76
+ * Subsequent calls with the same constructor return the cached instance.
77
+ */
13
78
  get(constructor) {
14
79
  if (this.cache.has(constructor)) return this.cache.get(constructor);
15
80
  const value = this.new(constructor);
16
- this.cache.set(constructor, value);
81
+ if (!(constantSymbol in constructor && constructor[constantSymbol])) this.cache.set(constructor, value);
17
82
  return value;
18
83
  }
84
+ /** Resolves multiple constructors at once. */
85
+ all = new BoxAll(this);
86
+ /**
87
+ * Returns a {@link Construct} builder for creating class instances by
88
+ * resolving constructors for each constructor parameter.
89
+ *
90
+ * Intended to be used inside a class's `static init` method. The instance
91
+ * returned by the builder is never cached itself, but can be cached when
92
+ * the class is retrieved via {@link Box.get} or kept transient via {@link Box.new}.
93
+ */
19
94
  for(constructor) {
20
95
  return new Construct(this, constructor);
21
96
  }
97
+ /**
98
+ * Registers a mock value in the box's cache for a given constructor.
99
+ * Useful for replacing dependencies in tests.
100
+ */
22
101
  static mock(box, constructor, value) {
23
102
  box.cache.set(constructor, value);
24
103
  }
104
+ /**
105
+ * Removes the instance from the box's cache for a given constructor.
106
+ * Removes all instances if no constructor is provided.
107
+ */
108
+ static clear(box, constructor) {
109
+ if (!constructor) return box.cache.clear();
110
+ box.cache.delete(constructor);
111
+ }
112
+ };
113
+ /** Resolves multiple constructors at once from a {@link Box}. */
114
+ var BoxAll = class {
115
+ constructor(box) {
116
+ this.box = box;
117
+ }
118
+ get(constructors) {
119
+ if (Array.isArray(constructors)) return constructors.map((c) => this.box.get(c));
120
+ const result = {};
121
+ for (const [key, constructor] of Object.entries(constructors)) result[key] = this.box.get(constructor);
122
+ return result;
123
+ }
124
+ new(constructors) {
125
+ if (Array.isArray(constructors)) return constructors.map((c) => this.box.new(c));
126
+ const result = {};
127
+ for (const [key, constructor] of Object.entries(constructors)) result[key] = this.box.new(constructor);
128
+ return result;
129
+ }
25
130
  };
131
+ /** Builder for creating class instances with constructor dependencies resolved from a {@link Box}. */
26
132
  var Construct = class {
27
133
  constructor(box, construct) {
28
134
  this.box = box;
29
135
  this.construct = construct;
30
136
  }
137
+ /**
138
+ * Resolves each dependency as a new transient instance via {@link Box.new},
139
+ * meaning dependencies are not cached or shared.
140
+ *
141
+ * The returned instance is cached or transient depending on whether the
142
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * class UserService {
147
+ * constructor(private db: Database, private logger: Logger) {}
148
+ * static init(box: Box) {
149
+ * return box.for(UserService).new(Database, LoggerFactory);
150
+ * }
151
+ * }
152
+ * ```
153
+ */
31
154
  new(...args) {
32
155
  const instances = args.map((arg) => this.box.new(arg));
33
156
  return new this.construct(...instances);
34
157
  }
158
+ /**
159
+ * Resolves each dependency as a cached instance via {@link Box.get},
160
+ * meaning dependencies are shared across the box.
161
+ *
162
+ * The returned instance is cached or transient depending on whether the
163
+ * class is retrieved via {@link Box.get} or {@link Box.new}.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * class UserService {
168
+ * constructor(private db: Database, private logger: Logger) {}
169
+ * static init(box: Box) {
170
+ * return box.for(UserService).get(Database, LoggerFactory);
171
+ * }
172
+ * }
173
+ * ```
174
+ */
35
175
  get(...args) {
36
176
  const instances = args.map((arg) => this.box.get(arg));
37
177
  return new this.construct(...instances);
@@ -39,4 +179,4 @@ var Construct = class {
39
179
  };
40
180
 
41
181
  //#endregion
42
- export { Box, constant, factory };
182
+ export { Box, Construct, constant, factory };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getbox",
3
- "version": "0.2.0",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight dependency injection for TypeScript",
5
5
  "private": false,
6
6
  "main": "./dist/index.cjs",
@@ -16,15 +16,18 @@
16
16
  "types": "./dist/index.d.cts",
17
17
  "default": "./dist/index.cjs"
18
18
  }
19
+ },
20
+ "./context": {
21
+ "import": {
22
+ "types": "./dist/context.d.mts",
23
+ "default": "./dist/context.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./dist/context.d.cts",
27
+ "default": "./dist/context.cjs"
28
+ }
19
29
  }
20
30
  },
21
- "scripts": {
22
- "build": "tsc && tsdown src/index.ts --format esm,cjs",
23
- "release": "pnpm run build && changeset publish",
24
- "watch": "vitest",
25
- "test": "vitest run",
26
- "test:coverage": "vitest run --coverage"
27
- },
28
31
  "keywords": [
29
32
  "DI",
30
33
  "dependency injection",
@@ -44,9 +47,17 @@
44
47
  "homepage": "https://github.com/eriicafes/getbox#readme",
45
48
  "devDependencies": {
46
49
  "@changesets/cli": "^2.29.8",
50
+ "@types/node": "^25.2.3",
47
51
  "@vitest/coverage-v8": "^4.0.16",
48
52
  "tsdown": "^0.18.3",
49
53
  "typescript": "^5.9.3",
50
54
  "vitest": "^4.0.16"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc && tsdown src/index.ts src/context.ts --format esm,cjs",
58
+ "release": "pnpm run build && changeset publish",
59
+ "watch": "vitest",
60
+ "test": "vitest run",
61
+ "test:coverage": "vitest run --coverage"
51
62
  }
52
- }
63
+ }