@romikus/dimpl 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/README.md ADDED
@@ -0,0 +1,608 @@
1
+ # dimpl
2
+
3
+ Simple dependency injection for TypeScript and JavaScript.
4
+
5
+ - Works with functions and classes.
6
+ - Dependencies stay visible in plain code.
7
+ - No decorators.
8
+ - No metadata reflection.
9
+ - No class-only container model.
10
+
11
+ `dimpl` treats a function or class as the dependency key. The first time you ask for it, the container creates an instance and caches it. Every later lookup in the same container returns the same instance.
12
+
13
+ ```ts
14
+ import { dimpl } from "dimpl";
15
+
16
+ const di = dimpl();
17
+
18
+ const Config = () => ({
19
+ dbUrl: process.env.DATABASE_URL ?? "postgres://localhost/app",
20
+ });
21
+
22
+ const Db = (config = di.get(Config)) => {
23
+ return connect(config.dbUrl);
24
+ };
25
+
26
+ const Users = ({ db } = di.deps({ db: Db })) => {
27
+ return {
28
+ findById(id: string) {
29
+ return db.user.findUnique({ where: { id } });
30
+ },
31
+ };
32
+ };
33
+
34
+ const users = di.get(Users);
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ```sh
40
+ npm install @romikus/dimpl
41
+ ```
42
+
43
+ ## Motivation
44
+
45
+ With `dimpl`, dependencies are just default parameters:
46
+
47
+ ```ts
48
+ const UsersService = (
49
+ { db, logger } = di.deps({
50
+ db: Db,
51
+ logger: Logger,
52
+ }),
53
+ ) => {
54
+ return {
55
+ list() {
56
+ logger.info("Listing users");
57
+ return db.query("select * from users");
58
+ },
59
+ };
60
+ };
61
+ ```
62
+
63
+ Single dependency:
64
+
65
+ ```ts
66
+ const StripeClient = (config = di.get(Config)) => {
67
+ return new Stripe(config.stripeSecretKey);
68
+ };
69
+ ```
70
+
71
+ Class dependency:
72
+
73
+ ```ts
74
+ class Clock {
75
+ now() {
76
+ return new Date();
77
+ }
78
+ }
79
+
80
+ const Tokens = ({ clock } = di.deps({ clock: Clock })) => {
81
+ return {
82
+ issue() {
83
+ return { issuedAt: clock.now() };
84
+ },
85
+ };
86
+ };
87
+ ```
88
+
89
+ Startup side effects:
90
+
91
+ ```ts
92
+ const app = di.get(App);
93
+
94
+ di.wire(HealthController, AuthController, UsersController);
95
+
96
+ serve({ fetch: app.fetch, port: 3000 });
97
+ ```
98
+
99
+ Controllers can register routes when they are instantiated. Services can stay lazy until something needs them.
100
+
101
+ ## request-scoped lifetime
102
+
103
+ `dimpl` is designed only around a singleton runtime. Every dependency resolved by a container is cached as one instance for that container.
104
+
105
+ Disregarding of a DI library, `AsyncLocalStorage` is a much more convenient way of handling request-scoped or more granular scoped dependencies anyway.
106
+
107
+ You can use a `dimpl` singleton instance of `AsyncLocalStorage` to retrieve request-scoped dependencies from it.
108
+
109
+ For example, imagine a pino logger is request-scoped. When a request starts, you create a child logger with `userId`. Services use that child logger without becoming request-scoped themselves.
110
+
111
+ ```ts
112
+ import { AsyncLocalStorage } from "node:async_hooks";
113
+ import pino, { type Logger } from "pino";
114
+
115
+ type RequestScope = {
116
+ logger: Logger;
117
+ };
118
+
119
+ const RootLogger = () => pino();
120
+
121
+ const RequestScope = () => {
122
+ return new AsyncLocalStorage<RequestScope>();
123
+ };
124
+
125
+ const RequestLogger = (
126
+ { rootLogger, requestScope } = di.deps({
127
+ rootLogger: RootLogger,
128
+ requestScope: RequestScope,
129
+ }),
130
+ ) => {
131
+ return {
132
+ get() {
133
+ return requestScope.getStore()?.logger ?? rootLogger;
134
+ },
135
+ };
136
+ };
137
+
138
+ const OrdersService = (
139
+ { logger } = di.deps({
140
+ logger: RequestLogger,
141
+ }),
142
+ ) => {
143
+ return {
144
+ createOrder(userId: string) {
145
+ logger.get().info({ userId }, "Creating order");
146
+ // ...
147
+ },
148
+ };
149
+ };
150
+
151
+ const requestScope = di.get(RequestScope);
152
+ const rootLogger = di.get(RootLogger);
153
+ const ordersService = di.get(OrdersService);
154
+
155
+ app.use(async (req, res, next) => {
156
+ const userId = await getUserId(req);
157
+ const logger = rootLogger.child({ userId });
158
+
159
+ requestScope.run({ logger }, () => {
160
+ next();
161
+ });
162
+ });
163
+
164
+ app.post("/orders", async (req, res) => {
165
+ const userId = await getUserId(req);
166
+ ordersService.createOrder(userId);
167
+ res.sendStatus(201);
168
+ });
169
+ ```
170
+
171
+ The container still has singleton services. The request-specific value lives in `AsyncLocalStorage`, and services read it at call time.
172
+
173
+ ## Async Constructors
174
+
175
+ `dimpl` does not support async constructors in any special way for now. This keeps the library simple, and async dependency construction is rare enough that it may not be worth adding container-level machinery for it.
176
+
177
+ When a dependency must be awaited before use, do that in your app setup and seed the resolved instance with `di.set`.
178
+
179
+ For example, imagine an AMQP library requires an async connection before anything can publish messages:
180
+
181
+ ```ts
182
+ import { connect, type Channel } from "amqplib";
183
+
184
+ export const Amqp = (): Channel => {
185
+ throw new Error("amqp instance must be set in the setup");
186
+ };
187
+
188
+ export const NotificationsService = (amqp = di.get(Amqp)) => {
189
+ return {
190
+ async sendWelcomeEmail(userId: string) {
191
+ await amqp.sendToQueue("emails", Buffer.from(JSON.stringify({ type: "welcome", userId })));
192
+ },
193
+ };
194
+ };
195
+
196
+ export async function setup() {
197
+ const connection = await connect(process.env.AMQP_URL);
198
+ const channel = await connection.createChannel();
199
+
200
+ di.set(Amqp, channel);
201
+
202
+ return {
203
+ notificationsService: di.get(NotificationsService),
204
+ };
205
+ }
206
+ ```
207
+
208
+ The throwing factory is just the dependency key and type source. If setup forgets to provide the real instance, the app fails with a clear error.
209
+
210
+ ## Unit Testing
211
+
212
+ Because dependencies are normal function parameters, the simplest test does not need a container at all. Pass every dependency explicitly.
213
+
214
+ ```ts
215
+ import { describe, expect, it, vi } from "vitest";
216
+
217
+ const AuthService = (
218
+ { users, tokens } = di.deps({
219
+ users: UsersRepo,
220
+ tokens: TokenService,
221
+ }),
222
+ ) => {
223
+ return {
224
+ async login(email: string, password: string) {
225
+ const user = await users.verifyPassword(email, password);
226
+ return tokens.sign(user.id);
227
+ },
228
+ };
229
+ };
230
+
231
+ it("logs in a user", async () => {
232
+ const users = {
233
+ verifyPassword: vi.fn().mockResolvedValue({ id: "user_1" }),
234
+ };
235
+
236
+ const tokens = {
237
+ sign: vi.fn().mockReturnValue("jwt"),
238
+ };
239
+
240
+ const auth = AuthService({ users, tokens });
241
+
242
+ await expect(auth.login("a@example.com", "secret")).resolves.toBe("jwt");
243
+ expect(users.verifyPassword).toHaveBeenCalledWith("a@example.com", "secret");
244
+ expect(tokens.sign).toHaveBeenCalledWith("user_1");
245
+ });
246
+ ```
247
+
248
+ Use `di.set` when you want most of the dependency graph to stay real, but one or two dependencies should be mocked. `di.deps` will keep resolving everything normally, and the dependencies you seeded with `di.set` will be returned instead of being created.
249
+
250
+ ```ts
251
+ it("uses a real dependency graph with one mocked dependency", async () => {
252
+ const di = dimpl();
253
+
254
+ const Db = () => connect(process.env.DATABASE_URL);
255
+
256
+ const Logger = () => ({
257
+ info: vi.fn(),
258
+ });
259
+
260
+ const UsersRepo = (
261
+ { db, logger } = di.deps({
262
+ db: Db,
263
+ logger: Logger,
264
+ }),
265
+ ) => ({
266
+ async findById(id: string) {
267
+ logger.info(`Loading user ${id}`);
268
+ return db.users.findById(id);
269
+ },
270
+ });
271
+
272
+ const fakeDb = {
273
+ users: {
274
+ findById: vi.fn().mockResolvedValue({ id: "user_1" }),
275
+ },
276
+ };
277
+
278
+ di.set(Db, fakeDb);
279
+
280
+ const users = di.get(UsersRepo);
281
+
282
+ await expect(users.findById("user_1")).resolves.toEqual({ id: "user_1" });
283
+ expect(fakeDb.users.findById).toHaveBeenCalledWith("user_1");
284
+ expect(di.get(Logger).info).toHaveBeenCalledWith("Loading user user_1");
285
+ });
286
+ ```
287
+
288
+ ## Example App
289
+
290
+ See [`example/`](./example) for a sample API using `dimpl`.
291
+
292
+ It shows:
293
+
294
+ - `example/src/di.ts`: exporting one app container.
295
+ - `example/src/server.ts`: getting the app and wiring controllers.
296
+ - `example/src/modules/auth/auth.controller.ts`: injecting multiple dependencies with `di.deps`.
297
+ - `example/src/infrastructure/db.ts`: injecting a single dependency with `di.get`.
298
+
299
+ ```ts
300
+ import { dimpl } from "dimpl";
301
+ ```
302
+
303
+ ## Creating a Container
304
+
305
+ ```ts
306
+ import { dimpl } from "dimpl";
307
+
308
+ export const di = dimpl();
309
+ ```
310
+
311
+ Each call to `dimpl()` creates an independent container with its own cache.
312
+
313
+ ```ts
314
+ const appDi = dimpl();
315
+ const testDi = dimpl();
316
+
317
+ const Db = () => ({ connected: true });
318
+
319
+ appDi.get(Db) === appDi.get(Db); // true
320
+ appDi.get(Db) === testDi.get(Db); // false
321
+ ```
322
+
323
+ Use this when:
324
+
325
+ - You want one container for the whole app.
326
+ - You want a fresh container per test.
327
+ - You want distinct containers for multiple apps, tenants, workers, or integration-test scenarios.
328
+
329
+ ## API
330
+
331
+ ### `dimpl()`
332
+
333
+ Creates a new DI container.
334
+
335
+ ```ts
336
+ const di = dimpl();
337
+ ```
338
+
339
+ The returned container has four methods:
340
+
341
+ - `get`
342
+ - `deps`
343
+ - `set`
344
+ - `wire`
345
+
346
+ It also owns its own instance cache. No state is shared between containers.
347
+
348
+ ### `di.get(fnOrClass)`
349
+
350
+ Creates or returns the cached instance for one dependency.
351
+
352
+ ```ts
353
+ const Config = () => ({ port: 3000 });
354
+
355
+ const config = di.get(Config);
356
+ ```
357
+
358
+ Use `get` when:
359
+
360
+ - You need one dependency.
361
+ - You are bootstrapping the app.
362
+ - A factory depends on one other dependency.
363
+
364
+ With classes:
365
+
366
+ ```ts
367
+ class Logger {
368
+ info(message: string) {
369
+ console.log(message);
370
+ }
371
+ }
372
+
373
+ const logger = di.get(Logger);
374
+ ```
375
+
376
+ `get` is lazy. The function or class is called only once per container, then cached.
377
+
378
+ ### `di.deps({ name: fnOrClass })`
379
+
380
+ Creates or returns multiple dependencies as a typed object.
381
+
382
+ ```ts
383
+ const Service = (
384
+ { db, logger } = di.deps({
385
+ db: Db,
386
+ logger: Logger,
387
+ }),
388
+ ) => {
389
+ return {
390
+ run() {
391
+ logger.info("Running");
392
+ return db.query("select 1");
393
+ },
394
+ };
395
+ };
396
+ ```
397
+
398
+ Use `deps` when:
399
+
400
+ - A factory needs more than one dependency.
401
+ - You want a readable dependency list at the top of a function.
402
+ - You want TypeScript to infer a named object of dependency instances.
403
+
404
+ The object keys are yours. The values are dependency factories or classes. The returned object has the same keys, but each value is the resolved instance.
405
+
406
+ `deps` can also handle circular dependencies when access is delayed until after construction. See [Circular Dependencies](#circular-dependencies).
407
+
408
+ ### `di.set(fnOrClass, instance)`
409
+
410
+ Stores an instance for a dependency key.
411
+
412
+ ```ts
413
+ const fakeLogger = {
414
+ info() {},
415
+ };
416
+
417
+ di.set(Logger, fakeLogger);
418
+
419
+ di.get(Logger) === fakeLogger; // true
420
+ ```
421
+
422
+ Use `set` when:
423
+
424
+ - You want to override a dependency in a test.
425
+ - You already created an instance yourself.
426
+ - You want to provide an adapter from outside the container.
427
+
428
+ Call `set` before the dependency is resolved if you want consumers to receive the override.
429
+
430
+ ### `di.wire(...fnOrClass)`
431
+
432
+ Eagerly instantiates dependencies and returns nothing.
433
+
434
+ ```ts
435
+ di.wire(HealthController, AuthController, ProductsController);
436
+ ```
437
+
438
+ Use `wire` when:
439
+
440
+ - A dependency exists for side effects.
441
+ - Controllers register routes during construction.
442
+ - Startup should fail immediately if a dependency cannot be created.
443
+
444
+ `wire` uses the same cache as `get` and `deps`. If something was already created, it will not be created again.
445
+
446
+ ### `dimplCircularDepError`
447
+
448
+ `dimplCircularDepError` is exported for code that wants to recognize container-level circular dependency failures.
449
+
450
+ ```ts
451
+ import { dimplCircularDepError } from "dimpl";
452
+ ```
453
+
454
+ Most application code does not need to catch it. Prefer structuring circular references so they are accessed lazily through methods or getters.
455
+
456
+ ## Circular Dependencies
457
+
458
+ If two services depend on each other, `di.get` inside those services will not work. It tries to resolve the dependency immediately, so the container detects that the first service is still being created and throws a `dimplCircularDepError`.
459
+
460
+ ```ts
461
+ const A = (b = di.get(B)) => ({
462
+ name: "a",
463
+ b,
464
+ });
465
+
466
+ const B = (a = di.get(A)) => ({
467
+ name: "b",
468
+ a,
469
+ });
470
+
471
+ di.get(A); // throws dimplCircularDepError
472
+ ```
473
+
474
+ Use `di.deps` for circular dependencies, and keep access lazy. A service cannot read a dependency that is still being constructed. That includes destructuring it or reading it in the factory body.
475
+
476
+ ```ts
477
+ const A = (deps = di.deps({ b: B })) => {
478
+ return {
479
+ name: "a",
480
+ getB() {
481
+ return deps.b;
482
+ },
483
+ };
484
+ };
485
+
486
+ const B = (deps = di.deps({ a: A })) => {
487
+ deps.a; // throws: A is not ready yet
488
+
489
+ return {
490
+ name: "b",
491
+ };
492
+ };
493
+ ```
494
+
495
+ Access the circular dependency later from a method or getter:
496
+
497
+ ```ts
498
+ const A = (deps = di.deps({ b: B })) => ({
499
+ name: "a",
500
+ getB() {
501
+ return deps.b;
502
+ },
503
+ });
504
+
505
+ const B = (deps = di.deps({ a: A })) => ({
506
+ name: "b",
507
+ getA() {
508
+ return deps.a;
509
+ },
510
+ });
511
+
512
+ const a = di.get(A);
513
+ const b = di.get(B);
514
+
515
+ a.getB() === b; // true
516
+ b.getA() === a; // true
517
+ ```
518
+
519
+ For circular dependencies, avoid destructuring the circular dependency in the parameter list:
520
+
521
+ ```ts
522
+ // Avoid this for circular dependencies.
523
+ const B = ({ a } = di.deps({ a: A })) => ({
524
+ useA() {
525
+ return a;
526
+ },
527
+ });
528
+ ```
529
+
530
+ Destructuring reads `a` during construction. Keep the `deps` object and read `deps.a` later.
531
+
532
+ ## Patterns
533
+
534
+ ### Export One App Container
535
+
536
+ ```ts
537
+ // di.ts
538
+ import { dimpl } from "dimpl";
539
+
540
+ export const di = dimpl();
541
+ ```
542
+
543
+ Then import it from factories:
544
+
545
+ ```ts
546
+ import { di } from "./di";
547
+
548
+ export const Orders = ({ db, payments } = di.deps({ db: Db, payments: Payments })) => {
549
+ return {
550
+ create() {
551
+ // ...
552
+ },
553
+ };
554
+ };
555
+ ```
556
+
557
+ ### Prefer Default Parameters
558
+
559
+ Default parameters make production code automatic and tests explicit.
560
+
561
+ ```ts
562
+ export const Orders = (
563
+ { db, payments } = di.deps({
564
+ db: Db,
565
+ payments: Payments,
566
+ }),
567
+ ) => {
568
+ // ...
569
+ };
570
+ ```
571
+
572
+ In production:
573
+
574
+ ```ts
575
+ const orders = di.get(Orders);
576
+ ```
577
+
578
+ In tests:
579
+
580
+ ```ts
581
+ const orders = Orders({ db: fakeDb, payments: fakePayments });
582
+ ```
583
+
584
+ ### Keep Constructors Simple
585
+
586
+ Classes are supported, but `dimpl` constructs them without arguments.
587
+
588
+ ```ts
589
+ class Ids {
590
+ create() {
591
+ return crypto.randomUUID();
592
+ }
593
+ }
594
+
595
+ di.get(Ids);
596
+ ```
597
+
598
+ If a dependency needs other dependencies, a factory function with default parameters is usually clearer.
599
+
600
+ ```ts
601
+ const Orders = ({ db, ids } = di.deps({ db: Db, ids: Ids })) => {
602
+ // ...
603
+ };
604
+ ```
605
+
606
+ ## License
607
+
608
+ ISC
@@ -0,0 +1,21 @@
1
+ declare class dimplCircularDepError extends Error {
2
+ functionName: string;
3
+ constructor(message: string, functionName: string);
4
+ }
5
+ interface Fn<T> {
6
+ (...args: any[]): T;
7
+ }
8
+ interface Class<T> {
9
+ new (...args: any[]): T;
10
+ }
11
+ type Constructor<T> = Fn<T> | Class<T>;
12
+ type Instance<T> = T extends (new (...args: any[]) => infer R) ? R : T extends ((...args: any[]) => infer R) ? R : never;
13
+ declare const dimpl: () => {
14
+ get: <T>(fn: Constructor<T>) => T;
15
+ set<T>(fn: Constructor<T>, instance: T): void;
16
+ deps: <T extends {
17
+ [K: string]: Constructor<unknown>;
18
+ }>(obj: T) => { [K in keyof T]: Instance<T[K]> };
19
+ wire: (...args: Constructor<unknown>[]) => void;
20
+ };
21
+ export { dimpl, dimplCircularDepError };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ var dimplCircularDepError = class extends Error {
3
+ functionName;
4
+ constructor(message, functionName) {
5
+ super(message);
6
+ this.functionName = functionName;
7
+ }
8
+ };
9
+ const dimpl = () => {
10
+ const instances = /* @__PURE__ */ new Map();
11
+ const delayedDeps = /* @__PURE__ */ new Map();
12
+ const pending = /* @__PURE__ */ new Set();
13
+ const isClass = (fn) => {
14
+ return Object.getOwnPropertyDescriptor(fn, "prototype")?.writable === false;
15
+ };
16
+ const get = (fn) => {
17
+ let instance = instances.get(fn);
18
+ if (!instance) {
19
+ if (pending.has(fn)) throw new dimplCircularDepError("Cannot resolve a circular dependency", fn.name);
20
+ pending.add(fn);
21
+ instance = isClass(fn) ? new fn() : fn();
22
+ instances.set(fn, instance);
23
+ const delayedDependents = delayedDeps.get(fn);
24
+ if (delayedDependents) for (const [deps, keys] of delayedDependents.entries()) for (const key of keys) Object.defineProperty(deps, key, {
25
+ configurable: true,
26
+ enumerable: true,
27
+ value: instance
28
+ });
29
+ pending.delete(fn);
30
+ }
31
+ return instance;
32
+ };
33
+ return {
34
+ get,
35
+ set(fn, instance) {
36
+ instances.set(fn, instance);
37
+ },
38
+ deps: (obj) => {
39
+ const res = {};
40
+ for (const key in obj) {
41
+ const fn = obj[key];
42
+ try {
43
+ res[key] = get(fn);
44
+ } catch (e) {
45
+ if (!(e instanceof dimplCircularDepError)) throw e;
46
+ Object.defineProperty(res, key, {
47
+ configurable: true,
48
+ enumerable: true,
49
+ get() {
50
+ throw new Error(`Cannot access property ${key} yet due to a circular dependency`);
51
+ }
52
+ });
53
+ let delayedDependents = delayedDeps.get(fn);
54
+ if (!delayedDependents) {
55
+ delayedDependents = /* @__PURE__ */ new Map();
56
+ delayedDeps.set(fn, delayedDependents);
57
+ }
58
+ let keys = delayedDependents.get(res);
59
+ if (!keys) {
60
+ keys = [];
61
+ delayedDependents.set(res, keys);
62
+ }
63
+ keys.push(key);
64
+ }
65
+ }
66
+ return res;
67
+ },
68
+ wire: (...args) => {
69
+ for (const fn of args) get(fn);
70
+ }
71
+ };
72
+ };
73
+ exports.dimpl = dimpl;
74
+ exports.dimplCircularDepError = dimplCircularDepError;
75
+
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["export class dimplCircularDepError extends Error {\n constructor(\n message: string,\n public functionName: string,\n ) {\n super(message);\n }\n}\n\ninterface Fn<T> {\n (...args: any[]): T;\n}\n\ninterface Class<T> {\n new (...args: any[]): T;\n}\n\ntype Constructor<T> = Fn<T> | Class<T>;\ntype Instance<T> = T extends new (...args: any[]) => infer R\n ? R\n : T extends (...args: any[]) => infer R\n ? R\n : never;\n\nexport const dimpl = () => {\n const instances = new Map<unknown, unknown>();\n const delayedDeps = new Map<unknown, Map<{ [K: string]: unknown }, string[]>>();\n const pending = new Set<Constructor<unknown>>();\n\n const isClass = (fn: Constructor<unknown>): fn is Class<unknown> => {\n return Object.getOwnPropertyDescriptor(fn, \"prototype\")?.writable === false;\n };\n\n const get = <T>(fn: Constructor<T>): T => {\n let instance = instances.get(fn);\n if (!instance) {\n if (pending.has(fn)) {\n throw new dimplCircularDepError(\"Cannot resolve a circular dependency\", fn.name);\n }\n pending.add(fn);\n\n instance = isClass(fn) ? new fn() : fn();\n instances.set(fn, instance);\n\n const delayedDependents = delayedDeps.get(fn);\n if (delayedDependents) {\n for (const [deps, keys] of delayedDependents.entries()) {\n for (const key of keys) {\n Object.defineProperty(deps, key, {\n configurable: true,\n enumerable: true,\n value: instance,\n });\n }\n }\n }\n\n pending.delete(fn);\n }\n\n return instance as never;\n };\n\n return {\n get,\n\n set<T>(fn: Constructor<T>, instance: T): void {\n instances.set(fn, instance);\n },\n\n deps: <T extends { [K: string]: Constructor<unknown> }>(\n obj: T,\n ): {\n [K in keyof T]: Instance<T[K]>;\n } => {\n const res: { [K: string]: unknown } = {};\n\n for (const key in obj) {\n const fn = obj[key];\n\n try {\n res[key] = get(fn);\n } catch (e) {\n if (!(e instanceof dimplCircularDepError)) {\n throw e;\n }\n\n Object.defineProperty(res, key, {\n configurable: true,\n enumerable: true,\n get() {\n throw new Error(`Cannot access property ${key} yet due to a circular dependency`);\n },\n });\n\n let delayedDependents = delayedDeps.get(fn);\n if (!delayedDependents) {\n delayedDependents = new Map();\n delayedDeps.set(fn, delayedDependents);\n }\n\n let keys = delayedDependents.get(res);\n if (!keys) {\n keys = [];\n delayedDependents.set(res, keys);\n }\n\n keys.push(key);\n }\n }\n\n return res as never;\n },\n\n wire: (...args: Constructor<unknown>[]): void => {\n for (const fn of args) {\n get(fn);\n }\n },\n };\n};\n"],"mappings":";AAAA,IAAa,wBAAb,cAA2C,MAAM;CAGtC;CAFT,YACE,SACA,cACA;EACA,MAAM,OAAO;EAFN,KAAA,eAAA;CAGT;AACF;AAiBA,MAAa,cAAc;CACzB,MAAM,4BAAY,IAAI,IAAsB;CAC5C,MAAM,8BAAc,IAAI,IAAsD;CAC9E,MAAM,0BAAU,IAAI,IAA0B;CAE9C,MAAM,WAAW,OAAmD;EAClE,OAAO,OAAO,yBAAyB,IAAI,WAAW,CAAC,EAAE,aAAa;CACxE;CAEA,MAAM,OAAU,OAA0B;EACxC,IAAI,WAAW,UAAU,IAAI,EAAE;EAC/B,IAAI,CAAC,UAAU;GACb,IAAI,QAAQ,IAAI,EAAE,GAChB,MAAM,IAAI,sBAAsB,wCAAwC,GAAG,IAAI;GAEjF,QAAQ,IAAI,EAAE;GAEd,WAAW,QAAQ,EAAE,IAAI,IAAI,GAAG,IAAI,GAAG;GACvC,UAAU,IAAI,IAAI,QAAQ;GAE1B,MAAM,oBAAoB,YAAY,IAAI,EAAE;GAC5C,IAAI,mBACF,KAAK,MAAM,CAAC,MAAM,SAAS,kBAAkB,QAAQ,GACnD,KAAK,MAAM,OAAO,MAChB,OAAO,eAAe,MAAM,KAAK;IAC/B,cAAc;IACd,YAAY;IACZ,OAAO;GACT,CAAC;GAKP,QAAQ,OAAO,EAAE;EACnB;EAEA,OAAO;CACT;CAEA,OAAO;EACL;EAEA,IAAO,IAAoB,UAAmB;GAC5C,UAAU,IAAI,IAAI,QAAQ;EAC5B;EAEA,OACE,QAGG;GACH,MAAM,MAAgC,CAAC;GAEvC,KAAK,MAAM,OAAO,KAAK;IACrB,MAAM,KAAK,IAAI;IAEf,IAAI;KACF,IAAI,OAAO,IAAI,EAAE;IACnB,SAAS,GAAG;KACV,IAAI,EAAE,aAAa,wBACjB,MAAM;KAGR,OAAO,eAAe,KAAK,KAAK;MAC9B,cAAc;MACd,YAAY;MACZ,MAAM;OACJ,MAAM,IAAI,MAAM,0BAA0B,IAAI,kCAAkC;MAClF;KACF,CAAC;KAED,IAAI,oBAAoB,YAAY,IAAI,EAAE;KAC1C,IAAI,CAAC,mBAAmB;MACtB,oCAAoB,IAAI,IAAI;MAC5B,YAAY,IAAI,IAAI,iBAAiB;KACvC;KAEA,IAAI,OAAO,kBAAkB,IAAI,GAAG;KACpC,IAAI,CAAC,MAAM;MACT,OAAO,CAAC;MACR,kBAAkB,IAAI,KAAK,IAAI;KACjC;KAEA,KAAK,KAAK,GAAG;IACf;GACF;GAEA,OAAO;EACT;EAEA,OAAO,GAAG,SAAuC;GAC/C,KAAK,MAAM,MAAM,MACf,IAAI,EAAE;EAEV;CACF;AACF"}
package/dist/index.mjs ADDED
@@ -0,0 +1,74 @@
1
+ var dimplCircularDepError = class extends Error {
2
+ functionName;
3
+ constructor(message, functionName) {
4
+ super(message);
5
+ this.functionName = functionName;
6
+ }
7
+ };
8
+ const dimpl = () => {
9
+ const instances = /* @__PURE__ */ new Map();
10
+ const delayedDeps = /* @__PURE__ */ new Map();
11
+ const pending = /* @__PURE__ */ new Set();
12
+ const isClass = (fn) => {
13
+ return Object.getOwnPropertyDescriptor(fn, "prototype")?.writable === false;
14
+ };
15
+ const get = (fn) => {
16
+ let instance = instances.get(fn);
17
+ if (!instance) {
18
+ if (pending.has(fn)) throw new dimplCircularDepError("Cannot resolve a circular dependency", fn.name);
19
+ pending.add(fn);
20
+ instance = isClass(fn) ? new fn() : fn();
21
+ instances.set(fn, instance);
22
+ const delayedDependents = delayedDeps.get(fn);
23
+ if (delayedDependents) for (const [deps, keys] of delayedDependents.entries()) for (const key of keys) Object.defineProperty(deps, key, {
24
+ configurable: true,
25
+ enumerable: true,
26
+ value: instance
27
+ });
28
+ pending.delete(fn);
29
+ }
30
+ return instance;
31
+ };
32
+ return {
33
+ get,
34
+ set(fn, instance) {
35
+ instances.set(fn, instance);
36
+ },
37
+ deps: (obj) => {
38
+ const res = {};
39
+ for (const key in obj) {
40
+ const fn = obj[key];
41
+ try {
42
+ res[key] = get(fn);
43
+ } catch (e) {
44
+ if (!(e instanceof dimplCircularDepError)) throw e;
45
+ Object.defineProperty(res, key, {
46
+ configurable: true,
47
+ enumerable: true,
48
+ get() {
49
+ throw new Error(`Cannot access property ${key} yet due to a circular dependency`);
50
+ }
51
+ });
52
+ let delayedDependents = delayedDeps.get(fn);
53
+ if (!delayedDependents) {
54
+ delayedDependents = /* @__PURE__ */ new Map();
55
+ delayedDeps.set(fn, delayedDependents);
56
+ }
57
+ let keys = delayedDependents.get(res);
58
+ if (!keys) {
59
+ keys = [];
60
+ delayedDependents.set(res, keys);
61
+ }
62
+ keys.push(key);
63
+ }
64
+ }
65
+ return res;
66
+ },
67
+ wire: (...args) => {
68
+ for (const fn of args) get(fn);
69
+ }
70
+ };
71
+ };
72
+ export { dimpl, dimplCircularDepError };
73
+
74
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export class dimplCircularDepError extends Error {\n constructor(\n message: string,\n public functionName: string,\n ) {\n super(message);\n }\n}\n\ninterface Fn<T> {\n (...args: any[]): T;\n}\n\ninterface Class<T> {\n new (...args: any[]): T;\n}\n\ntype Constructor<T> = Fn<T> | Class<T>;\ntype Instance<T> = T extends new (...args: any[]) => infer R\n ? R\n : T extends (...args: any[]) => infer R\n ? R\n : never;\n\nexport const dimpl = () => {\n const instances = new Map<unknown, unknown>();\n const delayedDeps = new Map<unknown, Map<{ [K: string]: unknown }, string[]>>();\n const pending = new Set<Constructor<unknown>>();\n\n const isClass = (fn: Constructor<unknown>): fn is Class<unknown> => {\n return Object.getOwnPropertyDescriptor(fn, \"prototype\")?.writable === false;\n };\n\n const get = <T>(fn: Constructor<T>): T => {\n let instance = instances.get(fn);\n if (!instance) {\n if (pending.has(fn)) {\n throw new dimplCircularDepError(\"Cannot resolve a circular dependency\", fn.name);\n }\n pending.add(fn);\n\n instance = isClass(fn) ? new fn() : fn();\n instances.set(fn, instance);\n\n const delayedDependents = delayedDeps.get(fn);\n if (delayedDependents) {\n for (const [deps, keys] of delayedDependents.entries()) {\n for (const key of keys) {\n Object.defineProperty(deps, key, {\n configurable: true,\n enumerable: true,\n value: instance,\n });\n }\n }\n }\n\n pending.delete(fn);\n }\n\n return instance as never;\n };\n\n return {\n get,\n\n set<T>(fn: Constructor<T>, instance: T): void {\n instances.set(fn, instance);\n },\n\n deps: <T extends { [K: string]: Constructor<unknown> }>(\n obj: T,\n ): {\n [K in keyof T]: Instance<T[K]>;\n } => {\n const res: { [K: string]: unknown } = {};\n\n for (const key in obj) {\n const fn = obj[key];\n\n try {\n res[key] = get(fn);\n } catch (e) {\n if (!(e instanceof dimplCircularDepError)) {\n throw e;\n }\n\n Object.defineProperty(res, key, {\n configurable: true,\n enumerable: true,\n get() {\n throw new Error(`Cannot access property ${key} yet due to a circular dependency`);\n },\n });\n\n let delayedDependents = delayedDeps.get(fn);\n if (!delayedDependents) {\n delayedDependents = new Map();\n delayedDeps.set(fn, delayedDependents);\n }\n\n let keys = delayedDependents.get(res);\n if (!keys) {\n keys = [];\n delayedDependents.set(res, keys);\n }\n\n keys.push(key);\n }\n }\n\n return res as never;\n },\n\n wire: (...args: Constructor<unknown>[]): void => {\n for (const fn of args) {\n get(fn);\n }\n },\n };\n};\n"],"mappings":"AAAA,IAAa,wBAAb,cAA2C,MAAM;CAGtC;CAFT,YACE,SACA,cACA;EACA,MAAM,OAAO;EAFN,KAAA,eAAA;CAGT;AACF;AAiBA,MAAa,cAAc;CACzB,MAAM,4BAAY,IAAI,IAAsB;CAC5C,MAAM,8BAAc,IAAI,IAAsD;CAC9E,MAAM,0BAAU,IAAI,IAA0B;CAE9C,MAAM,WAAW,OAAmD;EAClE,OAAO,OAAO,yBAAyB,IAAI,WAAW,CAAC,EAAE,aAAa;CACxE;CAEA,MAAM,OAAU,OAA0B;EACxC,IAAI,WAAW,UAAU,IAAI,EAAE;EAC/B,IAAI,CAAC,UAAU;GACb,IAAI,QAAQ,IAAI,EAAE,GAChB,MAAM,IAAI,sBAAsB,wCAAwC,GAAG,IAAI;GAEjF,QAAQ,IAAI,EAAE;GAEd,WAAW,QAAQ,EAAE,IAAI,IAAI,GAAG,IAAI,GAAG;GACvC,UAAU,IAAI,IAAI,QAAQ;GAE1B,MAAM,oBAAoB,YAAY,IAAI,EAAE;GAC5C,IAAI,mBACF,KAAK,MAAM,CAAC,MAAM,SAAS,kBAAkB,QAAQ,GACnD,KAAK,MAAM,OAAO,MAChB,OAAO,eAAe,MAAM,KAAK;IAC/B,cAAc;IACd,YAAY;IACZ,OAAO;GACT,CAAC;GAKP,QAAQ,OAAO,EAAE;EACnB;EAEA,OAAO;CACT;CAEA,OAAO;EACL;EAEA,IAAO,IAAoB,UAAmB;GAC5C,UAAU,IAAI,IAAI,QAAQ;EAC5B;EAEA,OACE,QAGG;GACH,MAAM,MAAgC,CAAC;GAEvC,KAAK,MAAM,OAAO,KAAK;IACrB,MAAM,KAAK,IAAI;IAEf,IAAI;KACF,IAAI,OAAO,IAAI,EAAE;IACnB,SAAS,GAAG;KACV,IAAI,EAAE,aAAa,wBACjB,MAAM;KAGR,OAAO,eAAe,KAAK,KAAK;MAC9B,cAAc;MACd,YAAY;MACZ,MAAM;OACJ,MAAM,IAAI,MAAM,0BAA0B,IAAI,kCAAkC;MAClF;KACF,CAAC;KAED,IAAI,oBAAoB,YAAY,IAAI,EAAE;KAC1C,IAAI,CAAC,mBAAmB;MACtB,oCAAoB,IAAI,IAAI;MAC5B,YAAY,IAAI,IAAI,iBAAiB;KACvC;KAEA,IAAI,OAAO,kBAAkB,IAAI,GAAG;KACpC,IAAI,CAAC,MAAM;MACT,OAAO,CAAC;MACR,kBAAkB,IAAI,KAAK,IAAI;KACjC;KAEA,KAAK,KAAK,GAAG;IACf;GACF;GAEA,OAAO;EACT;EAEA,OAAO,GAAG,SAAuC;GAC/C,KAAK,MAAM,MAAM,MACf,IAAI,EAAE;EAEV;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@romikus/dimpl",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Simple dependency-injection library",
6
+ "keywords": [
7
+ "di",
8
+ "dependency injection"
9
+ ],
10
+ "license": "ISC",
11
+ "author": "Roman Kushyn",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/romeerez/dimpl.git"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "main": "dist/index.js",
20
+ "module": "dist/index.mjs",
21
+ "typings": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "require": {
25
+ "default": "./dist/index.js",
26
+ "types": "./dist/index.d.ts"
27
+ },
28
+ "import": {
29
+ "default": "./dist/index.mjs",
30
+ "types": "./dist/index.d.ts"
31
+ }
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "oxfmt": "0.53.0",
36
+ "oxlint": "1.68.0",
37
+ "rimraf": "^6.1.3",
38
+ "rolldown": "^1.1.0",
39
+ "rolldown-plugin-dts": "^0.25.2",
40
+ "typescript": "^6.0.3",
41
+ "vitest": "^4.1.8"
42
+ },
43
+ "scripts": {
44
+ "test": "vitest run",
45
+ "lint": "oxlint --ignore-path=.gitignore .",
46
+ "fmt": "oxfmt --write --ignore-path=.gitignore .",
47
+ "fmt:check": "oxfmt --check --ignore-path=.gitignore .",
48
+ "build": "rimraf ./dist/ && rolldown -c ./rolldown.config.mjs",
49
+ "prepublish": "pnpm build"
50
+ }
51
+ }