ts-ioc-container 71.0.0 → 72.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.
Files changed (66) hide show
  1. package/README.md +203 -65
  2. package/cjm/container/Container.js +5 -5
  3. package/cjm/hooks/HookContext.js +3 -2
  4. package/cjm/hooks/injectProp.js +2 -4
  5. package/cjm/index.js +7 -4
  6. package/cjm/injector/IInjector.js +2 -2
  7. package/cjm/injector/MetadataInjector.js +14 -9
  8. package/cjm/injector/ProxyInjector.js +1 -1
  9. package/cjm/injector/SimpleInjector.js +2 -2
  10. package/cjm/metadata/class.js +3 -1
  11. package/cjm/metadata/method.js +3 -1
  12. package/cjm/metadata/parameter.js +7 -1
  13. package/cjm/provider/Provider.js +9 -8
  14. package/cjm/registration/IRegistration.js +2 -2
  15. package/cjm/select.js +2 -2
  16. package/cjm/token/ClassToken.js +3 -3
  17. package/cjm/token/FunctionToken.js +5 -4
  18. package/cjm/token/GroupAliasToken.js +3 -3
  19. package/cjm/token/InjectionToken.js +1 -1
  20. package/cjm/token/SingleAliasToken.js +3 -3
  21. package/cjm/token/SingleToken.js +3 -3
  22. package/cjm/token/toToken.js +1 -8
  23. package/esm/container/Container.js +5 -5
  24. package/esm/hooks/HookContext.js +3 -2
  25. package/esm/hooks/injectProp.js +1 -4
  26. package/esm/index.js +5 -5
  27. package/esm/injector/IInjector.js +2 -2
  28. package/esm/injector/MetadataInjector.js +13 -9
  29. package/esm/injector/ProxyInjector.js +1 -1
  30. package/esm/injector/SimpleInjector.js +2 -2
  31. package/esm/metadata/class.js +1 -0
  32. package/esm/metadata/method.js +1 -0
  33. package/esm/metadata/parameter.js +5 -0
  34. package/esm/provider/Provider.js +9 -8
  35. package/esm/registration/IRegistration.js +2 -2
  36. package/esm/select.js +2 -2
  37. package/esm/token/ClassToken.js +3 -3
  38. package/esm/token/FunctionToken.js +5 -4
  39. package/esm/token/GroupAliasToken.js +3 -3
  40. package/esm/token/InjectionToken.js +1 -1
  41. package/esm/token/SingleAliasToken.js +3 -3
  42. package/esm/token/SingleToken.js +3 -3
  43. package/esm/token/toToken.js +0 -6
  44. package/package.json +1 -1
  45. package/typings/container/IContainer.d.ts +2 -2
  46. package/typings/hooks/HookContext.d.ts +3 -3
  47. package/typings/hooks/hook.d.ts +1 -2
  48. package/typings/hooks/injectProp.d.ts +2 -14
  49. package/typings/index.d.ts +7 -7
  50. package/typings/injector/IInjector.d.ts +7 -4
  51. package/typings/injector/MetadataInjector.d.ts +6 -16
  52. package/typings/injector/ProxyInjector.d.ts +1 -2
  53. package/typings/injector/SimpleInjector.d.ts +1 -2
  54. package/typings/metadata/class.d.ts +1 -0
  55. package/typings/metadata/method.d.ts +1 -0
  56. package/typings/metadata/parameter.d.ts +1 -0
  57. package/typings/provider/IProvider.d.ts +5 -4
  58. package/typings/provider/Provider.d.ts +2 -2
  59. package/typings/select.d.ts +3 -3
  60. package/typings/token/ClassToken.d.ts +2 -2
  61. package/typings/token/FunctionToken.d.ts +2 -2
  62. package/typings/token/GroupAliasToken.d.ts +2 -2
  63. package/typings/token/InjectionToken.d.ts +2 -2
  64. package/typings/token/SingleAliasToken.d.ts +3 -3
  65. package/typings/token/SingleToken.d.ts +2 -2
  66. package/typings/token/toToken.d.ts +0 -2
package/README.md CHANGED
@@ -52,6 +52,7 @@ provider pipelines, aliases, and custom injector strategies.
52
52
  - [Registration](#registration) `@register`
53
53
  - [Token](#token) `bindTo`
54
54
  - [Scope](#scope) `scope`
55
+ - [Composing decorators](#composing-decorators) `createComposeClassDecorator`
55
56
  - [Module](#module)
56
57
  - [Hook](#hook) `@hook`
57
58
  - [Hook domains](#hook-domains) `ScopeHook` `InjectorHook` `ProviderHook`
@@ -114,7 +115,7 @@ bundlers tree-shake unused exports.
114
115
  ## Quickstart
115
116
 
116
117
  ```typescript
117
- import { bindTo, Container, inject, register, Registration as R, singleton, SingleToken } from 'ts-ioc-container';
118
+ import { bindTo, Container, inject, register, Registration as R, singleton, SingleToken, by } from 'ts-ioc-container';
118
119
 
119
120
  interface ILogger {
120
121
  log(message: string): void;
@@ -130,7 +131,7 @@ class Logger implements ILogger {
130
131
  }
131
132
 
132
133
  class App {
133
- constructor(@inject(ILoggerToken) private logger: ILogger) {}
134
+ constructor(@inject(by(ILoggerToken)) private logger: ILogger) {}
134
135
  start() {
135
136
  this.logger.log('hello');
136
137
  }
@@ -153,16 +154,18 @@ describe('Quickstart', function () {
153
154
 
154
155
  - Register class with key (preferred): `@register(bindTo('Key')) class Service {}` then `container.addRegistration(R.fromClass(Service))`
155
156
  - Register value: `R.fromValue(config).bindTo('Config')`
156
- - Register factory: `R.fromFn((c) => createX(c)).bindTo('X')`
157
+ - Register factory: `R.fromFn(({ scope }) => createX(scope)).bindTo('X')`
157
158
  - Singleton: `@register(singleton())`
158
159
  - Eager service: `@register(autoResolve())` + `container.useModule(new AutoResolveModule())`
159
160
  - Scoped registration: `@register(scope((s) => s.hasTag('request')))`
160
161
  - Resolve by alias: `container.resolveByAlias('Alias')`
161
162
  - Current scope token: `select.scope.current`
162
163
  - Lazy token: `select.token('Service').lazy()`
163
- - Inject decorator: `@inject('Key')`
164
- - Map an injected value: `@inject('Key', sanitize(), validate())`
165
- - Property inject: `@hook('onInit', injectProp('Key'))`
164
+ - Inject decorator: `@inject(by(Token))` / `@inject(by('Key'))` / `@inject(by(Logger))` — the argument is always one `InjectFn`, `by(...)` builds the usual one
165
+ - Custom inject function: `@inject(({ scope, args }) => ...)`
166
+ - Map an injected value: `@inject(pipe(by('Key'), sanitize(), validate()))`
167
+ - Property inject: `@hook('onInit', injectProp(by('Key')))`
168
+ - Name a decorator stack: `const repository = (token) => createComposeClassDecorator(register(token, singleton()), addClassMeta('injection-token', () => token))`
166
169
 
167
170
  > [!TIP]
168
171
  > For classes, prefer the `@register(bindTo('Key'))` decorator over the fluent
@@ -184,7 +187,7 @@ describe('Quickstart', function () {
184
187
 
185
188
  ```typescript
186
189
  import 'reflect-metadata';
187
- import { bindTo, Container, type IContainer, inject, register, Registration as R, select } from 'ts-ioc-container';
190
+ import { bindTo, Container, type IContainer, inject, register, Registration as R, select, by } from 'ts-ioc-container';
188
191
 
189
192
  /**
190
193
  * User Management Domain - Basic Dependency Injection
@@ -219,7 +222,7 @@ describe('Basic usage', function () {
219
222
  it('should inject dependencies', function () {
220
223
  // AuthService depends on IUserRepository
221
224
  class AuthService {
222
- constructor(@inject('IUserRepository') private userRepo: IUserRepository) {}
225
+ constructor(@inject(by('IUserRepository')) private userRepo: IUserRepository) {}
223
226
 
224
227
  authenticate(email: string): boolean {
225
228
  const user = this.userRepo.findByEmail(email);
@@ -243,7 +246,7 @@ describe('Basic usage', function () {
243
246
  const appContainer = new Container({ tags: ['application'] });
244
247
 
245
248
  class RequestHandler {
246
- constructor(@inject(select.scope.current) public requestScope: IContainer) {}
249
+ constructor(@inject(by(select.scope.current)) public requestScope: IContainer) {}
247
250
 
248
251
  handleRequest(): string {
249
252
  // Access request-scoped dependencies
@@ -286,6 +289,7 @@ import {
286
289
  scope,
287
290
  select,
288
291
  singleton,
292
+ by,
289
293
  } from 'ts-ioc-container';
290
294
 
291
295
  /**
@@ -345,7 +349,10 @@ describe('Scopes', function () {
345
349
 
346
350
  // RequestHandler can create a transaction scope for database operations
347
351
  class RequestHandler {
348
- constructor(@inject(select.scope.create({ tags: ['transaction'] })) public transactionScope: IContainer) {}
352
+ constructor(
353
+ @inject(by(select.scope.create({ tags: ['transaction'] })))
354
+ public transactionScope: IContainer,
355
+ ) {}
349
356
 
350
357
  executeInTransaction(): boolean {
351
358
  // Transaction scope inherits from request scope
@@ -371,7 +378,7 @@ Sometimes you want to get all instances from container and its scopes. For examp
371
378
  - you can get instances from container and scope which were created by injector
372
379
 
373
380
  ```typescript
374
- import { bindTo, Container, inject, register, Registration as R, select } from 'ts-ioc-container';
381
+ import { bindTo, Container, inject, register, Registration as R, select, by } from 'ts-ioc-container';
375
382
 
376
383
  /**
377
384
  * User Management Domain - Instance Collection
@@ -391,7 +398,7 @@ describe('Instances', function () {
391
398
  it('should collect instances across scope hierarchy', () => {
392
399
  // App that needs access to all logger instances (e.g., for flushing)
393
400
  class App {
394
- constructor(@inject(select.instances()) public loggers: Logger[]) {}
401
+ constructor(@inject(by(select.instances())) public loggers: Logger[]) {}
395
402
  }
396
403
 
397
404
  const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
@@ -413,7 +420,7 @@ describe('Instances', function () {
413
420
  it('should return only current scope instances when cascade is disabled', () => {
414
421
  // Only get instances from current scope, not parent scopes
415
422
  class App {
416
- constructor(@inject(select.instances().cascade(false)) public loggers: Logger[]) {}
423
+ constructor(@inject(by(select.instances().cascade(false))) public loggers: Logger[]) {}
417
424
  }
418
425
 
419
426
  const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
@@ -432,7 +439,7 @@ describe('Instances', function () {
432
439
  const isLogger = (instance: unknown) => instance instanceof Logger;
433
440
 
434
441
  class App {
435
- constructor(@inject(select.instances(isLogger)) public loggers: Logger[]) {}
442
+ constructor(@inject(by(select.instances(isLogger))) public loggers: Logger[]) {}
436
443
  }
437
444
 
438
445
  const container = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
@@ -569,7 +576,7 @@ Sometimes you want to create dependency only when somebody want to invoke it's m
569
576
 
570
577
  ```typescript
571
578
  import 'reflect-metadata';
572
- import { Container, inject, register, Registration as R, select as s, singleton } from 'ts-ioc-container';
579
+ import { Container, inject, register, Registration as R, select as s, singleton, by } from 'ts-ioc-container';
573
580
 
574
581
  /**
575
582
  * User Management Domain - Lazy Loading
@@ -600,7 +607,7 @@ describe('lazy provider', () => {
600
607
 
601
608
  // EmailNotifier is expensive - establishes SMTP connection on construction
602
609
  class EmailNotifier {
603
- constructor(@inject('SmtpConnectionStatus') private smtp: SmtpConnectionStatus) {
610
+ constructor(@inject(by('SmtpConnectionStatus')) private smtp: SmtpConnectionStatus) {
604
611
  // Simulate expensive SMTP connection
605
612
  this.smtp.connect();
606
613
  }
@@ -613,7 +620,10 @@ describe('lazy provider', () => {
613
620
  // AuthService might need to send password reset emails
614
621
  // But most login requests don't need email (only password reset does)
615
622
  class AuthService {
616
- constructor(@inject(s.token('EmailNotifier').lazy()) public emailNotifier: EmailNotifier) {}
623
+ constructor(
624
+ @inject(by(s.token('EmailNotifier').lazy()))
625
+ public emailNotifier: EmailNotifier,
626
+ ) {}
617
627
 
618
628
  login(email: string, password: string): boolean {
619
629
  // Most requests just validate credentials - no email needed
@@ -720,6 +730,7 @@ import {
720
730
  register,
721
731
  Registration as R,
722
732
  singleton,
733
+ by,
723
734
  } from 'ts-ioc-container';
724
735
 
725
736
  /**
@@ -762,7 +773,7 @@ describe('lazy registerPipe', () => {
762
773
  // Analytics service - expensive, but only used occasionally
763
774
  @register(bindTo('AnalyticsService'), lazy(), singleton())
764
775
  class AnalyticsService {
765
- constructor(@inject('DatabasePool') private db: DatabasePool) {
776
+ constructor(@inject(by('DatabasePool')) private db: DatabasePool) {
766
777
  initLog.push('AnalyticsService initialized');
767
778
  }
768
779
 
@@ -777,7 +788,7 @@ describe('lazy registerPipe', () => {
777
788
 
778
789
  // Application service - always used
779
790
  class AppService {
780
- constructor(@inject('AnalyticsService') public analytics: AnalyticsService) {
791
+ constructor(@inject(by('AnalyticsService')) public analytics: AnalyticsService) {
781
792
  initLog.push('AppService initialized');
782
793
  }
783
794
 
@@ -874,8 +885,8 @@ describe('lazy registerPipe', () => {
874
885
  // Notification service - uses email and SMS, but maybe not both
875
886
  class NotificationService {
876
887
  constructor(
877
- @inject('EmailService') public email: EmailService,
878
- @inject('SmsService') public sms: SmsService,
888
+ @inject(by('EmailService')) public email: EmailService,
889
+ @inject(by('SmsService')) public sms: SmsService,
879
890
  ) {
880
891
  initLog.push('NotificationService initialized');
881
892
  }
@@ -967,7 +978,7 @@ describe('lazy registerPipe', () => {
967
978
  }
968
979
 
969
980
  class ApiService {
970
- constructor(@inject('CacheService') private cache: CacheService) {
981
+ constructor(@inject(by('CacheService')) private cache: CacheService) {
971
982
  initLog.push('ApiService initialized');
972
983
  }
973
984
 
@@ -1085,8 +1096,8 @@ describe('lazy registerPipe', () => {
1085
1096
 
1086
1097
  class Application {
1087
1098
  constructor(
1088
- @inject('FeatureFlagService') private flags: FeatureFlagService,
1089
- @inject('PremiumFeature') private premium: PremiumFeature,
1099
+ @inject(by('FeatureFlagService')) private flags: FeatureFlagService,
1100
+ @inject(by('PremiumFeature')) private premium: PremiumFeature,
1090
1101
  ) {
1091
1102
  initLog.push('Application initialized');
1092
1103
  }
@@ -1144,8 +1155,23 @@ describe('lazy registerPipe', () => {
1144
1155
  This type of injector uses `@inject` decorator to mark where dependencies should be injected. It's bases on `reflect-metadata` package. That's why I call it `MetadataInjector`.
1145
1156
  Also you can [inject property.](#inject-property)
1146
1157
 
1158
+ `@inject` takes exactly one argument, an `InjectFn`: `(options: ProviderOptions) => T`.
1159
+ It is called with the resolution context of the class being constructed — the
1160
+ `scope` it is built in and the runtime `args` it is built with — and whatever it
1161
+ returns is injected. There is no other form, only functions which build one:
1162
+
1163
+ - `by(target)` — resolves a token, a `DependencyKey` or a class from the scope,
1164
+ forwarding the runtime args (see
1165
+ [runtime args flow through tokens](#runtime-args-flow-through-tokens)):
1166
+ `@inject(by(LoggerToken))`, `@inject(by('Logger'))`, `@inject(by(Logger))`.
1167
+ - `arg(index)` / `args` / `argsFn(predicate)` — pick from the runtime args.
1168
+ - `pipe(fn, ...mappers)` — map what another `InjectFn` returns.
1169
+ - Your own: `@inject(({ scope }) => scope)` injects the current scope,
1170
+ `@inject(({ scope }) => Token.resolve(scope))` resolves a token *without*
1171
+ the args.
1172
+
1147
1173
  ```typescript
1148
- import { bindTo, Container, inject, register, Registration as R } from 'ts-ioc-container';
1174
+ import { bindTo, Container, inject, register, Registration as R, by } from 'ts-ioc-container';
1149
1175
 
1150
1176
  /**
1151
1177
  * User Management Domain - Metadata Injection
@@ -1154,7 +1180,7 @@ import { bindTo, Container, inject, register, Registration as R } from 'ts-ioc-c
1154
1180
  * to automatically inject dependencies into constructor parameters.
1155
1181
  *
1156
1182
  * How it works:
1157
- * 1. @inject('key') decorator marks a parameter for injection
1183
+ * 1. @inject(by('key')) decorator marks a parameter for injection
1158
1184
  * 2. Container reads metadata at resolution time
1159
1185
  * 3. Dependencies are resolved and passed to constructor
1160
1186
  *
@@ -1169,10 +1195,10 @@ class Logger {
1169
1195
 
1170
1196
  class App {
1171
1197
  // @inject tells the container which dependency to resolve for this parameter
1172
- constructor(@inject('ILogger') private logger: Logger) {}
1198
+ constructor(@inject(by('ILogger')) private logger: Logger) {}
1173
1199
 
1174
1200
  // Alternative: inject via function for dynamic resolution
1175
- // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}
1201
+ // constructor(@inject(by('ILogger')) private logger: ILogger) {}
1176
1202
 
1177
1203
  getLoggerName(): string {
1178
1204
  return this.logger.name;
@@ -1194,31 +1220,32 @@ describe('Metadata Injector', function () {
1194
1220
 
1195
1221
  ### Mapping injected values
1196
1222
 
1197
- Every argument after the first one is a **mapper** applied to the resolved
1198
- instance, left to right — the value the last mapper returns is what reaches the
1199
- constructor parameter:
1223
+ An `InjectFn` is a function, so mapping the value it resolves is function
1224
+ composition — `pipe(fn, ...mappers)` (exported) runs each **mapper** on the
1225
+ previous result, left to right, and the value the last one returns is what
1226
+ reaches the constructor parameter:
1200
1227
 
1201
1228
  ```typescript
1202
- @inject('Config', takeApiUrl(), stripTrailingSlash(), requireHttps())
1229
+ @inject(pipe(by<Config>('Config'), takeApiUrl(), stripTrailingSlash(), requireHttps()))
1203
1230
  ```
1204
1231
 
1205
1232
  A mapper is a plain `(value) => value` function, so mappers compose into named,
1206
1233
  reusable steps (selecting a member, sanitizing, validating) and each step's
1207
- parameter type is inferred from the previous one. With no mapper the resolved
1208
- instance is injected untouched.
1209
-
1210
- `injectProp` takes the same rest parameters: `injectProp('Config', takeApiUrl())`.
1234
+ parameter type is inferred from the previous one. `pipe(fn, ...mappers)` is
1235
+ itself an `InjectFn`, so it goes wherever one does — `@inject(...)`, a
1236
+ `FunctionToken`, or `injectProp(pipe(by('Config'), takeApiUrl()))`.
1211
1237
 
1212
1238
  ```typescript
1213
1239
  import 'reflect-metadata';
1214
- import { Container, inject, Registration as R } from 'ts-ioc-container';
1240
+ import { Container, inject, Registration as R, pipe, by } from 'ts-ioc-container';
1215
1241
 
1216
1242
  /**
1217
1243
  * Mapping injected values
1218
1244
  *
1219
- * Every argument after the first one passed to `@inject` (or `injectProp`) is a
1220
- * mapper applied to the resolved instance, left to right. Mappers are plain
1221
- * functions, so they compose into reusable, named steps.
1245
+ * `@inject` takes one `InjectFn`, so mapping what it resolves is composition:
1246
+ * `pipe(fn, ...mappers)` applies each mapper to the previous result, left to
1247
+ * right, and is itself an `InjectFn`. Mappers are plain functions, so they
1248
+ * compose into reusable, named steps.
1222
1249
  */
1223
1250
 
1224
1251
  interface Config {
@@ -1239,7 +1266,10 @@ const requireHttps = () => (url: string) => {
1239
1266
  describe('inject mappers', () => {
1240
1267
  it('should pipe the resolved dependency through every mapper', () => {
1241
1268
  class ApiClient {
1242
- constructor(@inject('Config', takeApiUrl(), stripTrailingSlash(), requireHttps()) readonly apiUrl: string) {}
1269
+ constructor(
1270
+ @inject(pipe(by<Config>('Config'), takeApiUrl(), stripTrailingSlash(), requireHttps()))
1271
+ readonly apiUrl: string,
1272
+ ) {}
1243
1273
  }
1244
1274
 
1245
1275
  const container = new Container().addRegistration(
@@ -1251,7 +1281,10 @@ describe('inject mappers', () => {
1251
1281
 
1252
1282
  it('should throw from a mapper when the resolved value is not acceptable', () => {
1253
1283
  class ApiClient {
1254
- constructor(@inject('Config', takeApiUrl(), requireHttps()) readonly apiUrl: string) {}
1284
+ constructor(
1285
+ @inject(pipe(by<Config>('Config'), takeApiUrl(), requireHttps()))
1286
+ readonly apiUrl: string,
1287
+ ) {}
1255
1288
  }
1256
1289
 
1257
1290
  const container = new Container().addRegistration(
@@ -1391,7 +1424,7 @@ Provider is dependency factory which creates dependency.
1391
1424
 
1392
1425
  - `Provider.fromClass(Logger)`
1393
1426
  - `Provider.fromValue(logger)`
1394
- - `new Provider((container, options) => container.resolve(Logger, options))`
1427
+ - `new Provider(({ scope, ...options }) => scope.resolve(Logger, options))`
1395
1428
 
1396
1429
  ```typescript
1397
1430
  import { arg, bindTo, Container, inject, lazy, Provider, register, Registration as R } from 'ts-ioc-container';
@@ -1466,7 +1499,7 @@ describe('Provider', () => {
1466
1499
 
1467
1500
  const container = new Container().register(
1468
1501
  'FileService',
1469
- Provider.fromClass(FileService).addArgsFn((_, { args = [] } = {}) => [...args, '/var/data']),
1502
+ Provider.fromClass(FileService).addArgsFn(({ args = [] }) => [...args, '/var/data']),
1470
1503
  );
1471
1504
 
1472
1505
  const service = container.resolve<FileService>('FileService');
@@ -1481,7 +1514,7 @@ describe('Provider', () => {
1481
1514
  const container = new Container().register('DbPath', Provider.fromValue('localhost:5432')).register(
1482
1515
  'Database',
1483
1516
  // Dynamically resolve connection string at creation time
1484
- Provider.fromClass(Database).addArgsFn((scope) => [`postgres://${scope.resolve('DbPath')}`]),
1517
+ Provider.fromClass(Database).addArgsFn(({ scope }) => [`postgres://${scope.resolve('DbPath')}`]),
1485
1518
  );
1486
1519
 
1487
1520
  const db = container.resolve<Database>('Database');
@@ -1771,7 +1804,7 @@ describe('Auto resolve', function () {
1771
1804
  Sometimes you want to bind some arguments to provider.
1772
1805
 
1773
1806
  - `provider(appendArgs('someArgument'))`
1774
- - `provider(appendArgsFn((container) => [container.resolve(Logger), 'someValue']))`
1807
+ - `provider(appendArgsFn(({ scope }) => [scope.resolve(Logger), 'someValue']))`
1775
1808
  - `Provider.fromClass(Logger).pipe(appendArgs('someArgument'))`
1776
1809
 
1777
1810
  ### Dependencies as arguments
@@ -1783,7 +1816,7 @@ Args are passed to the constructor **as-is** — the library never resolves an `
1783
1816
  - `ServiceToken.args('literal')` — literal value passed directly
1784
1817
  - `ServiceToken.args(ValueToken)` — the token object itself is passed as arg
1785
1818
 
1786
- `argToToken(value)` is the helper for a call site that wants "resolve tokens, pass literals through": it returns an `InjectionToken` as-is and wraps anything else in a `ConstantToken`, so `@inject((scope, { args = [] }) => argToToken(args[0]).resolve(scope))` accepts either.
1819
+ `argToToken(value)` is the helper for a call site that wants "resolve tokens, pass literals through": it returns an `InjectionToken` as-is and wraps anything else in a `ConstantToken`, so `@inject(({ scope, args = [] }) => argToToken(args[0]).resolve(scope))` accepts either.
1787
1820
 
1788
1821
  ### Positional arg injection with `arg(index)`, `args`, and `argsFn`
1789
1822
 
@@ -1793,7 +1826,7 @@ Constructor parameters that should pick up positional args from `ProviderOptions
1793
1826
  - `@inject(args)` — resolves the whole runtime `args` array
1794
1827
  - Works together with `token.args(...)` / `token.argsFn(...)` to pass typed dependencies through the args context
1795
1828
 
1796
- `argsFn(predicate)` is the general form: it iterates the runtime `args` array and returns the **first argument matching** `predicate(value, index)` — think `args.find(predicate)`. `arg(index)` is just a shortcut for matching by position: `arg(0)` is `argsFn((value, index) => index === 0)`. `args` is `(scope, options) => options.args`, i.e. it returns the runtime args array as-is. Every `InjectFn` receives `(scope, options)`, where `options.args` is the runtime args array.
1829
+ `argsFn(predicate)` is the general form: it iterates the runtime `args` array and returns the **first argument matching** `predicate(value, index)` — think `args.find(predicate)`. `arg(index)` is just a shortcut for matching by position: `arg(0)` is `argsFn((value, index) => index === 0)`. `args` is `({ args }) => args`, i.e. it returns the runtime args array as-is. Every `InjectFn` receives one `ProviderOptions` object — `{ scope, args, lazy }` — where `args` is the runtime args array.
1797
1830
 
1798
1831
  `findOrFail(predicate)` is the strict counterpart for `singleton()` cache keys — `singleton(findOrFail(isUserId))` keys a per-argument singleton, for example. It receives the full runtime `args` array, returns the first argument matching `predicate(value)`, and throws `ArgumentNotFoundError` when none does, instead of silently handing out `undefined`.
1799
1832
 
@@ -1801,7 +1834,7 @@ Constructor parameters that should pick up positional args from `ProviderOptions
1801
1834
 
1802
1835
  Every container-backed token (`SingleToken`, `ClassToken`, `SingleAliasToken`, `GroupAliasToken`, `FunctionToken`) forwards the `args` of its own `resolve` call to the provider, exactly like `container.resolve(key, { args })` does. `token.args(...)` and `token.argsFn(...)` **append after** the runtime args, so `token.args('x').resolve(container, { args: ['r'] })` hands the provider `['r', 'x']`.
1803
1836
 
1804
- Because `@inject(token)` parameters are resolved with the args of the class being constructed, those args **cascade** into every injected dependency: they reach the dependency's provider, its `@inject(arg(index))` parameters, its `scopeAccess` rule and its `singleton()` cache key. That is what lets a per-user `UserService` share a per-user `UserRepository` without passing the id along by hand:
1837
+ Every `@inject` function is called with the args of the class being constructed, so an `InjectFn` which hands them on — `by(Token)` does, being `({ scope, ...options }) => Token.resolve(scope, options)` — **cascades** them into the injected dependency: they reach the dependency's provider, its `@inject(arg(index))` parameters, its `scopeAccess` rule and its `singleton()` cache key. One that does not — `({ scope }) => Token.resolve(scope)` — resolves the dependency with no args at all; which one a parameter wants is written at the parameter. The cascade is what lets a per-user `UserService` share a per-user `UserRepository` without passing the id along by hand:
1805
1838
 
1806
1839
  ```typescript
1807
1840
  import {
@@ -1814,6 +1847,7 @@ import {
1814
1847
  Registration as R,
1815
1848
  singleton,
1816
1849
  SingleToken,
1850
+ by,
1817
1851
  } from 'ts-ioc-container';
1818
1852
 
1819
1853
  interface IUserRepository {
@@ -1830,7 +1864,7 @@ class UserRepository implements IUserRepository {
1830
1864
  }
1831
1865
 
1832
1866
  class UserService {
1833
- constructor(@inject(IUserRepositoryKey) public repository: IUserRepository) {}
1867
+ constructor(@inject(by(IUserRepositoryKey)) public repository: IUserRepository) {}
1834
1868
  }
1835
1869
 
1836
1870
  describe('Token Runtime Arguments', function () {
@@ -1881,6 +1915,7 @@ import {
1881
1915
  Registration as R,
1882
1916
  SingleToken,
1883
1917
  singleton,
1918
+ by,
1884
1919
  } from 'ts-ioc-container';
1885
1920
 
1886
1921
  /**
@@ -1938,7 +1973,7 @@ describe('IProvider', function () {
1938
1973
  }
1939
1974
 
1940
1975
  // Extract 'env' from Config service dynamically
1941
- @register(appendArgsFn((scope) => [scope.resolve<Config>('Config').env]))
1976
+ @register(appendArgsFn(({ scope }) => [scope.resolve<Config>('Config').env]))
1942
1977
  class Service {
1943
1978
  constructor(@inject(arg(0)) public env: string) {}
1944
1979
  }
@@ -1974,7 +2009,7 @@ describe('IProvider', function () {
1974
2009
  tenant = 'tenant-a';
1975
2010
  }
1976
2011
 
1977
- @register(appendArgs('fixed'), appendArgsFn((scope) => [scope.resolve<Config>('Config').tenant]))
2012
+ @register(appendArgs('fixed'), appendArgsFn(({ scope }) => [scope.resolve<Config>('Config').tenant]))
1978
2013
  class Service {
1979
2014
  constructor(
1980
2015
  @inject(arg(0)) public runtime: string,
@@ -2033,11 +2068,11 @@ describe('IProvider', function () {
2033
2068
  class App {
2034
2069
  constructor(
2035
2070
  // Inject EntityManager configured for Users
2036
- @inject(withRepository(UserRepositoryToken))
2071
+ @inject(by(withRepository(UserRepositoryToken)))
2037
2072
  public userManager: EntityManager,
2038
2073
 
2039
2074
  // Inject EntityManager configured for Todos
2040
- @inject(withRepository(TodoRepositoryToken))
2075
+ @inject(by(withRepository(TodoRepositoryToken)))
2041
2076
  public todoManager: EntityManager,
2042
2077
  ) {}
2043
2078
  }
@@ -2200,6 +2235,7 @@ import {
2200
2235
  Registration as R,
2201
2236
  scope,
2202
2237
  select as s,
2238
+ by,
2203
2239
  } from 'ts-ioc-container';
2204
2240
 
2205
2241
  /**
@@ -2254,7 +2290,10 @@ describe('alias', () => {
2254
2290
  it('should notify through all channels', () => {
2255
2291
  // NotificationManager broadcasts to ALL registered channels
2256
2292
  class NotificationManager {
2257
- constructor(@inject(s.alias(INotificationChannel)) private channels: INotificationChannel[]) {}
2293
+ constructor(
2294
+ @inject(by(s.alias(INotificationChannel)))
2295
+ private channels: INotificationChannel[],
2296
+ ) {}
2258
2297
 
2259
2298
  notifyUser(userId: string, message: string): void {
2260
2299
  for (const channel of this.channels) {
@@ -2346,6 +2385,7 @@ import {
2346
2385
  Registration as R,
2347
2386
  select as s,
2348
2387
  singleton,
2388
+ by,
2349
2389
  } from 'ts-ioc-container';
2350
2390
 
2351
2391
  /**
@@ -2391,7 +2431,7 @@ describe('Decorator Pattern', () => {
2391
2431
  class LoggingRepository implements IRepository {
2392
2432
  constructor(
2393
2433
  @inject(arg(0)) private repository: IRepository,
2394
- @inject(s.token('Logger').lazy()) private logger: Logger,
2434
+ @inject(by(s.token('Logger').lazy())) private logger: Logger,
2395
2435
  ) {}
2396
2436
 
2397
2437
  async save(item: Todo): Promise<void> {
@@ -2415,7 +2455,7 @@ describe('Decorator Pattern', () => {
2415
2455
  }
2416
2456
 
2417
2457
  class App {
2418
- constructor(@inject('IRepository') public repository: IRepository) {}
2458
+ constructor(@inject(by('IRepository')) public repository: IRepository) {}
2419
2459
 
2420
2460
  async run() {
2421
2461
  await this.repository.save({ id: '1', text: 'Buy groceries' });
@@ -2533,7 +2573,7 @@ Registration is provider factory which registers provider in container.
2533
2573
  - `Registration.fromClass(Logger).bindTo('logger')`
2534
2574
  - `Registration.fromClass(Logger)`
2535
2575
  - `Registration.fromValue(Logger)`
2536
- - `Registration.fromFn((container, options) => container.resolve(Logger, options))`
2576
+ - `Registration.fromFn(({ scope, ...options }) => scope.resolve(Logger, options))`
2537
2577
 
2538
2578
  ### Token
2539
2579
 
@@ -2673,6 +2713,7 @@ import {
2673
2713
  scope,
2674
2714
  select,
2675
2715
  singleton,
2716
+ by,
2676
2717
  } from 'ts-ioc-container';
2677
2718
 
2678
2719
  /**
@@ -2732,7 +2773,10 @@ describe('Scopes', function () {
2732
2773
 
2733
2774
  // RequestHandler can create a transaction scope for database operations
2734
2775
  class RequestHandler {
2735
- constructor(@inject(select.scope.create({ tags: ['transaction'] })) public transactionScope: IContainer) {}
2776
+ constructor(
2777
+ @inject(by(select.scope.create({ tags: ['transaction'] })))
2778
+ public transactionScope: IContainer,
2779
+ ) {}
2736
2780
 
2737
2781
  executeInTransaction(): boolean {
2738
2782
  // Transaction scope inherits from request scope
@@ -2751,6 +2795,98 @@ describe('Scopes', function () {
2751
2795
 
2752
2796
  ```
2753
2797
 
2798
+ ### Composing decorators
2799
+
2800
+ A decorator stack repeated on every class of a layer is worth a name.
2801
+ `createComposeClassDecorator(...decorators)` gives it one: it returns a single
2802
+ `ClassDecorator` which applies the stack **bottom-up, exactly as stacking would**,
2803
+ so `@createComposeClassDecorator(a, b)` behaves like `@a @b`. A decorator which
2804
+ returns a replacement class hands it to the next one, the way the runtime threads
2805
+ a stack.
2806
+
2807
+ `createComposeMethodDecorator` and `createComposeParameterDecorator` do the same
2808
+ for methods and constructor parameters. The method form threads the property
2809
+ descriptor, so wrapping decorators (`@once`, `@throttle`, ...) compose too.
2810
+
2811
+ ```typescript
2812
+ import {
2813
+ addClassMeta,
2814
+ Container,
2815
+ createComposeClassDecorator,
2816
+ createComposeParameterDecorator,
2817
+ getClassMeta,
2818
+ inject,
2819
+ by,
2820
+ addParamLabel,
2821
+ getParamLabels,
2822
+ register,
2823
+ Registration as R,
2824
+ scope,
2825
+ singleton,
2826
+ SingleToken,
2827
+ } from 'ts-ioc-container';
2828
+
2829
+ /**
2830
+ * A decorator stack repeated on every class of a layer is worth a name.
2831
+ * `createComposeClassDecorator` (and its `createComposeMethodDecorator` /
2832
+ * `createComposeParameterDecorator` siblings) turns one into a single decorator,
2833
+ * applied bottom-up exactly as stacking would.
2834
+ */
2835
+ describe('composing decorators', () => {
2836
+ const INJECTION_TOKEN = 'injection-token';
2837
+
2838
+ // Every repository binds to its own token, is an application-scoped singleton,
2839
+ // and remembers the token it was registered under.
2840
+ const repository = <T>(token: SingleToken<T>) =>
2841
+ createComposeClassDecorator(
2842
+ register(
2843
+ token,
2844
+ scope((s) => s.hasTag('application')),
2845
+ singleton(),
2846
+ ),
2847
+ addClassMeta(INJECTION_TOKEN, () => token),
2848
+ );
2849
+
2850
+ it('should apply the whole stack the composed decorator stands for', () => {
2851
+ const UserRepositoryToken = new SingleToken<UserRepository>('IUserRepository');
2852
+
2853
+ @repository(UserRepositoryToken)
2854
+ class UserRepository {
2855
+ findById(id: string) {
2856
+ return { id };
2857
+ }
2858
+ }
2859
+
2860
+ const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(UserRepository));
2861
+
2862
+ expect(UserRepositoryToken.resolve(app)).toBeInstanceOf(UserRepository);
2863
+ // singleton() applied, so the same instance comes back
2864
+ expect(UserRepositoryToken.resolve(app)).toBe(UserRepositoryToken.resolve(app));
2865
+ // and the class still carries the metadata written beside the registration
2866
+ expect(getClassMeta(UserRepository, INJECTION_TOKEN)).toBe(UserRepositoryToken);
2867
+ });
2868
+
2869
+ it('should compose parameter decorators the same way', () => {
2870
+ const ConfigToken = new SingleToken<{ apiUrl: string }>('IConfig');
2871
+
2872
+ // @inject plus a label describing where the value came from
2873
+ const fromConfig = createComposeParameterDecorator(inject(by(ConfigToken)), addParamLabel('source', 'config'));
2874
+
2875
+ class ApiClient {
2876
+ constructor(@fromConfig public config: { apiUrl: string }) {}
2877
+ }
2878
+
2879
+ const app = new Container({ tags: ['application'] }).addRegistration(
2880
+ R.fromValue({ apiUrl: 'https://api.example.com' }).bindTo(ConfigToken),
2881
+ );
2882
+
2883
+ expect(app.resolve(ApiClient).config.apiUrl).toBe('https://api.example.com');
2884
+ expect(getParamLabels(ApiClient, 0).get('source')).toBe('config');
2885
+ });
2886
+ });
2887
+
2888
+ ```
2889
+
2754
2890
  ## Module
2755
2891
 
2756
2892
  Sometimes you want to encapsulate registration logic in separate module. This is what `IContainerModule` is for.
@@ -3103,6 +3239,7 @@ import {
3103
3239
  toTask,
3104
3240
  type ExecutionContext,
3105
3241
  type HookAction,
3242
+ by,
3106
3243
  } from 'ts-ioc-container';
3107
3244
 
3108
3245
  const execute: HookFn = (ctx) => {
@@ -3151,7 +3288,7 @@ describe('onConstruct', function () {
3151
3288
  connectionString = '';
3152
3289
 
3153
3290
  @onConstruct(execute)
3154
- connect(@inject('ConnectionString') connectionString: string) {
3291
+ connect(@inject(by('ConnectionString')) connectionString: string) {
3155
3292
  this.connectionString = connectionString;
3156
3293
  this.isConnected = true;
3157
3294
  }
@@ -3229,7 +3366,7 @@ describe('onConstruct', function () {
3229
3366
  }
3230
3367
 
3231
3368
  @onConstruct(executeAsync)
3232
- async connect(@inject('ConnectionString') connectionString: string) {
3369
+ async connect(@inject(by('ConnectionString')) connectionString: string) {
3233
3370
  await Promise.resolve();
3234
3371
  this.connectionString = connectionString;
3235
3372
  this.isConnected = true;
@@ -3300,6 +3437,7 @@ import {
3300
3437
  type ExecutionContext,
3301
3438
  type HookAction,
3302
3439
  type IContainerModule,
3440
+ by,
3303
3441
  } from 'ts-ioc-container';
3304
3442
 
3305
3443
  const execute: HookFn = (ctx) => {
@@ -3349,7 +3487,7 @@ class LogsRepo {
3349
3487
  class Logger {
3350
3488
  private messages: string[] = [];
3351
3489
 
3352
- constructor(@inject('logsRepo') private logsRepo: LogsRepo) {}
3490
+ constructor(@inject(by('logsRepo')) private logsRepo: LogsRepo) {}
3353
3491
 
3354
3492
  log(message: string): void {
3355
3493
  this.messages.push(message);
@@ -3384,7 +3522,7 @@ describe('onScopeDisposed', function () {
3384
3522
 
3385
3523
  ```typescript
3386
3524
  import 'reflect-metadata';
3387
- import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask } from 'ts-ioc-container';
3525
+ import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask, by } from 'ts-ioc-container';
3388
3526
 
3389
3527
  /**
3390
3528
  * UI Components - Property Injection
@@ -3404,7 +3542,7 @@ describe('inject property', () => {
3404
3542
 
3405
3543
  class UserViewModel {
3406
3544
  // Inject 'GreetingService' into 'greeting' property during 'onInit'
3407
- @hook('onInit', injectProp('GreetingService'))
3545
+ @hook('onInit', injectProp(by('GreetingService')))
3408
3546
  greetingService!: string;
3409
3547
 
3410
3548
  display(): string {
@@ -3435,7 +3573,7 @@ describe('inject property', () => {
3435
3573
  class UserViewModel {
3436
3574
  @hook(
3437
3575
  'onInit',
3438
- sequential(injectProp('GreetingService'), (context) => {
3576
+ sequential(injectProp(by('GreetingService')), (context) => {
3439
3577
  injectedValue = context.getProperty();
3440
3578
  }),
3441
3579
  )