ts-ioc-container 68.1.0 → 69.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 CHANGED
@@ -1774,13 +1774,16 @@ Sometimes you want to bind some arguments to provider.
1774
1774
  - `provider(appendArgsFn((container) => [container.resolve(Logger), 'someValue']))`
1775
1775
  - `Provider.fromClass(Logger).pipe(appendArgs('someArgument'))`
1776
1776
 
1777
- ### Token as argument
1777
+ ### Dependencies as arguments
1778
1778
 
1779
- When you pass an `InjectionToken` via `token.args(...)`, the container resolves it before the value reaches the constructor. Bare constructors are **not** auto-resolved — wrap a class in `ClassToken` to opt into resolution.
1779
+ Args are passed to the constructor **as-is** — the library never resolves an `InjectionToken` (or a bare constructor) it finds in the args list. Resolve it at the call site with `argsFn`:
1780
1780
 
1781
- - `ServiceToken.args(ValueToken)` — `ValueToken` is resolved from the container, its value is passed as arg
1782
- - `ServiceToken.args(new ClassToken(SomeService))` — `SomeService` is constructed by the container
1781
+ - `ServiceToken.argsFn((scope) => [ValueToken.resolve(scope)])` — `ValueToken` is resolved from the container, its value is passed as arg
1782
+ - `ServiceToken.argsFn((scope) => [scope.resolve(SomeService)])` — `SomeService` is constructed by the container
1783
1783
  - `ServiceToken.args('literal')` — literal value passed directly
1784
+ - `ServiceToken.args(ValueToken)` — the token object itself is passed as arg
1785
+
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.
1784
1787
 
1785
1788
  ### Positional arg injection with `arg(index)`, `args`, and `argsFn`
1786
1789
 
@@ -1788,12 +1791,73 @@ Constructor parameters that should pick up positional args from `ProviderOptions
1788
1791
 
1789
1792
  - `@inject(arg(0))` — resolves the first element of the `args` array passed at resolution time
1790
1793
  - `@inject(args)` — resolves the whole runtime `args` array
1791
- - Works together with `token.args(...)` to pass typed dependencies through the args context
1794
+ - Works together with `token.args(...)` / `token.argsFn(...)` to pass typed dependencies through the args context
1792
1795
 
1793
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.
1794
1797
 
1795
1798
  `findOrFail(predicate)` is the strict, variadic counterpart for places that take the raw args list — `singleton(findOrFail(isUserId))` keys a per-argument singleton, for example. It returns the first argument matching `predicate(value)` and throws `ArgumentNotFoundError` when none does, instead of silently handing out `undefined`.
1796
1799
 
1800
+ ### Runtime args flow through tokens
1801
+
1802
+ 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
+
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:
1805
+
1806
+ ```typescript
1807
+ import {
1808
+ arg,
1809
+ bindTo,
1810
+ Container,
1811
+ findOrFail,
1812
+ inject,
1813
+ register,
1814
+ Registration as R,
1815
+ singleton,
1816
+ SingleToken,
1817
+ } from 'ts-ioc-container';
1818
+
1819
+ interface IUserRepository {
1820
+ userId: string;
1821
+ }
1822
+
1823
+ const IUserRepositoryKey = new SingleToken<IUserRepository>('IUserRepository');
1824
+ const isUserId = (value: unknown): value is string => typeof value === 'string';
1825
+
1826
+ // one repository per user id - the id is the singleton cache key
1827
+ @register(bindTo(IUserRepositoryKey), singleton(findOrFail<string>(isUserId)))
1828
+ class UserRepository implements IUserRepository {
1829
+ constructor(@inject(arg(0)) public userId: string) {}
1830
+ }
1831
+
1832
+ class UserService {
1833
+ constructor(@inject(IUserRepositoryKey) public repository: IUserRepository) {}
1834
+ }
1835
+
1836
+ describe('Token Runtime Arguments', function () {
1837
+ it('should forward runtime args to the provider behind the token', function () {
1838
+ const container = new Container().addRegistration(R.fromClass(UserRepository));
1839
+
1840
+ expect(IUserRepositoryKey.resolve(container, { args: ['user-1'] }).userId).toBe('user-1');
1841
+ });
1842
+
1843
+ it('should cascade the runtime args of a class into its injected dependencies', function () {
1844
+ const container = new Container().addRegistration(R.fromClass(UserRepository));
1845
+
1846
+ const service = container.resolve(UserService, { args: ['user-1'] });
1847
+ const sameUser = container.resolve(UserService, { args: ['user-1'] });
1848
+ const otherUser = container.resolve(UserService, { args: ['user-2'] });
1849
+
1850
+ expect(service.repository.userId).toBe('user-1');
1851
+ expect(sameUser.repository).toBe(service.repository);
1852
+ expect(otherUser.repository.userId).toBe('user-2');
1853
+ });
1854
+ });
1855
+
1856
+ ```
1857
+
1858
+ > [!IMPORTANT]
1859
+ > Runtime args come first. A dependency that reads `@inject(arg(0))` sees the *caller's* first runtime arg whenever the caller was resolved with args, even if its token was specialized with `token.args(...)`. Pick args by shape (`argsFn(predicate)`, `findOrFail(predicate)`) rather than by position when a class can be resolved with runtime args and its dependencies are specialized with `token.args(...)`.
1860
+
1797
1861
  ### Immutable token chaining
1798
1862
 
1799
1863
  `token.args(...)`, `token.argsFn(...)`, and `token.lazy()` all return **new token instances** — the parent token is never mutated. This allows the same token to be specialized in multiple independent ways (one-way linked list: parent → many children).
@@ -1951,14 +2015,16 @@ describe('IProvider', function () {
1951
2015
  }
1952
2016
 
1953
2017
  // EntityManager is generic - it works with ANY repository.
1954
- // The repository is the first arg passed via `EntityManagerToken.args(...)`.
1955
- // `@inject(arg(0))` reads it; the container auto-resolves InjectionToken args
1956
- // before they reach the constructor.
2018
+ // The repository is the first arg; `@inject(arg(0))` reads it. Args are
2019
+ // passed through as-is, so the call site resolves the repository token
2020
+ // itself with `argsFn` before it reaches the constructor.
1957
2021
  const EntityManagerToken = new SingleToken<EntityManager>('EntityManager');
2022
+ const withRepository = (token: SingleToken<IRepository>) =>
2023
+ EntityManagerToken.argsFn((scope) => [token.resolve(scope)]);
1958
2024
 
1959
2025
  @register(
1960
2026
  bindTo(EntityManagerToken),
1961
- singleton((arg1) => (arg1 as SingleToken).token), // Cache unique instance per repository type
2027
+ singleton((repository) => (repository as IRepository).name), // Cache unique instance per repository type
1962
2028
  )
1963
2029
  class EntityManager {
1964
2030
  constructor(@inject(arg(0)) public repository: IRepository) {}
@@ -1967,11 +2033,11 @@ describe('IProvider', function () {
1967
2033
  class App {
1968
2034
  constructor(
1969
2035
  // Inject EntityManager configured for Users
1970
- @inject(EntityManagerToken.args(UserRepositoryToken))
2036
+ @inject(withRepository(UserRepositoryToken))
1971
2037
  public userManager: EntityManager,
1972
2038
 
1973
2039
  // Inject EntityManager configured for Todos
1974
- @inject(EntityManagerToken.args(TodoRepositoryToken))
2040
+ @inject(withRepository(TodoRepositoryToken))
1975
2041
  public todoManager: EntityManager,
1976
2042
  ) {}
1977
2043
  }
@@ -1995,14 +2061,14 @@ describe('IProvider', function () {
1995
2061
  .addRegistration(R.fromClass(TodoRepository));
1996
2062
 
1997
2063
  // Resolve user manager twice
1998
- const userManager1 = EntityManagerToken.args(UserRepositoryToken).resolve(root);
1999
- const userManager2 = EntityManagerToken.args(UserRepositoryToken).resolve(root);
2064
+ const userManager1 = withRepository(UserRepositoryToken).resolve(root);
2065
+ const userManager2 = withRepository(UserRepositoryToken).resolve(root);
2000
2066
 
2001
2067
  // Should be same instance (cached)
2002
2068
  expect(userManager1).toBe(userManager2);
2003
2069
 
2004
2070
  // Resolve todo manager
2005
- const todoManager = EntityManagerToken.args(TodoRepositoryToken).resolve(root);
2071
+ const todoManager = withRepository(TodoRepositoryToken).resolve(root);
2006
2072
 
2007
2073
  // Should be different from user manager
2008
2074
  expect(todoManager).not.toBe(userManager1);
@@ -27,6 +27,6 @@ const args = (c, { args = [] }) => args;
27
27
  exports.args = args;
28
28
  const resolveArgs = (target, methodName) => {
29
29
  const tokens = (0, parameter_1.getParamMeta)(hookMetaKey(methodName), target);
30
- return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args: args.map(toToken_1.argToToken).map((t) => t.resolve(scope)), lazy }));
30
+ return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args, lazy }));
31
31
  };
32
32
  exports.resolveArgs = resolveArgs;
@@ -6,7 +6,7 @@ class ClassToken extends InjectionToken_1.InjectionToken {
6
6
  target;
7
7
  _getArgsFn;
8
8
  _isLazy;
9
- constructor(target, { getArgsFn = () => [], isLazy = false } = {}) {
9
+ constructor(target, { getArgsFn = InjectionToken_1.forwardArgs, isLazy = false } = {}) {
10
10
  super();
11
11
  this.target = target;
12
12
  this._getArgsFn = getArgsFn;
@@ -6,7 +6,7 @@ class FunctionToken extends InjectionToken_1.InjectionToken {
6
6
  fn;
7
7
  _getArgsFn;
8
8
  _isLazy;
9
- constructor(fn, { getArgsFn = (_, { args = [] } = {}) => args, isLazy = false } = {}) {
9
+ constructor(fn, { getArgsFn = InjectionToken_1.forwardArgs, isLazy = false } = {}) {
10
10
  super();
11
11
  this.fn = fn;
12
12
  this._getArgsFn = getArgsFn;
@@ -6,7 +6,7 @@ class GroupAliasToken extends InjectionToken_1.InjectionToken {
6
6
  token;
7
7
  _getArgsFn;
8
8
  _isLazy;
9
- constructor(token, { getArgsFn = () => [], isLazy = false } = {}) {
9
+ constructor(token, { getArgsFn = InjectionToken_1.forwardArgs, isLazy = false } = {}) {
10
10
  super();
11
11
  this.token = token;
12
12
  this._getArgsFn = getArgsFn;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.InjectionToken = void 0;
3
+ exports.InjectionToken = exports.forwardArgs = void 0;
4
4
  exports.isInjectionToken = isInjectionToken;
5
5
  const basic_1 = require("../utils/basic");
6
+ const forwardArgs = (_, { args = [] } = {}) => args;
7
+ exports.forwardArgs = forwardArgs;
6
8
  class InjectionToken {
7
9
  }
8
10
  exports.InjectionToken = InjectionToken;
@@ -6,7 +6,7 @@ class SingleAliasToken extends InjectionToken_1.InjectionToken {
6
6
  token;
7
7
  _getArgsFn;
8
8
  _isLazy;
9
- constructor(token, { getArgsFn = () => [], isLazy = false } = {}) {
9
+ constructor(token, { getArgsFn = InjectionToken_1.forwardArgs, isLazy = false } = {}) {
10
10
  super();
11
11
  this.token = token;
12
12
  this._getArgsFn = getArgsFn;
@@ -6,7 +6,7 @@ class SingleToken extends InjectionToken_1.InjectionToken {
6
6
  token;
7
7
  _getArgsFn;
8
8
  _isLazy;
9
- constructor(token, { getArgsFn = () => [], isLazy = false } = {}) {
9
+ constructor(token, { getArgsFn = InjectionToken_1.forwardArgs, isLazy = false } = {}) {
10
10
  super();
11
11
  this.token = token;
12
12
  this._getArgsFn = getArgsFn;
@@ -1,7 +1,7 @@
1
1
  import { Injector } from './IInjector.js';
2
2
  import { resolveConstructor } from '../metadata/target.js';
3
- import { getParamMeta, addParamMeta } from '../metadata/parameter.js';
4
- import { argToToken, toMappedToken } from '../token/toToken.js';
3
+ import { addParamMeta, getParamMeta } from '../metadata/parameter.js';
4
+ import { toMappedToken } from '../token/toToken.js';
5
5
  export class MetadataInjector extends Injector {
6
6
  createInstance(scope, Target, { args: deps = [] } = {}) {
7
7
  const args = resolveArgs(Target)(scope, { args: deps });
@@ -19,5 +19,5 @@ export const arg = (index) => argsFn((value, i) => i === index);
19
19
  export const args = (c, { args = [] }) => args;
20
20
  export const resolveArgs = (target, methodName) => {
21
21
  const tokens = getParamMeta(hookMetaKey(methodName), target);
22
- return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args: args.map(argToToken).map((t) => t.resolve(scope)), lazy }));
22
+ return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args, lazy }));
23
23
  };
@@ -1,9 +1,9 @@
1
- import { InjectionToken } from './InjectionToken.js';
1
+ import { forwardArgs, InjectionToken } from './InjectionToken.js';
2
2
  export class ClassToken extends InjectionToken {
3
3
  target;
4
4
  _getArgsFn;
5
5
  _isLazy;
6
- constructor(target, { getArgsFn = () => [], isLazy = false } = {}) {
6
+ constructor(target, { getArgsFn = forwardArgs, isLazy = false } = {}) {
7
7
  super();
8
8
  this.target = target;
9
9
  this._getArgsFn = getArgsFn;
@@ -1,9 +1,9 @@
1
- import { InjectionToken } from './InjectionToken.js';
1
+ import { forwardArgs, InjectionToken } from './InjectionToken.js';
2
2
  export class FunctionToken extends InjectionToken {
3
3
  fn;
4
4
  _getArgsFn;
5
5
  _isLazy;
6
- constructor(fn, { getArgsFn = (_, { args = [] } = {}) => args, isLazy = false } = {}) {
6
+ constructor(fn, { getArgsFn = forwardArgs, isLazy = false } = {}) {
7
7
  super();
8
8
  this.fn = fn;
9
9
  this._getArgsFn = getArgsFn;
@@ -1,9 +1,9 @@
1
- import { InjectionToken } from './InjectionToken.js';
1
+ import { forwardArgs, InjectionToken } from './InjectionToken.js';
2
2
  export class GroupAliasToken extends InjectionToken {
3
3
  token;
4
4
  _getArgsFn;
5
5
  _isLazy;
6
- constructor(token, { getArgsFn = () => [], isLazy = false } = {}) {
6
+ constructor(token, { getArgsFn = forwardArgs, isLazy = false } = {}) {
7
7
  super();
8
8
  this.token = token;
9
9
  this._getArgsFn = getArgsFn;
@@ -1,4 +1,5 @@
1
1
  import { Is } from '../utils/basic.js';
2
+ export const forwardArgs = (_, { args = [] } = {}) => args;
2
3
  export class InjectionToken {
3
4
  }
4
5
  export function isInjectionToken(target) {
@@ -1,9 +1,9 @@
1
- import { InjectionToken } from './InjectionToken.js';
1
+ import { forwardArgs, InjectionToken } from './InjectionToken.js';
2
2
  export class SingleAliasToken extends InjectionToken {
3
3
  token;
4
4
  _getArgsFn;
5
5
  _isLazy;
6
- constructor(token, { getArgsFn = () => [], isLazy = false } = {}) {
6
+ constructor(token, { getArgsFn = forwardArgs, isLazy = false } = {}) {
7
7
  super();
8
8
  this.token = token;
9
9
  this._getArgsFn = getArgsFn;
@@ -1,9 +1,9 @@
1
- import { InjectionToken } from './InjectionToken.js';
1
+ import { forwardArgs, InjectionToken } from './InjectionToken.js';
2
2
  export class SingleToken extends InjectionToken {
3
3
  token;
4
4
  _getArgsFn;
5
5
  _isLazy;
6
- constructor(token, { getArgsFn = () => [], isLazy = false } = {}) {
6
+ constructor(token, { getArgsFn = forwardArgs, isLazy = false } = {}) {
7
7
  super();
8
8
  this.token = token;
9
9
  this._getArgsFn = getArgsFn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-ioc-container",
3
- "version": "68.1.0",
3
+ "version": "69.0.0",
4
4
  "description": "Fast, lightweight TypeScript dependency injection container with a clean API, scoped lifecycles, decorators, tokens, hooks, lazy injection, customizable providers, and no global container objects.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,5 +1,6 @@
1
1
  import { type IContainer } from '../container/IContainer.js';
2
- import { ProviderOptions } from '../provider/IProvider.js';
2
+ import { ArgsFn, ProviderOptions } from '../provider/IProvider.js';
3
+ export declare const forwardArgs: ArgsFn;
3
4
  export declare abstract class InjectionToken<T = any> {
4
5
  abstract resolve(s: IContainer, options?: ProviderOptions): T;
5
6
  abstract args(...deps: unknown[]): InjectionToken<T>;