├── .editorconfig
├── .eslintrc
├── .github
└── workflows
│ ├── docs.yml
│ └── main.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest.config.js
├── lemon.svg
├── lerna.json
├── media
├── diagram.puml
├── diagram.svg
└── lemon.svg
├── package-lock.json
├── package.json
├── packages
├── ditox-react
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── DependencyContainer.test.tsx
│ │ ├── DependencyContainer.tsx
│ │ ├── DependencyModule.test.tsx
│ │ ├── DependencyModule.tsx
│ │ ├── hooks.test.tsx
│ │ ├── hooks.ts
│ │ └── index.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
└── ditox
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── index.ts
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ ├── container.test.ts
│ ├── container.ts
│ ├── index.ts
│ ├── modules.test.ts
│ ├── modules.ts
│ ├── tokens.test.ts
│ ├── tokens.ts
│ ├── utils.test.ts
│ └── utils.ts
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── tsconfig.json
└── typedoc.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint", "jest", "react-hooks"],
5 | "extends": [
6 | "eslint:recommended",
7 | "plugin:@typescript-eslint/eslint-recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | ],
10 | "rules": {
11 | "@typescript-eslint/no-explicit-any": "off",
12 | "react-hooks/rules-of-hooks": "error",
13 | "react-hooks/exhaustive-deps": "warn"
14 | },
15 | "env": {
16 | "jest/globals": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs to Pages
2 |
3 | on:
4 | # Runs on pushes targeting the default branch
5 | push:
6 | branches: ['master']
7 |
8 | # Allows you to run this workflow manually from the Actions tab
9 | workflow_dispatch:
10 |
11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
12 | permissions:
13 | contents: read
14 | pages: write
15 | id-token: write
16 |
17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
19 | concurrency:
20 | group: 'pages'
21 | cancel-in-progress: false
22 |
23 | jobs:
24 | # Build job
25 | build:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 | - name: Setup Node
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: '20'
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Install dependencies
37 | run: npm ci
38 | - name: Build Docs
39 | run: npm run build-docs
40 | - name: Upload artifact
41 | uses: actions/upload-pages-artifact@v3
42 | with:
43 | path: ./build/docs
44 |
45 | # Deployment job
46 | deploy:
47 | environment:
48 | name: github-pages
49 | url: ${{ steps.deployment.outputs.page_url }}
50 | runs-on: ubuntu-latest
51 | needs: build
52 | steps:
53 | - name: Deploy to GitHub Pages
54 | id: deployment
55 | uses: actions/deploy-pages@v4
56 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on: push
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v2
11 | with:
12 | node-version: '20'
13 | - name: Prepare
14 | run: npm ci
15 | - name: Lint
16 | run: npm run lint
17 | - name: Test
18 | run: npm run test -- --coverage
19 | - name: Coveralls
20 | uses: coverallsapp/github-action@master
21 | with:
22 | github-token: ${{ secrets.GITHUB_TOKEN }}
23 | - name: Build
24 | run: npm run build
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.vscode
3 | /build
4 | /coverage
5 | /dist
6 | /docs/api
7 | /packages/*/dist
8 | .DS_Store
9 | lerna-debug.log
10 | node_modules
11 | npm-debug.log
12 | Thumbs.db
13 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": false,
3 | "printWidth": 80,
4 | "proseWrap": "always",
5 | "semi": true,
6 | "singleQuote": true,
7 | "tabWidth": 2,
8 | "trailingComma": "all",
9 | "useTabs": false
10 | }
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See
4 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | ## [3.0.1](https://github.com/mnasyrov/ditox/compare/v3.0.0...v3.0.1) (2025-03-02)
7 |
8 | **Note:** Version bump only for package ditox-root
9 |
10 | # Change Log
11 |
12 | All notable changes to this project will be documented in this file. See
13 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
14 |
15 | # [3.0.0](https://github.com/mnasyrov/ditox/compare/v2.4.1...v3.0.0) (2024-07-12)
16 |
17 | ### Bug Fixes
18 |
19 | - Changed resolution of "scoped" bindings to keep a created value in the
20 | container which owns the factory
21 | ([#40](https://github.com/mnasyrov/ditox/issues/40))
22 | ([736ef2f](https://github.com/mnasyrov/ditox/commit/736ef2f927d43c91f027c68e230371cce3f50131))
23 | - Fixed binding modules
24 | ([997ca44](https://github.com/mnasyrov/ditox/commit/997ca44b09446a6e4d524ba9c16ce0c9cd7995d8))
25 |
26 | # Change Log
27 |
28 | All notable changes to this project will be documented in this file. See
29 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
30 |
31 | # [3.0.0-dev.2](https://github.com/mnasyrov/ditox/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2024-07-11)
32 |
33 | ### Bug Fixes
34 |
35 | - Fixed binding modules
36 | ([997ca44](https://github.com/mnasyrov/ditox/commit/997ca44b09446a6e4d524ba9c16ce0c9cd7995d8))
37 | - New factory binding doesn't reset a cached value
38 | ([7a33dc6](https://github.com/mnasyrov/ditox/commit/7a33dc63d5b95588c5ed5e7ad4e1e748230bcc2e))
39 |
40 | # Change Log
41 |
42 | All notable changes to this project will be documented in this file. See
43 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
44 |
45 | # [3.0.0-dev.1](https://github.com/mnasyrov/ditox/compare/v2.4.1...v3.0.0-dev.1) (2024-05-31)
46 |
47 | ### Bug Fixes
48 |
49 | - Changed resolution of "scoped" bindings to keep a created value in the
50 | container which owns the factory
51 | ([#40](https://github.com/mnasyrov/ditox/issues/40))
52 | ([736ef2f](https://github.com/mnasyrov/ditox/commit/736ef2f927d43c91f027c68e230371cce3f50131))
53 |
54 | # Change Log
55 |
56 | All notable changes to this project will be documented in this file. See
57 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
58 |
59 | ## [2.4.1](https://github.com/mnasyrov/ditox/compare/v2.4.0...v2.4.1) (2023-10-27)
60 |
61 | ### Bug Fixes
62 |
63 | - Add export CustomDependencyContainer to index
64 | ([#34](https://github.com/mnasyrov/ditox/issues/34))
65 | ([1723671](https://github.com/mnasyrov/ditox/commit/17236718c54c381ffca0a0c9160615aa26c79eaa))
66 |
67 | # Change Log
68 |
69 | All notable changes to this project will be documented in this file. See
70 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
71 |
72 | # [2.4.0](https://github.com/mnasyrov/ditox/compare/v2.3.1...v2.4.0) (2023-09-08)
73 |
74 | ### Features
75 |
76 | - Ability to create a shareable token by providing a key for its symbol
77 | ([4fde1d9](https://github.com/mnasyrov/ditox/commit/4fde1d95dc728018c67f8fc5e154597e9115d8b5))
78 |
79 | ### Reverts
80 |
81 | - Revert to TypeDoc 0.23
82 | ([e3e836e](https://github.com/mnasyrov/ditox/commit/e3e836e48ca55794359ed3e94197eb27977002ce))
83 |
84 | # Change Log
85 |
86 | All notable changes to this project will be documented in this file. See
87 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
88 |
89 | ## [2.3.1](https://github.com/mnasyrov/ditox/compare/v2.3.0...v2.3.1) (2023-04-01)
90 |
91 | **Note:** Version bump only for package ditox-root
92 |
93 | # Change Log
94 |
95 | All notable changes to this project will be documented in this file. See
96 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
97 |
98 | # [2.3.0](https://github.com/mnasyrov/ditox/compare/v2.2.0...v2.3.0) (2022-12-22)
99 |
100 | ### Bug Fixes
101 |
102 | - Compatible with Typescript 4.9. Updated dependencies.
103 | ([d8aa9e2](https://github.com/mnasyrov/ditox/commit/d8aa9e2a4a665d6e14f3581c652feb6ad01b71d8))
104 |
105 | # [2.2.0](https://github.com/mnasyrov/ditox/compare/v2.1.0...v2.2.0) (2022-01-29)
106 |
107 | ### Code Refactoring
108 |
109 | - Removed the deprecated code
110 | ([a821171](https://github.com/mnasyrov/ditox/commit/a821171e5de011b82f4f1aedbe91f3f6d2ef6360))
111 |
112 | ### BREAKING CHANGES
113 |
114 | _Use v2.1 version to migrate to the unified API._
115 |
116 | Removed the deprecated code:
117 |
118 | - `exportedProps` property of the module declaration
119 | - utilities:
120 | - `getValues()` is replaced by `tryResolveValues()`
121 | - `getProps()` is replaced by `tryResolveValue()`
122 | - `resolveProps()` is replaced by `resolveValue()`
123 | - `injectableProps()` is replaced by `injectable()`
124 |
125 | # [2.1.0](https://github.com/mnasyrov/ditox/compare/v2.0.0...v2.1.0) (2022-01-29)
126 |
127 | ### Features
128 |
129 | - Unified API of resolving utilities. Introduced "injectableClass()" utility.
130 | ([aea7a8f](https://github.com/mnasyrov/ditox/commit/aea7a8fdedae079e8bad86f8556669ab3257197a))
131 |
132 | # [2.0.0](https://github.com/mnasyrov/ditox/compare/v1.4.2...v2.0.0) (2022-01-26)
133 |
134 | ### chore
135 |
136 | - Dropped supporting of Flow.js typings.
137 | ([9a94f55](https://github.com/mnasyrov/ditox/commit/9a94f558daf107ff4744462e078571e4ccc7c444))
138 |
139 | ### BREAKING CHANGES
140 |
141 | - Dropped supporting of Flow.js typings. Use the previous versions of the
142 | library with Flow.js
143 |
144 | ## [1.4.2](https://github.com/mnasyrov/ditox/compare/v1.4.1...v1.4.2) (2021-11-18)
145 |
146 | ### Bug Fixes
147 |
148 | - Typings for `bindMultiValue()` utility function.
149 | ([cb2b2da](https://github.com/mnasyrov/ditox/commit/cb2b2da27ad7bd293d8d5591017422ba4599d09a))
150 |
151 | ## [1.4.1](https://github.com/mnasyrov/ditox/compare/v1.4.0...v1.4.1) (2021-11-11)
152 |
153 | ### Bug Fixes
154 |
155 | - beforeBinding() must be called before importing external modules.
156 | ([153c602](https://github.com/mnasyrov/ditox/commit/153c60256a4c2796e5a9f2d9c5115c3ff3f2f10b))
157 |
158 | # [1.4.0](https://github.com/mnasyrov/ditox/compare/v1.3.0...v1.4.0) (2021-10-30)
159 |
160 | ### Features
161 |
162 | - `ModuleDeclaration.imports` property takes module entries for binding with the
163 | module.
164 | ([6757bdc](https://github.com/mnasyrov/ditox/commit/6757bdc87a02d2336ca07661c50812a5823ca844))
165 |
166 | # [1.3.0](https://github.com/mnasyrov/ditox/compare/v1.2.0...v1.3.0) (2021-08-04)
167 |
168 | ### Features
169 |
170 | - `declareModule()` and `declareModuleBindings()` utility functions
171 | ([990ffe7](https://github.com/mnasyrov/ditox/commit/990ffe79af37c6ee56738b6ac7e9c071818d10f7))
172 |
173 | # [1.2.0](https://github.com/mnasyrov/ditox/compare/v1.1.0...v1.2.0) (2021-06-20)
174 |
175 | ### Bug Fixes
176 |
177 | - Fixed resolving a singleton registered in the parent.
178 | ([224c595](https://github.com/mnasyrov/ditox/commit/224c59585e43c68cc23e6abf521ecad09aeb1c7a))
179 |
180 | ### Features
181 |
182 | - `bindModules()` utility function.
183 | ([e62d6c3](https://github.com/mnasyrov/ditox/commit/e62d6c3332f991b8e3943ad269bd6e76cb6a266c))
184 | - Dependency modules
185 | ([d01d333](https://github.com/mnasyrov/ditox/commit/d01d33347788c7eeeaee014fa794684ffbd4b2e7))
186 | - DependencyModule component
187 | ([e5836b8](https://github.com/mnasyrov/ditox/commit/e5836b89f4506cabe0303a91501c43f732455fe1))
188 |
189 | # [1.1.0](https://github.com/mnasyrov/ditox/compare/v1.0.2...v1.1.0) (2021-04-17)
190 |
191 | ### Bug Fixes
192 |
193 | - Fixed typings and arity for `getValues()`, `resolveValues()` and
194 | `injectable()`.
195 | ([92f57dc](https://github.com/mnasyrov/ditox/commit/92f57dcc1777c4c622d61c68196db3d48f3fa186))
196 |
197 | ### Features
198 |
199 | - `Container.hasToken()` for checking token presence.
200 | ([56043ae](https://github.com/mnasyrov/ditox/commit/56043aec494481cc624b30d81e33df33a8273e63))
201 | - Utilities for resolving object properties by tokens
202 | ([d4a41e8](https://github.com/mnasyrov/ditox/commit/d4a41e8d777540905a4bc15fc22bcb06a85cf90a))
203 |
204 | ## [1.0.2](https://github.com/mnasyrov/ditox/compare/v1.0.1...v1.0.2) (2021-03-04)
205 |
206 | ### Bug Fixes
207 |
208 | - Fixed flow typings
209 | ([60bc7e7](https://github.com/mnasyrov/ditox/commit/60bc7e76c987d713edb61ee967b4dc88ad1f0f8e))
210 |
211 | ## [1.0.1](https://github.com/mnasyrov/ditox/compare/v1.0.0...v1.0.1) (2021-03-04)
212 |
213 | ### Bug Fixes
214 |
215 | - Fixed bundled typings for Typescript
216 | ([7b1499e](https://github.com/mnasyrov/ditox/commit/7b1499e7cf1506f24f72387d83a055e6a4d3c336))
217 |
218 | # [1.0.0](https://github.com/mnasyrov/ditox/compare/v0.5.4...v1.0.0) (2021-02-28)
219 |
220 | **Note:** Version bump only for package ditox-root
221 |
222 | ## [0.5.4](https://github.com/mnasyrov/ditox/compare/v0.5.3...v0.5.4) (2021-02-28)
223 |
224 | **Note:** Version bump only for package ditox-root
225 |
226 | ## [0.5.3](https://github.com/mnasyrov/ditox/compare/v0.5.2...v0.5.3) (2021-02-25)
227 |
228 | **Note:** Version bump only for package ditox-root
229 |
230 | ## [0.5.2](https://github.com/mnasyrov/ditox/compare/v0.5.1...v0.5.2) (2021-02-25)
231 |
232 | **Note:** Version bump only for package ditox-root
233 |
234 | ## [0.5.1](https://github.com/mnasyrov/ditox/compare/v0.5.0...v0.5.1) (2021-02-25)
235 |
236 | **Note:** Version bump only for package ditox-root
237 |
238 | # [0.5.0](https://github.com/mnasyrov/ditox/compare/v0.4.1...v0.5.0) (2021-02-24)
239 |
240 | ### Features
241 |
242 | - Introduced @ditox/react - tooling for React apps
243 | ([cd9c9db](https://github.com/mnasyrov/ditox/commit/cd9c9db9d65fda468f0e740c49e090757f1ac73a))
244 |
245 | # Changelog
246 |
247 | All notable changes to this project will be documented in this file. See
248 | [standard-version](https://github.com/conventional-changelog/standard-version)
249 | for commit guidelines.
250 |
251 | ### [0.4.1](https://github.com/mnasyrov/ditox/compare/v0.4.0...v0.4.1) (2020-12-09)
252 |
253 | ### Bug Fixes
254 |
255 | - Fixed "onRemoved" callback for scoped factories.
256 | ([8b9cba7](https://github.com/mnasyrov/ditox/commit/8b9cba79ec211c328dafdd7c77ba760cc324855a))
257 |
258 | ## [0.4.0](https://github.com/mnasyrov/ditox/compare/v0.3.9...v0.4.0) (2020-12-07)
259 |
260 | ### ⚠ BREAKING CHANGES
261 |
262 | - A default factory scope was changed to 'singleton'.
263 |
264 | ### Bug Fixes
265 |
266 | - Changed a default scope to 'singleton'.
267 | ([346fce6](https://github.com/mnasyrov/ditox/commit/346fce68b03fe452224b8c0646d340285b6bd082))
268 | - Fixed resolving of "scope" bindings.
269 | ([528c8b4](https://github.com/mnasyrov/ditox/commit/528c8b4eb832ffc7fb147549c03075fb7fe6b9df))
270 |
271 | ## [0.3.9](https://github.com/mnasyrov/ditox/compare/v0.3.8...v0.3.9) (2020-12-06)
272 |
273 | ### Bug Fixes
274 |
275 | - Fixed types for Optional token.
276 | ([ea86ce0](https://github.com/mnasyrov/ditox/commit/ea86ce05c30606741a4ee9dc99d5496108e2a61b))
277 |
278 | ## [0.3.8](https://github.com/mnasyrov/ditox/compare/v0.3.7...v0.3.8) (2020-11-28)
279 |
280 | ### Bug Fixes
281 |
282 | - Bundle for Deno
283 | ([9ef6d4f](https://github.com/mnasyrov/ditox/commit/9ef6d4fbc08cc8aa3bb320a9e445686e0732e4ba))
284 |
285 | ## [0.3.7](https://github.com/mnasyrov/ditox/compare/v0.3.6...v0.3.7) (2020-11-28)
286 |
287 | ### Features
288 |
289 | - Added support for Deno
290 | ([adfe90f](https://github.com/mnasyrov/ditox/commit/adfe90ffc99ccfc7d3c03045d1f91b4d5071dc1d))
291 |
292 | ## [0.3.6](https://github.com/mnasyrov/ditox/compare/v0.3.5...v0.3.6) (2020-11-15)
293 |
294 | ## [0.3.5](https://github.com/mnasyrov/ditox/compare/v0.3.4...v0.3.5) (2020-11-13)
295 |
296 | ## [0.3.4](https://github.com/mnasyrov/ditox/compare/v0.3.3...v0.3.4) (2020-11-13)
297 |
298 | ## [0.3.3](https://github.com/mnasyrov/ditox/compare/v0.3.2...v0.3.3) (2020-11-13)
299 |
300 | ## [0.3.2](https://github.com/mnasyrov/ditox/compare/v0.3.1...v0.3.2) (2020-11-12)
301 |
302 | ## [0.3.1](https://github.com/mnasyrov/ditox/compare/v0.3.0...v0.3.1) (2020-11-10)
303 |
304 | # [0.3.0](https://github.com/mnasyrov/ditox/compare/v0.2.1...v0.3.0) (2020-11-08)
305 |
306 | ### Bug Fixes
307 |
308 | - Renamed umd's name to "Ditox"
309 | ([f26a04d](https://github.com/mnasyrov/ditox/commit/f26a04d0fc92e6b242649f5dd0688b57a8ffde11))
310 |
311 | ### Features
312 |
313 | - Introduced "scoped" factory scope. Refactored API.
314 | ([701af8e](https://github.com/mnasyrov/ditox/commit/701af8e19d113dc40a0e6c2e086f14b14e00a536))
315 |
316 | ## [0.2.1](https://github.com/mnasyrov/ditox/compare/v0.2.0...v0.2.1) (2020-11-03)
317 |
318 | ### Bug Fixes
319 |
320 | - Flow typings. Added flow tests.
321 | ([ea39bf0](https://github.com/mnasyrov/ditox/commit/ea39bf0c8f6d2b6ec5928f50787aa11e73629d7a))
322 |
323 | # [0.2.0](https://github.com/mnasyrov/ditox/compare/7d9c7549355878d792141a2eef9fb857f0402e46...v0.2.0) (2020-11-02)
324 |
325 | ### Bug Fixes
326 |
327 | - "onUnbind" is available for "singlenton" scope only. Added tests.
328 | ([0953379](https://github.com/mnasyrov/ditox/commit/0953379aaefd25763ecfdb903761d1e1b5fd8e01))
329 |
330 | ### Features
331 |
332 | - Added "bindMultiValue" utility function.
333 | ([5d00e40](https://github.com/mnasyrov/ditox/commit/5d00e40e8a6ba9d891443c50fccbf74698ddfb11))
334 | - Dependency injection
335 | ([7d9c754](https://github.com/mnasyrov/ditox/commit/7d9c7549355878d792141a2eef9fb857f0402e46))
336 | - Flow typings
337 | ([992123e](https://github.com/mnasyrov/ditox/commit/992123ead2f0e7860a6193e3d04252523c5d3c10))
338 | - Optional token
339 | ([e31fa45](https://github.com/mnasyrov/ditox/commit/e31fa452a971df82c3ea8a645156235955300e8a))
340 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Mikhail Nasyrov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ditox.js
2 |
3 |
4 |
5 | **Dependency injection for modular web applications**
6 |
7 | [](https://www.npmjs.com/package/ditox)
8 | [](https://github.com/mnasyrov/ditox/stargazers)
9 | [](https://www.npmjs.com/package/ditox)
10 | [](https://github.com/mnasyrov/ditox/blob/master/LICENSE)
11 | [](https://coveralls.io/github/mnasyrov/ditox)
12 |
13 | ## Overview
14 |
15 | Ditox.js is a lightweight dependency injection container for TypeScript. It
16 | provides a simple functional API to bind values and factories to container by
17 | tokens, and resolve values later. The library supports different scopes for
18 | factory bindings, including "singleton", "scoped", and "transient". Bindings can
19 | be organised as a dependency module in declarative way.
20 |
21 | Ditox.js works with containers, tokens, values and value factories. There are no
22 | class decorators, field injectors and other magic. Explicit binding and
23 | resolving are used.
24 |
25 | ## Features
26 |
27 | - Functional API
28 | - Container hierarchy
29 | - Scopes for factory bindings
30 | - Dependency modules
31 | - Multi-value tokens
32 | - Typescript typings
33 |
34 | ## API References
35 |
36 | The library is available as two packages:
37 |
38 | - **ditox** - DI container and core tools
39 | - **ditox-react** - Tools for React.js applications
40 |
41 | Please see the documentation at [ditox.js.org](https://ditox.js.org)
42 |
43 | ## Getting Started
44 |
45 | ### Installation
46 |
47 | You can use the following command to install packages:
48 |
49 | ```shell
50 | npm install --save ditox
51 | npm install --save ditox-react
52 | ```
53 |
54 | Packages can be used as [UMD](https://github.com/umdjs/umd) modules. Use
55 | [jsdelivr.com](https://jsdelivr.com) CDN site to load
56 | [ditox](https://www.jsdelivr.com/package/npm/ditox) and
57 | [ditox-react](https://www.jsdelivr.com/package/npm/ditox-react):
58 |
59 | ```html
60 |
61 |
62 |
66 | ```
67 |
68 | ### Basic concepts
69 |
70 | - **Token** specifies a future injection of an "internal" implementation with a
71 | concrete "public" type.
72 |
73 | ```ts
74 | type Logger = (message: string) => void;
75 |
76 | const LOGGER_TOKEN = token();
77 | ```
78 |
79 | - **Container** keeps bindings of **tokens** to concrete values and
80 | implementations
81 |
82 | ```ts
83 | const container = createContainer();
84 | container.bindValue(LOGGER_TOKEN, (message) => console.log(message));
85 | ```
86 |
87 | - **Code graph** is constructed at **runtime** by resolving values of tokens.
88 |
89 | ```ts
90 | const logger = container.resolve(LOGGER_TOKEN);
91 | logger('Hello World!');
92 | ```
93 |
94 | ## Usage Examples
95 |
96 | ### Binding a value
97 |
98 | Create an injection token for a logger and DI container. Bind a logger
99 | implementation and resolve its value later in the application:
100 |
101 | ```typescript
102 | import {createContainer, token} from 'ditox';
103 |
104 | type LoggerService = {
105 | log: (...messages: string[]) => void;
106 | };
107 |
108 | // Injection token
109 | const LOGGER_TOKEN = token();
110 |
111 | // Default implementation
112 | const CONSOLE_LOGGER: LoggerService = {
113 | log: (...messages) => console.log(...messages),
114 | };
115 |
116 | // Create a DI container
117 | const container = createContainer();
118 |
119 | container.bindValue(LOGGER_TOKEN, CONSOLE_LOGGER);
120 |
121 | // Later, somewhere in the app
122 | const logger = container.resolve(LOGGER_TOKEN);
123 | logger.log('Hello World!');
124 | ```
125 |
126 | ### Binding a factory
127 |
128 | Bind a factory of a remote logger which depends on an HTTP client:
129 |
130 | ```typescript
131 | import {injectable} from 'ditox';
132 |
133 | export type ServerClient = {
134 | log: (...messages: string[]) => void;
135 | sendMetric: (key: string, value: string) => void;
136 | };
137 |
138 | export const SERVER_CLIENT_TOKEN = token();
139 |
140 | function createLoggerClient(client: ServerClient): Logger {
141 | return {
142 | log: (...messages) => client.log(...messages),
143 | };
144 | }
145 |
146 | container.bindFactory(
147 | LOGGER_TOKEN,
148 | injectable(createLoggerClient, SERVER_CLIENT_TOKEN),
149 | );
150 |
151 | // Later, somewhere in the app
152 | const logger = container.resolve(LOGGER_TOKEN);
153 | logger.log('Hello World!');
154 | ```
155 |
156 | ### DI module
157 |
158 | Organise related bindings and functional as a DI module:
159 |
160 | ```typescript
161 | import {bindModule, declareModule} from 'ditox';
162 |
163 | type SendMetricFn = (key: string, value: string) => void;
164 |
165 | const SEND_METRIC_TOKEN = token();
166 |
167 | function createMetricClient(client: ServerClient): Logger {
168 | return {
169 | sendMetric: (key: string, value: string) => client.sendMetric(key, value),
170 | };
171 | }
172 |
173 | // Declare a DI module
174 | const TELEMETRY_MODULE = declareModule({
175 | factory: injectable((client) => {
176 | const logger = createLoggerClient(client);
177 |
178 | const sendMetric = (key: string, value: string) => {
179 | logger('metric', key, value);
180 | client.sendMetric(key, value);
181 | };
182 |
183 | return {logger, sendMetric};
184 | }, SERVER_CLIENT_TOKEN),
185 | exports: {
186 | logger: LOGGER_TOKEN,
187 | sendMetric: SEND_METRIC_TOKEN,
188 | },
189 | });
190 |
191 | // Bind the module
192 | bindModule(container, TELEMETRY_MODULE);
193 |
194 | // Later, somewhere in the app
195 | const logger = container.resolve(LOGGER_TOKEN);
196 | logger.log('Hello World!');
197 |
198 | const sendMetric = container.resolve(SEND_METRIC_TOKEN);
199 | sendMetric('foo', 'bar');
200 | ```
201 |
202 | ### Using in React app
203 |
204 | Wrap a component tree by a DI container and bind modules:
205 |
206 | ```tsx
207 | // index.tsx
208 |
209 | import ReactDOM from 'react-dom';
210 |
211 | import {Greeting} from './Greeting';
212 | import {TELEMETRY_MODULE} from './telemetry';
213 |
214 | const APP_MODULE = declareModule({
215 | imports: [TELEMETRY_MODULE],
216 | });
217 |
218 | const App: FC = () => {
219 | return (
220 |
221 |
222 |
223 |
224 |
225 | );
226 | };
227 |
228 | ReactDOM.render(, document.getElementById('root'));
229 | ```
230 |
231 | Injecting a dependency by a React component:
232 |
233 | ```tsx
234 | // Greeting.tsx
235 |
236 | import {useDependency} from 'ditox-react';
237 |
238 | export const Greeting: FC = () => {
239 | const logger = useDependency(LOGGER_TOKEN);
240 |
241 | useEffect(() => {
242 | logger.log('Hello World!');
243 | }, [logger]);
244 |
245 | return <>Hello>;
246 | };
247 | ```
248 |
249 | ## Contact & Support
250 |
251 | - Follow 👨🏻💻 **@mnasyrov** on [GitHub](https://github.com/mnasyrov) for
252 | announcements
253 | - Create a 💬 [GitHub issue](https://github.com/mnasyrov/ditox/issues) for bug
254 | reports, feature requests, or questions
255 | - Add a ⭐️ star on [GitHub](https://github.com/mnasyrov/ditox/issues) and 🐦
256 | [tweet](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fmnasyrov%2Fditox&hashtags=developers,frontend,javascript)
257 | to promote the project
258 |
259 | ## License
260 |
261 | This project is licensed under the
262 | [MIT license](https://github.com/mnasyrov/ditox/blob/master/LICENSE).
263 |
264 |
290 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // * https://jestjs.io/docs/en/configuration.html
3 | // * https://kulshekhar.github.io/ts-jest/user/config/
4 |
5 | import fastGlob from 'fast-glob';
6 |
7 | export default {
8 | roots: fastGlob.sync(['packages/*/src'], {onlyDirectories: true}),
9 | preset: 'ts-jest',
10 | testEnvironment: 'jsdom',
11 | collectCoverageFrom: [
12 | 'packages/*/src/**/*.{ts,tsx}',
13 | '!packages/*/src/**/index.{ts,tsx}',
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/lemon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
144 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "3.0.1",
3 | "useWorkspaces": true,
4 | "hoist": "**",
5 | "forceLocal": true,
6 | "command": {
7 | "bootstrap": {
8 | "npmClientArgs": ["--save-exact"]
9 | },
10 | "run": {
11 | "stream": true
12 | },
13 | "version": {
14 | "allowBranch": "master",
15 | "conventionalCommits": true,
16 | "exact": true
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/media/diagram.puml:
--------------------------------------------------------------------------------
1 | @startuml
2 | 'https://plantuml.com/component-diagram
3 |
4 | skinparam note {
5 | BorderColor black
6 | }
7 |
8 | skinparam component {
9 | BorderColor black
10 | BackgroundColor #FFF48C
11 | ArrowColor black
12 | }
13 |
14 | skinparam actor {
15 | BorderColor black
16 | BackgroundColor #FFF48C
17 | ArrowColor black
18 | }
19 |
20 | actor Client
21 |
22 | 'component Container #d2e660
23 | component Container #9CEB61
24 | component Token #9CEB61
25 | component "Optional Token" as OptionalToken #9CEB61
26 |
27 | note as DitoxNote #9CEB61
28 | Ditox.js
29 | end note
30 |
31 | component Value
32 | component "Produced Value" as ProducedValue
33 | component Factory
34 | component "Default value" as DefaultValue
35 |
36 | Client -> Container : creates a container
37 | Client -> Container : binds a value
38 | Client -> Container : binds a factory
39 | Client --> Container : resolves a value
40 |
41 | Container -> Factory : stores and invokes
42 | Container --> Token : uses as a key
43 | Container --> Value : stores and resolves
44 | Container --> ProducedValue : stores and resolves
45 |
46 | Factory --> ProducedValue : produces
47 | Factory ..> Token : is resolved by
48 | Value ..> Token : is resolved by
49 | ProducedValue ..> Token : is resolved by
50 |
51 | Token <.. OptionalToken : extends
52 | OptionalToken --> DefaultValue : has
53 |
54 | @enduml
55 |
--------------------------------------------------------------------------------
/media/diagram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/lemon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
144 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "ditox-root",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "author": "Mikhail Nasyrov (https://github.com/mnasyrov)",
7 | "description": "Dependency injection for modular web applications",
8 | "keywords": [
9 | "dependency container",
10 | "dependency injection",
11 | "typescript",
12 | "javascript",
13 | "npm",
14 | "dependency",
15 | "injection",
16 | "container",
17 | "module",
18 | "ioc",
19 | "di"
20 | ],
21 | "homepage": "https://github.com/mnasyrov/ditox",
22 | "bugs": "https://github.com/mnasyrov/ditox/issues",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/mnasyrov/ditox.git"
26 | },
27 | "workspaces": [
28 | "packages/*"
29 | ],
30 | "type": "module",
31 | "scripts": {
32 | "prepare": "husky install",
33 | "clean": "npm run -ws clean && rm -rf build coverage docs/api dist",
34 | "lint": "npm run lint:eslint && npm run lint:tsc",
35 | "lint:eslint": "eslint \"packages/*/{src,test*}/**\"",
36 | "lint:tsc": "tsc --noEmit --jsx react",
37 | "test": "jest",
38 | "build": "npm run -ws build",
39 | "build-docs": "npm run build && typedoc",
40 | "pack": "npm run build && mkdir -p dist && npm exec -ws -c 'npm pack --pack-destination ../../dist'",
41 | "preversion": "npm run build && npm run lint && npm run test && git add --all",
42 | "release-version": "lerna version --no-push",
43 | "release-publish": "lerna publish from-git"
44 | },
45 | "devDependencies": {
46 | "@rollup/plugin-terser": "0.2.1",
47 | "@rollup/plugin-typescript": "10.0.1",
48 | "@types/jest": "29.2.4",
49 | "@typescript-eslint/eslint-plugin": "5.47.0",
50 | "@typescript-eslint/parser": "5.47.0",
51 | "eslint": "8.30.0",
52 | "eslint-plugin-jest": "27.1.7",
53 | "eslint-plugin-react-hooks": "4.6.0",
54 | "fast-glob": "3.2.12",
55 | "husky": "8.0.2",
56 | "jest": "29.3.1",
57 | "jest-environment-jsdom": "29.3.1",
58 | "lerna": "6.1.0",
59 | "lint-staged": "13.1.0",
60 | "prettier": "2.8.1",
61 | "rollup": "3.7.5",
62 | "rollup-plugin-dts": "5.0.0",
63 | "shx": "0.3.4",
64 | "ts-jest": "29.0.3",
65 | "typedoc": "0.23.28",
66 | "typedoc-plugin-extras": "2.3.2",
67 | "typedoc-plugin-replace-text": "2.1.0",
68 | "typedoc-plugin-resolve-crossmodule-references": "0.3.3",
69 | "typescript": "4.9.4"
70 | },
71 | "lint-staged": {
72 | "{packages,scripts}/**/*.{ts,tsx}": [
73 | "eslint --max-warnings 0 --fix",
74 | "prettier --write"
75 | ],
76 | "*.{css,json,md,html,yml}": [
77 | "prettier --write"
78 | ]
79 | },
80 | "attributions": [
81 | {
82 | "lemon.svg": [
83 | "Vincent Le Moign, CC BY 4.0 , via Wikimedia Commons",
84 | "https://commons.wikimedia.org/wiki/File:526-lemon.svg"
85 | ]
86 | }
87 | ]
88 | }
89 |
--------------------------------------------------------------------------------
/packages/ditox-react/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See
4 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | ## [3.0.1](https://github.com/mnasyrov/ditox/compare/v3.0.0...v3.0.1) (2025-03-02)
7 |
8 | **Note:** Version bump only for package ditox-react
9 |
10 | # Change Log
11 |
12 | All notable changes to this project will be documented in this file. See
13 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
14 |
15 | # [3.0.0](https://github.com/mnasyrov/ditox/compare/v2.4.1...v3.0.0) (2024-07-12)
16 |
17 | **Note:** Version bump only for package ditox-react
18 |
19 | # Change Log
20 |
21 | All notable changes to this project will be documented in this file. See
22 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
23 |
24 | # [3.0.0-dev.2](https://github.com/mnasyrov/ditox/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2024-07-11)
25 |
26 | **Note:** Version bump only for package ditox-react
27 |
28 | # Change Log
29 |
30 | All notable changes to this project will be documented in this file. See
31 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
32 |
33 | # [3.0.0-dev.1](https://github.com/mnasyrov/ditox/compare/v2.4.1...v3.0.0-dev.1) (2024-05-31)
34 |
35 | **Note:** Version bump only for package ditox-react
36 |
37 | # Change Log
38 |
39 | All notable changes to this project will be documented in this file. See
40 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
41 |
42 | ## [2.4.1](https://github.com/mnasyrov/ditox/compare/v2.4.0...v2.4.1) (2023-10-27)
43 |
44 | ### Bug Fixes
45 |
46 | - Add export CustomDependencyContainer to index
47 | ([#34](https://github.com/mnasyrov/ditox/issues/34))
48 | ([1723671](https://github.com/mnasyrov/ditox/commit/17236718c54c381ffca0a0c9160615aa26c79eaa))
49 |
50 | # Change Log
51 |
52 | All notable changes to this project will be documented in this file. See
53 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
54 |
55 | # [2.4.0](https://github.com/mnasyrov/ditox/compare/v2.3.1...v2.4.0) (2023-09-08)
56 |
57 | **Note:** Version bump only for package ditox-react
58 |
59 | # Change Log
60 |
61 | All notable changes to this project will be documented in this file. See
62 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
63 |
64 | ## [2.3.1](https://github.com/mnasyrov/ditox/compare/v2.3.0...v2.3.1) (2023-04-01)
65 |
66 | **Note:** Version bump only for package ditox-react
67 |
68 | # [2.3.0](https://github.com/mnasyrov/ditox/compare/v2.2.0...v2.3.0) (2022-12-22)
69 |
70 | ### Bug Fixes
71 |
72 | - Compatible with Typescript 4.9. Updated dependencies.
73 | ([d8aa9e2](https://github.com/mnasyrov/ditox/commit/d8aa9e2a4a665d6e14f3581c652feb6ad01b71d8))
74 |
75 | # [2.2.0](https://github.com/mnasyrov/ditox/compare/v2.1.0...v2.2.0) (2022-01-29)
76 |
77 | **Note:** Version bump only for package ditox-react
78 |
79 | # [2.1.0](https://github.com/mnasyrov/ditox/compare/v2.0.0...v2.1.0) (2022-01-29)
80 |
81 | **Note:** Version bump only for package ditox-react
82 |
83 | # [2.0.0](https://github.com/mnasyrov/ditox/compare/v1.4.2...v2.0.0) (2022-01-26)
84 |
85 | ### chore
86 |
87 | - Dropped supporting of Flow.js typings.
88 | ([9a94f55](https://github.com/mnasyrov/ditox/commit/9a94f558daf107ff4744462e078571e4ccc7c444))
89 |
90 | ### BREAKING CHANGES
91 |
92 | - Dropped supporting of Flow.js typings. Use the previous versions of the
93 | library with Flow.js
94 |
95 | ## [1.4.2](https://github.com/mnasyrov/ditox/compare/v1.4.1...v1.4.2) (2021-11-18)
96 |
97 | **Note:** Version bump only for package ditox-react
98 |
99 | ## [1.4.1](https://github.com/mnasyrov/ditox/compare/v1.4.0...v1.4.1) (2021-11-11)
100 |
101 | **Note:** Version bump only for package ditox-react
102 |
103 | # [1.4.0](https://github.com/mnasyrov/ditox/compare/v1.3.0...v1.4.0) (2021-10-30)
104 |
105 | ### Features
106 |
107 | - `ModuleDeclaration.imports` property takes module entries for binding with the
108 | module.
109 | ([6757bdc](https://github.com/mnasyrov/ditox/commit/6757bdc87a02d2336ca07661c50812a5823ca844))
110 |
111 | # [1.3.0](https://github.com/mnasyrov/ditox/compare/v1.2.0...v1.3.0) (2021-08-04)
112 |
113 | ### Features
114 |
115 | - `declareModule()` and `declareModuleBindings()` utility functions
116 | ([990ffe7](https://github.com/mnasyrov/ditox/commit/990ffe79af37c6ee56738b6ac7e9c071818d10f7))
117 |
118 | # [1.2.0](https://github.com/mnasyrov/ditox/compare/v1.1.0...v1.2.0) (2021-06-20)
119 |
120 | ### Features
121 |
122 | - DependencyModule component
123 | ([e5836b8](https://github.com/mnasyrov/ditox/commit/e5836b89f4506cabe0303a91501c43f732455fe1))
124 |
125 | # [1.1.0](https://github.com/mnasyrov/ditox/compare/v1.0.2...v1.1.0) (2021-04-17)
126 |
127 | ### Features
128 |
129 | - Utilities for resolving object properties by tokens
130 | ([d4a41e8](https://github.com/mnasyrov/ditox/commit/d4a41e8d777540905a4bc15fc22bcb06a85cf90a))
131 |
132 | ## [1.0.2](https://github.com/mnasyrov/ditox/compare/v1.0.1...v1.0.2) (2021-03-04)
133 |
134 | ### Bug Fixes
135 |
136 | - Fixed flow typings
137 | ([60bc7e7](https://github.com/mnasyrov/ditox/commit/60bc7e76c987d713edb61ee967b4dc88ad1f0f8e))
138 |
139 | ## [1.0.1](https://github.com/mnasyrov/ditox/compare/v1.0.0...v1.0.1) (2021-03-04)
140 |
141 | ### Bug Fixes
142 |
143 | - Fixed bundled typings for Typescript
144 | ([7b1499e](https://github.com/mnasyrov/ditox/commit/7b1499e7cf1506f24f72387d83a055e6a4d3c336))
145 |
146 | # [1.0.0](https://github.com/mnasyrov/ditox/compare/v0.5.4...v1.0.0) (2021-02-28)
147 |
148 | **Note:** Version bump only for package @ditox/react
149 |
150 | ## [0.5.4](https://github.com/mnasyrov/ditox/compare/v0.5.3...v0.5.4) (2021-02-28)
151 |
152 | **Note:** Version bump only for package @ditox/react
153 |
154 | ## [0.5.3](https://github.com/mnasyrov/ditox/compare/v0.5.2...v0.5.3) (2021-02-25)
155 |
156 | **Note:** Version bump only for package @ditox/react
157 |
158 | ## [0.5.2](https://github.com/mnasyrov/ditox/compare/v0.5.1...v0.5.2) (2021-02-25)
159 |
160 | **Note:** Version bump only for package @ditox/react
161 |
162 | ## [0.5.1](https://github.com/mnasyrov/ditox/compare/v0.5.0...v0.5.1) (2021-02-25)
163 |
164 | **Note:** Version bump only for package @ditox/react
165 |
166 | # [0.5.0](https://github.com/mnasyrov/ditox/compare/v0.4.1...v0.5.0) (2021-02-24)
167 |
168 | ### Features
169 |
170 | - Introduced @ditox/react - tooling for React apps
171 | ([cd9c9db](https://github.com/mnasyrov/ditox/commit/cd9c9db9d65fda468f0e740c49e090757f1ac73a))
172 |
--------------------------------------------------------------------------------
/packages/ditox-react/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Mikhail Nasyrov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/ditox-react/README.md:
--------------------------------------------------------------------------------
1 | # ditox-react package
2 |
3 |
4 |
5 | **Dependency injection container for React.js**
6 |
7 | Please see the documentation at [ditox.js.org](https://ditox.js.org)
8 |
9 | [](https://www.npmjs.com/package/ditox)
10 | [](https://github.com/mnasyrov/ditox/stargazers)
11 | [](https://www.npmjs.com/package/ditox)
12 | [](https://github.com/mnasyrov/ditox/blob/master/LICENSE)
13 | [](https://coveralls.io/github/mnasyrov/ditox)
14 |
15 | ## Installation
16 |
17 | You can use the following command to install this package:
18 |
19 | ```shell
20 | npm install --save ditox-react
21 | ```
22 |
23 | The packages can be used as [UMD](https://github.com/umdjs/umd) modules. Use
24 | [jsdelivr.com](https://jsdelivr.com) CDN site to load
25 | [ditox](https://www.jsdelivr.com/package/npm/ditox) and
26 | [ditox-react](https://www.jsdelivr.com/package/npm/ditox-react):
27 |
28 | ```html
29 |
30 |
31 |
35 | ```
36 |
37 | ## Overview
38 |
39 | `ditox-react` is a set of helpers for providing and using a dependency container
40 | in React apps:
41 |
42 | - Components:
43 | - `DepencencyContainer` - provides a new or existed container to React
44 | components.
45 | - `DepencencyModule` - binds a dependency module to a new container.
46 | - `CustomDepencencyContainer` - provides an existed dependency container.
47 | - Hooks:
48 | - `useDependencyContainer()` - returns a provided dependency container.
49 | - `useDependency()` - returns a resolved value by a specified token. It throws
50 | an error in case a container or value is not found.
51 | - `useOptionalDependency()` - returns a resolved value by a specified token,
52 | or returns `undefined` in case a container or value is not found.
53 |
54 | ## Usage Examples
55 |
56 | ```jsx
57 | import {token} from 'ditox';
58 | import {
59 | DependencyContainer,
60 | useDependency,
61 | useDependencyContainer,
62 | useOptionalDependency,
63 | } from 'ditox-react';
64 |
65 | const FOO_TOKEN = token();
66 | const BAR_TOKEN = token();
67 |
68 | function appDependencyBinder(container) {
69 | container.bindValue(FOO_TOKEN, 'foo');
70 | }
71 |
72 | function App() {
73 | return (
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | function NestedComponent() {
81 | // Get access to the container
82 | const container = useDependencyContainer();
83 |
84 | // Use a resolved value
85 | const foo = useDependency(FOO_TOKEN);
86 |
87 | // Use an optional value. It is not provided in this example.
88 | const bar = useOptionalDependency(BAR_TOKEN);
89 |
90 | useEffect(() => {
91 | console.log({foo, bar}); // {foo: 'foo', bar: undefined}
92 | }, [foo, bar]);
93 |
94 | return null;
95 | }
96 | ```
97 |
98 | ## Dependency Modules
99 |
100 | Dependency modules can be provided to the app with ``
101 | component:
102 |
103 | ```tsx
104 | function App() {
105 | return (
106 |
107 |
108 |
109 | );
110 | }
111 | ```
112 |
113 | ---
114 |
115 | This project is licensed under the
116 | [MIT license](https://github.com/mnasyrov/ditox/blob/master/LICENSE).
117 |
--------------------------------------------------------------------------------
/packages/ditox-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ditox-react",
3 | "version": "3.0.1",
4 | "description": "Dependency injection container for React.js",
5 | "publishConfig": {
6 | "access": "public"
7 | },
8 | "license": "MIT",
9 | "author": "Mikhail Nasyrov (https://github.com/mnasyrov)",
10 | "homepage": "https://github.com/mnasyrov/ditox",
11 | "bugs": "https://github.com/mnasyrov/ditox/issues",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/mnasyrov/ditox.git"
15 | },
16 | "keywords": [
17 | "react",
18 | "react.js",
19 | "dependency container",
20 | "dependency injection",
21 | "typescript",
22 | "javascript",
23 | "npm",
24 | "dependency",
25 | "injection",
26 | "container",
27 | "module",
28 | "ioc",
29 | "di"
30 | ],
31 | "engines": {
32 | "node": ">=12"
33 | },
34 | "main": "dist/cjs/index.js",
35 | "module": "dist/esm/index.js",
36 | "jsnext:main": "dist/esm/index.js",
37 | "unpkg": "dist/umd/index.js",
38 | "umd:main": "dist/umd/index.js",
39 | "browser": "dist/browser/index.js",
40 | "react-native": "dist/browser/index.js",
41 | "types": "dist/esm/index.d.ts",
42 | "source": "src/index.ts",
43 | "sideEffects": false,
44 | "files": [
45 | "dist",
46 | "docs",
47 | "src",
48 | "LICENSE",
49 | "README.md"
50 | ],
51 | "scripts": {
52 | "clean": "shx rm -rf dist lib",
53 | "build": "npm run build:cjs && npm run build:esm && npm run build:rollup",
54 | "build:cjs": "tsc -p tsconfig.build.json --outDir dist/cjs --module commonjs",
55 | "build:esm": "tsc -p tsconfig.build.json --outDir dist/esm --module es2015",
56 | "build:rollup": "rollup -c",
57 | "typedoc": "typedoc"
58 | },
59 | "dependencies": {
60 | "ditox": "3.0.0"
61 | },
62 | "peerDependencies": {
63 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
64 | },
65 | "devDependencies": {
66 | "@testing-library/react": "16.2.0",
67 | "@types/react": "19.0.10",
68 | "react": "19.0.0",
69 | "react-dom": "19.0.0",
70 | "react-test-renderer": "19.0.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/packages/ditox-react/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import typescript from '@rollup/plugin-typescript';
3 | import dts from 'rollup-plugin-dts';
4 | import pkg from './package.json' assert {type: 'json'};
5 |
6 | const EXTERNALS = ['ditox', 'react'];
7 |
8 | const UMD_LIB_NAME = 'DitoxReact';
9 | const UMD_GLOBALS = {
10 | ditox: 'Ditox',
11 | react: 'React',
12 | };
13 |
14 | export default [
15 | {
16 | input: 'src/index.ts',
17 | output: [{file: 'dist/browser/index.d.ts'}, {file: 'dist/umd/index.d.ts'}],
18 | plugins: [dts()],
19 | },
20 | {
21 | external: EXTERNALS,
22 | input: 'src/index.ts',
23 | output: [
24 | {
25 | name: UMD_LIB_NAME,
26 | file: pkg.browser,
27 | format: 'es',
28 | globals: UMD_GLOBALS,
29 | sourcemap: true,
30 | },
31 | {
32 | name: UMD_LIB_NAME,
33 | file: pkg['umd:main'],
34 | format: 'umd',
35 | globals: UMD_GLOBALS,
36 | sourcemap: true,
37 | },
38 | ],
39 | plugins: [
40 | typescript({
41 | target: 'es5',
42 | declaration: false,
43 | }),
44 | terser({
45 | output: {comments: false},
46 | }),
47 | ],
48 | },
49 | ];
50 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/DependencyContainer.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | import {render, renderHook} from '@testing-library/react';
6 | import {Container, createContainer, Token, token} from 'ditox';
7 | import React, {useEffect} from 'react';
8 | import {
9 | CustomDependencyContainer,
10 | DependencyContainer,
11 | } from './DependencyContainer';
12 | import {useDependencyContainer} from './hooks';
13 |
14 | describe('DependencyContainer', () => {
15 | it('should provide a new container', () => {
16 | const {result} = renderHook(useDependencyContainer, {
17 | wrapper: ({children}) => (
18 | {children}
19 | ),
20 | });
21 |
22 | expect(result.current).toBeDefined();
23 | });
24 |
25 | it('should connect a new container with a parent one in case "root" property is not set', () => {
26 | const FOO = token('FOO');
27 | const BAR = token('BAR');
28 |
29 | const parentContainer = createContainer();
30 | parentContainer.bindValue(FOO, 'foo');
31 |
32 | const binder = (container: Container) => {
33 | container.bindValue(BAR, 'bar');
34 | };
35 |
36 | const {result} = renderHook(() => useDependencyContainer('strict'), {
37 | wrapper: ({children}) => (
38 |
39 | {children}
40 |
41 | ),
42 | });
43 |
44 | expect(result.current.resolve(BAR)).toEqual('bar');
45 | expect(result.current.resolve(FOO)).toEqual('foo');
46 | });
47 |
48 | it('should not connect a new container with a parent one in case "root" is "true"', () => {
49 | const FOO = token('FOO');
50 | const BAR = token('BAR');
51 |
52 | const parentContainer = createContainer();
53 | parentContainer.bindValue(FOO, 'foo');
54 |
55 | const binder = (container: Container) => {
56 | container.bindValue(BAR, 'bar');
57 | };
58 |
59 | const {result} = renderHook(() => useDependencyContainer('strict'), {
60 | wrapper: ({children}) => (
61 |
62 |
63 | {children}
64 |
65 |
66 | ),
67 | });
68 |
69 | expect(result.current.resolve(BAR)).toEqual('bar');
70 | expect(result.current.get(FOO)).toBeUndefined();
71 | });
72 |
73 | it('should disconnect a container from a parent one in case "root" is changed from "false" to "true"', () => {
74 | const FOO = token('FOO');
75 | const BAR = token('BAR');
76 |
77 | const parentContainer = createContainer();
78 | parentContainer.bindValue(FOO, 'foo');
79 |
80 | const binder = (container: Container) => container.bindValue(BAR, 'bar');
81 |
82 | const [FooMonitor, fooCallback] = createMonitor(FOO);
83 | const [BarMonitor, barCallback] = createMonitor(BAR);
84 |
85 | const {rerender} = render(
86 |
87 |
88 |
89 |
90 |
91 | ,
92 | );
93 |
94 | expect(fooCallback).toBeCalledTimes(1);
95 | expect(fooCallback).lastCalledWith('foo');
96 | expect(barCallback).toBeCalledTimes(1);
97 | expect(barCallback).lastCalledWith('bar');
98 |
99 | fooCallback.mockClear();
100 | barCallback.mockClear();
101 |
102 | rerender(
103 |
104 |
105 |
106 |
107 |
108 | ,
109 | );
110 |
111 | expect(fooCallback).toBeCalledTimes(1);
112 | expect(fooCallback).lastCalledWith(undefined);
113 | expect(barCallback).toBeCalledTimes(1);
114 | expect(barCallback).lastCalledWith('bar');
115 | });
116 |
117 | it('should connect a container to a parent one in case "root" is changed from "true" to "false"', () => {
118 | const FOO = token('FOO');
119 | const BAR = token('BAR');
120 |
121 | const parentContainer = createContainer();
122 | parentContainer.bindValue(FOO, 'foo');
123 |
124 | const binder = (container: Container) => container.bindValue(BAR, 'bar');
125 |
126 | const [FooMonitor, fooCallback] = createMonitor(FOO);
127 | const [BarMonitor, barCallback] = createMonitor(BAR);
128 |
129 | const {rerender} = render(
130 |
131 |
132 |
133 |
134 |
135 | ,
136 | );
137 |
138 | expect(fooCallback).toBeCalledTimes(1);
139 | expect(fooCallback).lastCalledWith(undefined);
140 | expect(barCallback).toBeCalledTimes(1);
141 | expect(barCallback).lastCalledWith('bar');
142 |
143 | fooCallback.mockClear();
144 | barCallback.mockClear();
145 |
146 | rerender(
147 |
148 |
149 |
150 |
151 |
152 | ,
153 | );
154 |
155 | expect(fooCallback).toBeCalledTimes(1);
156 | expect(fooCallback).lastCalledWith('foo');
157 | expect(barCallback).toBeCalledTimes(1);
158 | expect(barCallback).lastCalledWith('bar');
159 | });
160 |
161 | it('should create a new container and initialize it with "binder"', () => {
162 | const FOO = token('FOO');
163 |
164 | const binder = (container: Container) => container.bindValue(FOO, 'foo');
165 |
166 | const {result} = renderHook(() => useDependencyContainer('strict'), {
167 | wrapper: ({children}) => (
168 | {children}
169 | ),
170 | });
171 |
172 | expect(result.current.resolve(FOO)).toEqual('foo');
173 | });
174 |
175 | it('should remove all bindings from a previous container in case "binder" is changed', () => {
176 | const FOO = token('FOO');
177 |
178 | const removeHandler1 = jest.fn();
179 | const binder1 = (container: Container) =>
180 | container.bindFactory(FOO, () => 'foo1', {onRemoved: removeHandler1});
181 |
182 | const removeHandler2 = jest.fn();
183 | const binder2 = (container: Container) =>
184 | container.bindFactory(FOO, () => 'foo2', {onRemoved: removeHandler2});
185 |
186 | const [Monitor, monitorCallback] = createMonitor(FOO);
187 |
188 | const {rerender} = render(
189 |
190 |
191 | ,
192 | );
193 | expect(monitorCallback).toBeCalledTimes(1);
194 | expect(monitorCallback).lastCalledWith('foo1');
195 | expect(removeHandler1).toBeCalledTimes(0);
196 | expect(removeHandler2).toBeCalledTimes(0);
197 | monitorCallback.mockClear();
198 | removeHandler1.mockClear();
199 | removeHandler2.mockClear();
200 |
201 | rerender(
202 |
203 |
204 | ,
205 | );
206 | expect(monitorCallback).toBeCalledTimes(1);
207 | expect(monitorCallback).lastCalledWith('foo2');
208 | expect(removeHandler1).toBeCalledTimes(1);
209 | expect(removeHandler2).toBeCalledTimes(0);
210 | monitorCallback.mockClear();
211 | removeHandler1.mockClear();
212 | removeHandler2.mockClear();
213 |
214 | rerender(<>>);
215 | expect(monitorCallback).toBeCalledTimes(0);
216 | expect(removeHandler1).toBeCalledTimes(0);
217 | expect(removeHandler2).toBeCalledTimes(1);
218 | });
219 |
220 | it('should remove all bindings from a previous container in case "root" is changed', () => {
221 | const FOO = token('FOO');
222 |
223 | const removeHandler1 = jest.fn();
224 | const binder1 = (container: Container) =>
225 | container.bindFactory(FOO, () => 'foo1', {onRemoved: removeHandler1});
226 |
227 | const [Monitor, monitorCallback] = createMonitor(FOO);
228 |
229 | const {rerender} = render(
230 |
231 |
232 | ,
233 | );
234 | expect(monitorCallback).toBeCalledTimes(1);
235 | expect(monitorCallback).lastCalledWith('foo1');
236 | expect(removeHandler1).toBeCalledTimes(0);
237 | monitorCallback.mockClear();
238 | removeHandler1.mockClear();
239 |
240 | rerender(
241 |
242 |
243 | ,
244 | );
245 | expect(monitorCallback).toBeCalledTimes(1);
246 | expect(monitorCallback).lastCalledWith('foo1');
247 | expect(removeHandler1).toBeCalledTimes(1);
248 | monitorCallback.mockClear();
249 | removeHandler1.mockClear();
250 |
251 | rerender(<>>);
252 | expect(monitorCallback).toBeCalledTimes(0);
253 | expect(removeHandler1).toBeCalledTimes(1);
254 | });
255 | });
256 |
257 | describe('CustomDependencyContainer', () => {
258 | it('should provide a custom container', () => {
259 | const FOO = token('FOO');
260 |
261 | const container = createContainer();
262 | container.bindValue(FOO, 'foo');
263 |
264 | const {result} = renderHook(useDependencyContainer, {
265 | wrapper: ({children}) => (
266 |
267 | {children}
268 |
269 | ),
270 | });
271 |
272 | expect(result.current?.resolve(FOO)).toBe('foo');
273 | });
274 |
275 | it('should not clean a custom container on rerender or unmount', () => {
276 | const FOO = token('FOO');
277 |
278 | const removeHandler1 = jest.fn();
279 | const container1 = createContainer();
280 | container1.bindFactory(FOO, () => 'foo1', {onRemoved: removeHandler1});
281 |
282 | const removeHandler2 = jest.fn();
283 | const container2 = createContainer();
284 | container2.bindFactory(FOO, () => 'foo2', {onRemoved: removeHandler2});
285 |
286 | const [Monitor, monitorCallback] = createMonitor(FOO);
287 |
288 | const {rerender} = render(
289 |
290 |
291 | ,
292 | );
293 |
294 | rerender(
295 |
296 |
297 | ,
298 | );
299 |
300 | rerender(<>>);
301 |
302 | expect(monitorCallback).toBeCalledTimes(2);
303 | expect(monitorCallback).nthCalledWith(1, 'foo1');
304 | expect(monitorCallback).nthCalledWith(2, 'foo2');
305 | expect(removeHandler1).toBeCalledTimes(0);
306 | expect(removeHandler2).toBeCalledTimes(0);
307 | });
308 | });
309 |
310 | function createMonitor(token: Token): [() => null, jest.Mock] {
311 | const callback = jest.fn();
312 |
313 | const Monitor = () => {
314 | const container = useDependencyContainer();
315 | useEffect(() => callback(container?.get(token)), [container]);
316 | return null;
317 | };
318 |
319 | return [Monitor, callback];
320 | }
321 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/DependencyContainer.tsx:
--------------------------------------------------------------------------------
1 | import type {Container} from 'ditox';
2 | import {createContainer} from 'ditox';
3 | import React, {
4 | createContext,
5 | ReactElement,
6 | ReactNode,
7 | useContext,
8 | useEffect,
9 | useMemo,
10 | } from 'react';
11 |
12 | export const DependencyContainerContext = createContext(
13 | undefined,
14 | );
15 |
16 | /**
17 | * A callback for binding dependencies to a container
18 | */
19 | export type DependencyContainerBinder = (container: Container) => unknown;
20 |
21 | /**
22 | * Specifies an existed container or options for a new container:
23 | * @property binder - A callback which setup bindings to the container.
24 | * @property root - If `true` then a new container does not depend on any parent containers
25 | */
26 | export type DependencyContainerParams = {
27 | children: ReactNode;
28 | root?: boolean;
29 | binder?: DependencyContainerBinder;
30 | };
31 |
32 | /**
33 | * Provides a new dependency container to React app
34 | *
35 | * This component creates a new container and provides it down to React children.
36 | *
37 | * If `binder` callback is specified, it will be called for a new container
38 | * to binds it with dependencies.
39 | *
40 | * If a parent container is exist, it is connected to the current one by default.
41 | * For making a new root container specify `root` parameter as `true`,
42 | * and the container will not depend on any parent container.
43 | *
44 | * @param params.binder - A callback which setup bindings to the container.
45 | * @param params.root - If `true` then a new container does not depend on any parent containers
46 | *
47 | * @example
48 | *
49 | * ```tsx
50 | * const TOKEN = token();
51 | *
52 | * function appDependencyBinder(container: Container) {
53 | * container.bindValue(TOKEN, 'value');
54 | * }
55 | *
56 | * function App() {
57 | * return (
58 | *
59 | *
60 | *
61 | * );
62 | * }
63 | * ```
64 | *
65 | */
66 | export function DependencyContainer(
67 | params: DependencyContainerParams,
68 | ): ReactElement {
69 | const {children, root, binder} = params;
70 | const parentContainer = useContext(DependencyContainerContext);
71 |
72 | const container = useMemo(() => {
73 | const container = createContainer(root ? undefined : parentContainer);
74 | binder?.(container);
75 |
76 | return container;
77 | }, [binder, parentContainer, root]);
78 |
79 | useEffect(() => {
80 | return () => container.removeAll();
81 | }, [container]);
82 |
83 | return (
84 |
85 | {children}
86 |
87 | );
88 | }
89 |
90 | /**
91 | * Provides a custom dependency container to React app
92 | *
93 | * @param params.container - a custom container
94 | *
95 | * @example
96 | * ```tsx
97 | * const container = useMemo(() => {
98 | * return createContainer();
99 | * }
100 | *
101 | * return (
102 | *
103 | * {children}
104 | *
105 | * );
106 | * ```
107 | */
108 | export function CustomDependencyContainer(params: {
109 | children: ReactNode;
110 | container: Container;
111 | }): ReactElement {
112 | const {children, container} = params;
113 |
114 | return (
115 |
116 | {children}
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/DependencyModule.test.tsx:
--------------------------------------------------------------------------------
1 | import {renderHook} from '@testing-library/react';
2 | import {
3 | bindModule,
4 | createContainer,
5 | Module,
6 | ModuleDeclaration,
7 | token,
8 | } from 'ditox';
9 | import React from 'react';
10 | import {CustomDependencyContainer} from './DependencyContainer';
11 | import {DependencyModule} from './DependencyModule';
12 | import {useDependency} from './hooks';
13 |
14 | describe('DependencyModule', () => {
15 | type TestModule = Module<{value: number}>;
16 |
17 | const MODULE_TOKEN = token();
18 | const VALUE_TOKEN = token();
19 |
20 | const MODULE: ModuleDeclaration = {
21 | token: MODULE_TOKEN,
22 | factory: () => ({
23 | value: 1,
24 | }),
25 | exports: {
26 | value: VALUE_TOKEN,
27 | },
28 | };
29 |
30 | it('should bind a module and its provided values', () => {
31 | const {result} = renderHook(() => useDependency(VALUE_TOKEN), {
32 | wrapper: ({children}) => (
33 | {children}
34 | ),
35 | });
36 |
37 | expect(result.current).toBe(1);
38 | });
39 |
40 | it('should bind a "scoped" module and its provided values', () => {
41 | const parent = createContainer();
42 | bindModule(parent, MODULE);
43 | const parentModule = parent.resolve(MODULE_TOKEN);
44 |
45 | const {result} = renderHook(() => useDependency(MODULE_TOKEN), {
46 | wrapper: ({children}) => (
47 |
48 |
49 | {children}
50 |
51 |
52 | ),
53 | });
54 |
55 | const childModule = result.current;
56 | expect(parentModule).not.toBe(childModule);
57 | expect(childModule.value).toBe(1);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/DependencyModule.tsx:
--------------------------------------------------------------------------------
1 | import type {Module, ModuleDeclaration} from 'ditox';
2 | import {bindModule} from 'ditox';
3 | import React, {ReactElement, ReactNode, useCallback} from 'react';
4 | import {
5 | DependencyContainer,
6 | DependencyContainerBinder,
7 | } from './DependencyContainer';
8 |
9 | /**
10 | * Binds the module to a new dependency container.
11 | *
12 | * If a parent container is exist, it is connected to the current one by default.
13 | *
14 | * @param params.module - Module declaration for binding
15 | * @param params.scope - Optional scope for binding: `singleton` (default) or `scoped`.
16 | *
17 | * @example
18 | *
19 | * ```tsx
20 | * const LOGGER_MODULE: ModuleDeclaration = {
21 | *
22 | * function App() {
23 | * return (
24 | *
25 | *
26 | *
27 | * );
28 | * }
29 | * ```
30 | */
31 | export function DependencyModule(params: {
32 | children: ReactNode;
33 | module: ModuleDeclaration>>;
34 | scope?: 'scoped' | 'singleton';
35 | }): ReactElement {
36 | const {children, module, scope} = params;
37 |
38 | const binder: DependencyContainerBinder = useCallback(
39 | (container) => bindModule(container, module, {scope}),
40 | [module, scope],
41 | );
42 |
43 | return {children};
44 | }
45 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/hooks.test.tsx:
--------------------------------------------------------------------------------
1 | import {renderHook} from '@testing-library/react';
2 | import {createContainer, token} from 'ditox';
3 | import React from 'react';
4 | import {CustomDependencyContainer} from './DependencyContainer';
5 | import {
6 | useDependency,
7 | useDependencyContainer,
8 | useOptionalDependency,
9 | } from './hooks';
10 |
11 | beforeAll(() => {
12 | jest.spyOn(console, 'error').mockImplementation();
13 | });
14 |
15 | describe('useDependencyContainer()', () => {
16 | it('should return a provided container in "strict" mode', () => {
17 | const container = createContainer();
18 |
19 | const {result} = renderHook(() => useDependencyContainer('strict'), {
20 | wrapper: ({children}) => (
21 |
22 | {children}
23 |
24 | ),
25 | });
26 |
27 | expect(result.current).toEqual(container);
28 | });
29 |
30 | it('should throw an error in "strict" mode in case a container is not provided', () => {
31 | expect(() => renderHook(() => useDependencyContainer('strict'))).toThrow(
32 | 'Container is not provided by DependencyContainer component',
33 | );
34 | });
35 |
36 | it('should return a provided container in "optional" mode', () => {
37 | const container = createContainer();
38 |
39 | const {result} = renderHook(() => useDependencyContainer(), {
40 | wrapper: ({children}) => (
41 |
42 | {children}
43 |
44 | ),
45 | });
46 |
47 | expect(result.current).toEqual(container);
48 | });
49 |
50 | it('should return "undefined" in "optional" mode in case a container is not provided', () => {
51 | const {result} = renderHook(() => useDependencyContainer());
52 | expect(result.current).toBeUndefined();
53 | });
54 | });
55 |
56 | describe('useDependency()', () => {
57 | it('should return a resolved dependency from a provided container', () => {
58 | const TOKEN = token('token');
59 | const container = createContainer();
60 | container.bindValue(TOKEN, 'value');
61 |
62 | const {result} = renderHook(() => useDependency(TOKEN), {
63 | wrapper: ({children}) => (
64 |
65 | {children}
66 |
67 | ),
68 | });
69 |
70 | expect(result.current).toEqual('value');
71 | });
72 |
73 | it('should throw an error in case a container is not provided', () => {
74 | const TOKEN = token('token');
75 |
76 | expect(() => renderHook(() => useDependency(TOKEN))).toThrowError(
77 | 'Container is not provided by DependencyContainer component',
78 | );
79 | });
80 |
81 | it('should throw an error in case a value is not resolved', () => {
82 | const TOKEN = token('token');
83 | const container = createContainer();
84 |
85 | expect(() => {
86 | renderHook(() => useDependency(TOKEN), {
87 | wrapper: ({children}) => (
88 |
89 | {children}
90 |
91 | ),
92 | });
93 | }).toThrowError('Token "token" is not provided');
94 | });
95 | });
96 |
97 | describe('useOptionalDependency()', () => {
98 | it('should return a resolved dependency from a provided container', () => {
99 | const TOKEN = token('token');
100 | const container = createContainer();
101 | container.bindValue(TOKEN, 'value');
102 |
103 | const {result} = renderHook(() => useOptionalDependency(TOKEN), {
104 | wrapper: ({children}) => (
105 |
106 | {children}
107 |
108 | ),
109 | });
110 |
111 | expect(result.current).toEqual('value');
112 | });
113 |
114 | it('should return "undefined" in case a container is not provided', () => {
115 | const TOKEN = token('token');
116 |
117 | const {result} = renderHook(() => useOptionalDependency(TOKEN));
118 |
119 | expect(result.current).toBeUndefined();
120 | });
121 |
122 | it('should return "undefined" in case a value is not resolved', () => {
123 | const TOKEN = token('token');
124 | const container = createContainer();
125 |
126 | const {result} = renderHook(() => useOptionalDependency(TOKEN), {
127 | wrapper: ({children}) => (
128 |
129 | {children}
130 |
131 | ),
132 | });
133 |
134 | expect(result.current).toBeUndefined();
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import {Container, Token} from 'ditox';
2 | import {useContext, useMemo} from 'react';
3 | import {DependencyContainerContext} from './DependencyContainer';
4 |
5 | /**
6 | * @category Hook
7 | *
8 | * Returns a dependency container. Throws an error in case the container is not provided.
9 | */
10 | export function useDependencyContainer(mode: 'strict'): Container;
11 | /**
12 | * @category Hook
13 | *
14 | * Returns a dependency container, or `undefined` in case the container is not provided.
15 | */
16 | export function useDependencyContainer(
17 | mode?: 'optional',
18 | ): Container | undefined;
19 | /**
20 | * @internal
21 | */
22 | export function useDependencyContainer(
23 | mode?: 'strict' | 'optional',
24 | ): Container | undefined {
25 | const container = useContext(DependencyContainerContext);
26 |
27 | if (!container && mode === 'strict') {
28 | throw new Error(
29 | 'Container is not provided by DependencyContainer component',
30 | );
31 | }
32 |
33 | return container;
34 | }
35 |
36 | /**
37 | * @category Hook
38 | *
39 | * Returns a dependency by token, or fails with an error.
40 | */
41 | export function useDependency(token: Token): T {
42 | const container = useDependencyContainer('strict');
43 | const value = useMemo(() => container.resolve(token), [container, token]);
44 | return value;
45 | }
46 |
47 | /**
48 | * @category Hook
49 | *
50 | * Returns a dependency by token, or `undefined` in case the dependency is not provided.
51 | */
52 | export function useOptionalDependency(token: Token): T | undefined {
53 | const container = useDependencyContainer();
54 | const value = useMemo(() => container?.get(token), [container, token]);
55 | return value;
56 | }
57 |
--------------------------------------------------------------------------------
/packages/ditox-react/src/index.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | DependencyContainerBinder,
3 | DependencyContainerParams,
4 | } from './DependencyContainer';
5 | export {
6 | CustomDependencyContainer,
7 | DependencyContainer,
8 | } from './DependencyContainer';
9 |
10 | export {
11 | useDependencyContainer,
12 | useDependency,
13 | useOptionalDependency,
14 | } from './hooks';
15 |
16 | export {DependencyModule} from './DependencyModule';
17 |
--------------------------------------------------------------------------------
/packages/ditox-react/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "files": ["src/index.ts"],
4 | "compilerOptions": {
5 | "noEmit": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/ditox-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/ditox/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See
4 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [3.0.0](https://github.com/mnasyrov/ditox/compare/v2.4.1...v3.0.0) (2024-07-12)
7 |
8 | ### Bug Fixes
9 |
10 | - Changed resolution of "scoped" bindings to keep a created value in the
11 | container which owns the factory
12 | ([#40](https://github.com/mnasyrov/ditox/issues/40))
13 | ([736ef2f](https://github.com/mnasyrov/ditox/commit/736ef2f927d43c91f027c68e230371cce3f50131))
14 | - Fixed binding modules
15 | ([997ca44](https://github.com/mnasyrov/ditox/commit/997ca44b09446a6e4d524ba9c16ce0c9cd7995d8))
16 |
17 | # Change Log
18 |
19 | All notable changes to this project will be documented in this file. See
20 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
21 |
22 | # [3.0.0-dev.2](https://github.com/mnasyrov/ditox/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2024-07-11)
23 |
24 | ### Bug Fixes
25 |
26 | - Fixed binding modules
27 | ([997ca44](https://github.com/mnasyrov/ditox/commit/997ca44b09446a6e4d524ba9c16ce0c9cd7995d8))
28 | - New factory binding doesn't reset a cached value
29 | ([7a33dc6](https://github.com/mnasyrov/ditox/commit/7a33dc63d5b95588c5ed5e7ad4e1e748230bcc2e))
30 |
31 | # Change Log
32 |
33 | All notable changes to this project will be documented in this file. See
34 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
35 |
36 | # [3.0.0-dev.1](https://github.com/mnasyrov/ditox/compare/v2.4.1...v3.0.0-dev.1) (2024-05-31)
37 |
38 | ### Bug Fixes
39 |
40 | - Changed resolution of "scoped" bindings to keep a created value in the
41 | container which owns the factory
42 | ([#40](https://github.com/mnasyrov/ditox/issues/40))
43 | ([736ef2f](https://github.com/mnasyrov/ditox/commit/736ef2f927d43c91f027c68e230371cce3f50131))
44 |
45 | # Change Log
46 |
47 | All notable changes to this project will be documented in this file. See
48 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
49 |
50 | ## [2.4.1](https://github.com/mnasyrov/ditox/compare/v2.4.0...v2.4.1) (2023-10-27)
51 |
52 | **Note:** Version bump only for package ditox
53 |
54 | # Change Log
55 |
56 | All notable changes to this project will be documented in this file. See
57 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
58 |
59 | # [2.4.0](https://github.com/mnasyrov/ditox/compare/v2.3.1...v2.4.0) (2023-09-08)
60 |
61 | ### Features
62 |
63 | - Ability to create a shareable token by providing a key for its symbol
64 | ([4fde1d9](https://github.com/mnasyrov/ditox/commit/4fde1d95dc728018c67f8fc5e154597e9115d8b5))
65 |
66 | # Change Log
67 |
68 | All notable changes to this project will be documented in this file. See
69 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
70 |
71 | ## [2.3.1](https://github.com/mnasyrov/ditox/compare/v2.3.0...v2.3.1) (2023-04-01)
72 |
73 | **Note:** Version bump only for package ditox
74 |
75 | # [2.3.0](https://github.com/mnasyrov/ditox/compare/v2.2.0...v2.3.0) (2022-12-22)
76 |
77 | ### Bug Fixes
78 |
79 | - Compatible with Typescript 4.9. Updated dependencies.
80 | ([d8aa9e2](https://github.com/mnasyrov/ditox/commit/d8aa9e2a4a665d6e14f3581c652feb6ad01b71d8))
81 |
82 | # [2.2.0](https://github.com/mnasyrov/ditox/compare/v2.1.0...v2.2.0) (2022-01-29)
83 |
84 | ### Code Refactoring
85 |
86 | - Removed the deprecated code
87 | ([a821171](https://github.com/mnasyrov/ditox/commit/a821171e5de011b82f4f1aedbe91f3f6d2ef6360))
88 |
89 | ### BREAKING CHANGES
90 |
91 | - Removed the deprecated code:
92 |
93 | * `exportedProps` property of the module declaration
94 | * utilities:
95 | - getValues()
96 | - getProps()
97 | - resolveProps()
98 | - injectableProps() Use v2.1 version to migrate to the unified API.
99 |
100 | # [2.1.0](https://github.com/mnasyrov/ditox/compare/v2.0.0...v2.1.0) (2022-01-29)
101 |
102 | ### Features
103 |
104 | - Unified API of resolving utilities. Introduced "injectableClass()" utility.
105 | ([aea7a8f](https://github.com/mnasyrov/ditox/commit/aea7a8fdedae079e8bad86f8556669ab3257197a))
106 |
107 | # [2.0.0](https://github.com/mnasyrov/ditox/compare/v1.4.2...v2.0.0) (2022-01-26)
108 |
109 | ### chore
110 |
111 | - Dropped supporting of Flow.js typings.
112 | ([9a94f55](https://github.com/mnasyrov/ditox/commit/9a94f558daf107ff4744462e078571e4ccc7c444))
113 |
114 | ### BREAKING CHANGES
115 |
116 | - Dropped supporting of Flow.js typings. Use the previous versions of the
117 | library with Flow.js
118 |
119 | ## [1.4.2](https://github.com/mnasyrov/ditox/compare/v1.4.1...v1.4.2) (2021-11-18)
120 |
121 | ### Bug Fixes
122 |
123 | - Typings for `bindMultiValue()` utility function.
124 | ([cb2b2da](https://github.com/mnasyrov/ditox/commit/cb2b2da27ad7bd293d8d5591017422ba4599d09a))
125 |
126 | ## [1.4.1](https://github.com/mnasyrov/ditox/compare/v1.4.0...v1.4.1) (2021-11-11)
127 |
128 | ### Bug Fixes
129 |
130 | - beforeBinding() must be called before importing external modules.
131 | ([153c602](https://github.com/mnasyrov/ditox/commit/153c60256a4c2796e5a9f2d9c5115c3ff3f2f10b))
132 |
133 | # [1.4.0](https://github.com/mnasyrov/ditox/compare/v1.3.0...v1.4.0) (2021-10-30)
134 |
135 | ### Features
136 |
137 | - `ModuleDeclaration.imports` property takes module entries for binding with the
138 | module.
139 | ([6757bdc](https://github.com/mnasyrov/ditox/commit/6757bdc87a02d2336ca07661c50812a5823ca844))
140 |
141 | # [1.3.0](https://github.com/mnasyrov/ditox/compare/v1.2.0...v1.3.0) (2021-08-04)
142 |
143 | ### Features
144 |
145 | - `declareModule()` and `declareModuleBindings()` utility functions
146 | ([990ffe7](https://github.com/mnasyrov/ditox/commit/990ffe79af37c6ee56738b6ac7e9c071818d10f7))
147 |
148 | # [1.2.0](https://github.com/mnasyrov/ditox/compare/v1.1.0...v1.2.0) (2021-06-20)
149 |
150 | ### Bug Fixes
151 |
152 | - Fixed resolving a singleton registered in the parent.
153 | ([224c595](https://github.com/mnasyrov/ditox/commit/224c59585e43c68cc23e6abf521ecad09aeb1c7a))
154 |
155 | ### Features
156 |
157 | - `bindModules()` utility function.
158 | ([e62d6c3](https://github.com/mnasyrov/ditox/commit/e62d6c3332f991b8e3943ad269bd6e76cb6a266c))
159 | - Dependency modules
160 | ([d01d333](https://github.com/mnasyrov/ditox/commit/d01d33347788c7eeeaee014fa794684ffbd4b2e7))
161 |
162 | # [1.1.0](https://github.com/mnasyrov/ditox/compare/v1.0.2...v1.1.0) (2021-04-17)
163 |
164 | ### Bug Fixes
165 |
166 | - Fixed typings and arity for `getValues()`, `resolveValues()` and
167 | `injectable()`.
168 | ([92f57dc](https://github.com/mnasyrov/ditox/commit/92f57dcc1777c4c622d61c68196db3d48f3fa186))
169 |
170 | ### Features
171 |
172 | - `Container.hasToken()` for checking token presence.
173 | ([56043ae](https://github.com/mnasyrov/ditox/commit/56043aec494481cc624b30d81e33df33a8273e63))
174 | - Utilities for resolving object properties by tokens
175 | ([d4a41e8](https://github.com/mnasyrov/ditox/commit/d4a41e8d777540905a4bc15fc22bcb06a85cf90a))
176 |
177 | ## [1.0.1](https://github.com/mnasyrov/ditox/compare/v1.0.0...v1.0.1) (2021-03-04)
178 |
179 | ### Bug Fixes
180 |
181 | - Fixed bundled typings for Typescript
182 | ([7b1499e](https://github.com/mnasyrov/ditox/commit/7b1499e7cf1506f24f72387d83a055e6a4d3c336))
183 |
184 | # [1.0.0](https://github.com/mnasyrov/ditox/compare/v0.5.4...v1.0.0) (2021-02-28)
185 |
186 | **Note:** Version bump only for package ditox
187 |
188 | ## [0.5.4](https://github.com/mnasyrov/ditox/compare/v0.5.3...v0.5.4) (2021-02-28)
189 |
190 | **Note:** Version bump only for package ditox
191 |
192 | ## [0.5.3](https://github.com/mnasyrov/ditox/compare/v0.5.2...v0.5.3) (2021-02-25)
193 |
194 | **Note:** Version bump only for package ditox
195 |
196 | ## [0.5.1](https://github.com/mnasyrov/ditox/compare/v0.5.0...v0.5.1) (2021-02-25)
197 |
198 | **Note:** Version bump only for package ditox
199 |
200 | # [0.5.0](https://github.com/mnasyrov/ditox/compare/v0.4.1...v0.5.0) (2021-02-24)
201 |
202 | ### Features
203 |
204 | - Introduced @ditox/react - tooling for React apps
205 | ([cd9c9db](https://github.com/mnasyrov/ditox/commit/cd9c9db9d65fda468f0e740c49e090757f1ac73a))
206 |
--------------------------------------------------------------------------------
/packages/ditox/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Mikhail Nasyrov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/ditox/README.md:
--------------------------------------------------------------------------------
1 | # ditox package
2 |
3 |
4 |
5 | **Dependency injection for modular web applications**
6 |
7 | Please see the documentation at [ditox.js.org](https://ditox.js.org)
8 |
9 | [](https://www.npmjs.com/package/ditox)
10 | [](https://github.com/mnasyrov/ditox/stargazers)
11 | [](https://www.npmjs.com/package/ditox)
12 | [](https://github.com/mnasyrov/ditox/blob/master/LICENSE)
13 | [](https://coveralls.io/github/mnasyrov/ditox)
14 |
15 | ## Installation
16 |
17 | You can use the following command to install this package:
18 |
19 | ```shell
20 | npm install --save ditox
21 | ```
22 |
23 | The package can be used as [UMD](https://github.com/umdjs/umd) module. Use
24 | [jsdelivr.com](https://jsdelivr.com) CDN site to load
25 | [ditox](https://www.jsdelivr.com/package/npm/ditox):
26 |
27 | ```html
28 |
29 |
32 | ```
33 |
34 | ## General description
35 |
36 | DI pattern in general allows to declare and construct a code graph of an
37 | application. It can be described by following phases:
38 |
39 | 1. Code declaration phase:
40 |
41 | - Defining public API and types of business layer
42 | - Creation of injection tokens
43 | - Declaring DI modules
44 |
45 | 2. Binding phase:
46 |
47 | - Creation of a container
48 | - Binding values, factories and modules to the container
49 |
50 | 3. Runtime phase:
51 |
52 | - Running the code
53 | - Values are constructed by registered factories
54 |
55 | Diagram:
56 |
57 |
58 |
59 | ## Usage Example
60 |
61 | ```js
62 | import {createContainer, injectable, optional, token} from 'ditox';
63 |
64 | // This is app code, some factory functions and classes:
65 | function createStorage(config) {}
66 |
67 | function createLogger(config) {}
68 |
69 | class UserService {
70 | constructor(storage, logger) {}
71 | }
72 |
73 | // Define tokens for injections.
74 | const STORAGE_TOKEN = token < UserService > 'Token description for debugging';
75 | const LOGGER_TOKEN = token();
76 | const USER_SERVICE_TOKEN = token();
77 |
78 | // Token can be optional with a default value.
79 | const STORAGE_CONFIG_TOKEN = optional(token(), {name: 'default storage'});
80 |
81 | // Create the container.
82 | const container = createContainer();
83 |
84 | // Provide a value to the container.
85 | container.bindValue(STORAGE_CONFIG_TOKEN, {name: 'custom storage'});
86 |
87 | // Dynamic values are provided by factories.
88 |
89 | // A factory can be decorated with `injectable()` to resolve its arguments.
90 | // By default, a factory has `singleton` lifetime.
91 | container.bindFactory(
92 | STORAGE_TOKEN,
93 | injectable(createStorage, STORAGE_CONFIG_TOKEN),
94 | );
95 |
96 | // A factory can have `transient` lifetime to create a value on each resolving.
97 | container.bindFactory(LOGGER_TOKEN, createLogger, {scope: 'transient'});
98 |
99 | // A class can be injected by `injectableClass()` which calls its constructor
100 | // with injected dependencies as arguments.
101 | container.bindFactory(
102 | USER_SERVICE_TOKEN,
103 | injectable(
104 | (storage, logger) => new UserService(storage, logger),
105 | STORAGE_TOKEN,
106 | // A token can be made optional to resolve with a default value
107 | // when it is not found during resolving.
108 | optional(LOGGER_TOKEN),
109 | ),
110 | {
111 | // `scoped` and `singleton` scopes can have `onRemoved` callback.
112 | // It is called when a token is removed from the container.
113 | scope: 'scoped',
114 | onRemoved: (userService) => userService.destroy(),
115 | },
116 | );
117 |
118 | // Get a value from the container, it returns `undefined` in case a value is not found.
119 | const logger = container.get(LOGGER_TOKEN);
120 |
121 | // Resolve a value, it throws `ResolverError` in case a value is not found.
122 | const userService = container.resolve(USER_SERVICE_TOKEN);
123 |
124 | // Remove a value from the container.
125 | container.remove(LOGGER_TOKEN);
126 |
127 | // Clean up the container.
128 | container.removeAll();
129 | ```
130 |
131 | ## Container Hierarchy
132 |
133 | Ditox.js supports "parent-child" hierarchy. If the child container cannot to
134 | resolve a token, it asks the parent container to resolve it:
135 |
136 | ```js
137 | import {creatContainer, token} from 'ditox';
138 |
139 | const V1_TOKEN = token();
140 | const V2_TOKEN = token();
141 |
142 | const parent = createContainer();
143 | parent.bindValue(V1_TOKEN, 10);
144 | parent.bindValue(V2_TOKEN, 20);
145 |
146 | const container = createContainer(parent);
147 | container.bindValue(V2_TOKEN, 21);
148 |
149 | container.resolve(V1_TOKEN); // 10
150 | container.resolve(V2_TOKEN); // 21
151 | ```
152 |
153 | ## Factory Lifetimes
154 |
155 | Ditox.js supports managing the lifetime of values which are produced by
156 | factories. There are the following types:
157 |
158 | - `singleton` - **This is the default**. The value is created and cached by the
159 | most distant parent container which owns the factory function.
160 | - `scoped` - The value is created and cached by the nearest container which owns
161 | the factory function.
162 | - `transient` - The value is created every time it is resolved.
163 |
164 | ### `singleton`
165 |
166 | **This is the default scope**. "Singleton" allows to cache a produced value by a
167 | most distant parent container which registered the factory function:
168 |
169 | ```js
170 | import {creatContainer, token} from 'ditox';
171 |
172 | const TAG_TOKEN = token();
173 | const LOGGER_TOKEN = token();
174 |
175 | const createLogger = (tag) => (message) => console.log(`[${tag}] ${message}`);
176 |
177 | const parent = createContainer();
178 | parent.bindValue(TAG_TOKEN, 'parent');
179 | parent.bindFactory(LOGGER_TOKEN, injectable(createLogger, TAG_TOKEN), {
180 | scope: 'singleton',
181 | });
182 |
183 | const container1 = createContainer(parent);
184 | container1.bindValue(TAG_TOKEN, 'container1');
185 |
186 | const container2 = createContainer(parent);
187 | container2.bindValue(TAG_TOKEN, 'container2');
188 |
189 | parent.resolve(LOGGER_TOKEN)('xyz'); // [parent] xyz
190 | container1.resolve(LOGGER_TOKEN)('foo'); // [parent] foo
191 | container2.resolve(LOGGER_TOKEN)('bar'); // [parent] bar
192 | ```
193 |
194 | ### `scoped`
195 |
196 | "Scoped" lifetime allows to have sub-containers with own instances of some
197 | services which can be disposed. For example, a context during HTTP request
198 | handling, or other unit of work:
199 |
200 | ```js
201 | import {creatContainer, token} from 'ditox';
202 |
203 | const TAG_TOKEN = token();
204 | const LOGGER_TOKEN = token();
205 |
206 | const createLogger = (tag) => (message) => console.log(`[${tag}] ${message}`);
207 |
208 | const parent = createContainer();
209 | // `scoped` is default scope and can be omitted.
210 | parent.bindFactory(LOGGER_TOKEN, injectable(createLogger, TAG_TOKEN), {
211 | scope: 'scoped',
212 | });
213 |
214 | const container1 = createContainer(parent);
215 | container1.bindValue(TAG_TOKEN, 'container1');
216 |
217 | const container2 = createContainer(parent);
218 | container2.bindValue(TAG_TOKEN, 'container2');
219 |
220 | parent.resolve(LOGGER_TOKEN)('xyz'); // throws ResolverError, the parent does not have TAG value.
221 | container1.resolve(LOGGER_TOKEN)('foo'); // [container1] foo
222 | container2.resolve(LOGGER_TOKEN)('bar'); // [container2] bar
223 |
224 | // Dispose a container.
225 | container1.removeAll();
226 | ```
227 |
228 | ### `transient`
229 |
230 | "Transient" makes to a produce values by the factory for each resolving:
231 |
232 | ```js
233 | import {createContainer, token} from 'ditox';
234 |
235 | const TAG_TOKEN = token();
236 | const LOGGER_TOKEN = token();
237 |
238 | const createLogger = (tag) => (message) => console.log(`[${tag}] ${message}`);
239 |
240 | const parent = createContainer();
241 | parent.bindValue(TAG_TOKEN, 'parent');
242 | parent.bindFactory(LOGGER_TOKEN, injectable(createLogger, TAG_TOKEN), {
243 | scope: 'transient',
244 | });
245 |
246 | const container1 = createContainer(parent);
247 | container1.bindValue(TAG_TOKEN, 'container1');
248 |
249 | const container2 = createContainer(parent);
250 | container2.bindValue(TAG_TOKEN, 'container2');
251 |
252 | parent.resolve(LOGGER_TOKEN)('xyz'); // [parent] xyz
253 | container1.resolve(LOGGER_TOKEN)('foo'); // [container1] foo
254 | container2.resolve(LOGGER_TOKEN)('bar'); // [container2] bar
255 |
256 | parent.bindValue(TAG_TOKEN, 'parent-rebind');
257 | parent.resolve(LOGGER_TOKEN)('xyz'); // [parent-rebind] xyz
258 | ```
259 |
260 | ## Dependency Modules
261 |
262 | Dependencies can be organized as modules in declarative way with
263 | `ModuleDeclaration`. It is useful for providing pieces of functionality from
264 | libraries to an app which depends on them.
265 |
266 | ```typescript
267 | import {Module, ModuleDeclaration, token} from 'ditox';
268 | import {TRANSPORT_TOKEN} from './transport';
269 |
270 | export type Logger = {log: (message: string) => void};
271 | export const LOGGER_TOKEN = token();
272 |
273 | type LoggerModule = Module<{logger: Logger}>;
274 |
275 | const LOGGER_MODULE_TOKEN = token();
276 |
277 | const LOGGER_MODULE: ModuleDeclaration = {
278 | // An optional explicit token for a module itself
279 | token: LOGGER_MODULE_TOKEN,
280 |
281 | factory: (container) => {
282 | const transport = container.resolve(TRANSPORT_TOKEN).open();
283 | return {
284 | logger: {log: (message) => transport.write(message)},
285 | destroy: () => transport.close(),
286 | };
287 | },
288 | exports: {
289 | logger: LOGGER_TOKEN,
290 | },
291 | };
292 | ```
293 |
294 | Later such module declarations can be bound to a container:
295 |
296 | ```typescript
297 | const container = createContainer();
298 |
299 | // bind a single module
300 | bindModule(container, LOGGER_MODULE);
301 |
302 | // or bind multiple depenendency modules
303 | bindModules(container, [DATABASE_MODULE, CONFIG_MODULE, API_MODULE]);
304 | ```
305 |
306 | Utility functions for module declarations:
307 |
308 | - `declareModule()` – declare a module as `ModuleDeclaration` however `token`
309 | field can be optional for anonymous modules.
310 | - `declareModuleBindings()` – declares an anonymous module with imports. This
311 | module binds the provided ones to a container.
312 |
313 | Example for these functions:
314 |
315 | ```typescript
316 | const LOGGER_MODULE = declareModule({
317 | factory: createLoggerModule,
318 | exports: {
319 | logger: LOGGER_TOKEN,
320 | },
321 | });
322 |
323 | const APP_MODULE = declareModuleBinding([LOGGER_MODULE, DATABASE_MODULE]);
324 | ```
325 |
326 | ---
327 |
328 | This project is licensed under the
329 | [MIT license](https://github.com/mnasyrov/ditox/blob/master/LICENSE).
330 |
--------------------------------------------------------------------------------
/packages/ditox/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/index';
2 |
--------------------------------------------------------------------------------
/packages/ditox/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ditox",
3 | "version": "3.0.0",
4 | "description": "Dependency injection for modular web applications",
5 | "license": "MIT",
6 | "author": "Mikhail Nasyrov (https://github.com/mnasyrov)",
7 | "homepage": "https://github.com/mnasyrov/ditox",
8 | "bugs": "https://github.com/mnasyrov/ditox/issues",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/mnasyrov/ditox.git"
12 | },
13 | "keywords": [
14 | "dependency container",
15 | "dependency injection",
16 | "typescript",
17 | "javascript",
18 | "npm",
19 | "dependency",
20 | "injection",
21 | "container",
22 | "module",
23 | "ioc",
24 | "di"
25 | ],
26 | "engines": {
27 | "node": ">=12"
28 | },
29 | "main": "dist/cjs/index.js",
30 | "module": "dist/esm/index.js",
31 | "jsnext:main": "dist/esm/index.js",
32 | "unpkg": "dist/umd/index.js",
33 | "umd:main": "dist/umd/index.js",
34 | "browser": "dist/browser/index.js",
35 | "react-native": "dist/browser/index.js",
36 | "types": "dist/esm/index.d.ts",
37 | "source": "src/index.ts",
38 | "sideEffects": false,
39 | "files": [
40 | "dist",
41 | "docs",
42 | "src",
43 | "LICENSE",
44 | "README.md"
45 | ],
46 | "scripts": {
47 | "clean": "shx rm -rf dist lib",
48 | "build": "npm run build:cjs && npm run build:esm && npm run build:rollup",
49 | "build:cjs": "tsc -p tsconfig.build.json --outDir dist/cjs --module commonjs",
50 | "build:esm": "tsc -p tsconfig.build.json --outDir dist/esm --module es2015",
51 | "build:rollup": "rollup -c",
52 | "typedoc": "typedoc"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/ditox/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import typescript from '@rollup/plugin-typescript';
3 | import dts from 'rollup-plugin-dts';
4 | import pkg from './package.json' assert {type: 'json'};
5 |
6 | const UMD_LIB_NAME = 'Ditox';
7 |
8 | export default [
9 | {
10 | input: 'src/index.ts',
11 | output: [{file: 'dist/browser/index.d.ts'}, {file: 'dist/umd/index.d.ts'}],
12 | plugins: [dts()],
13 | },
14 | {
15 | input: 'src/index.ts',
16 | output: [
17 | {
18 | name: UMD_LIB_NAME,
19 | file: pkg.browser,
20 | format: 'es',
21 | sourcemap: true,
22 | },
23 | {
24 | name: UMD_LIB_NAME,
25 | file: pkg['umd:main'],
26 | format: 'umd',
27 | sourcemap: true,
28 | },
29 | ],
30 | plugins: [
31 | typescript({
32 | target: 'es5',
33 | declaration: false,
34 | }),
35 | terser({
36 | output: {comments: false},
37 | }),
38 | ],
39 | },
40 | ];
41 |
--------------------------------------------------------------------------------
/packages/ditox/src/container.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CONTAINER,
3 | createContainer,
4 | PARENT_CONTAINER,
5 | ResolverError,
6 | } from './container';
7 | import {optional, token} from './tokens';
8 | import {injectable} from './utils';
9 |
10 | const NUMBER = token('number');
11 | const STRING = token('string');
12 |
13 | describe('Container', () => {
14 | describe('bindValue()', () => {
15 | it('should bind a value to the container', () => {
16 | const container = createContainer();
17 | container.bindValue(NUMBER, 1);
18 | expect(container.get(NUMBER)).toBe(1);
19 | });
20 |
21 | it('should rebind a value in case it was bound', () => {
22 | const container = createContainer();
23 | container.bindValue(NUMBER, 1);
24 | container.bindValue(NUMBER, 2);
25 | expect(container.get(NUMBER)).toBe(2);
26 | });
27 |
28 | it('should bind a result of a factory and prevent invoking it', () => {
29 | const container = createContainer();
30 |
31 | const factory = jest.fn(() => 1);
32 | container.bindFactory(NUMBER, factory);
33 | container.bindValue(NUMBER, 2);
34 |
35 | expect(container.get(NUMBER)).toBe(2);
36 | expect(factory).toBeCalledTimes(0);
37 | });
38 |
39 | it('should not rebind CONTAINER and PARENT_CONTAINER tokens', () => {
40 | const parent = createContainer();
41 | const container = createContainer(parent);
42 |
43 | const custom = createContainer();
44 | container.bindValue(CONTAINER, custom);
45 | container.bindValue(PARENT_CONTAINER, custom);
46 |
47 | expect(container.get(CONTAINER)).toBe(container);
48 | expect(container.get(PARENT_CONTAINER)).toBe(parent);
49 | });
50 | });
51 |
52 | describe('bindFactory()', () => {
53 | it('should bind a factory to the container', () => {
54 | const container = createContainer();
55 |
56 | let containerArg;
57 | const factory = jest.fn((arg) => {
58 | containerArg = arg;
59 | return 1;
60 | });
61 | container.bindFactory(NUMBER, factory);
62 |
63 | expect(container.get(NUMBER)).toBe(1);
64 | expect(factory).toBeCalledTimes(1);
65 | expect(containerArg).toBe(container);
66 | });
67 |
68 | it('should rebind a factory in case it was bound', () => {
69 | const container = createContainer();
70 |
71 | const factory1 = jest.fn(() => 1);
72 | const factory2 = jest.fn(() => 2);
73 | container.bindFactory(NUMBER, factory1);
74 | container.bindFactory(NUMBER, factory2);
75 |
76 | expect(container.get(NUMBER)).toBe(2);
77 | expect(factory1).toBeCalledTimes(0);
78 | expect(factory2).toBeCalledTimes(1);
79 | });
80 |
81 | it('should bind a factory with "singleton" scope', () => {
82 | const parent = createContainer();
83 | const container = createContainer(parent);
84 |
85 | const START = token();
86 | parent.bindValue(START, 10);
87 | container.bindValue(START, 20);
88 |
89 | let counter = 0;
90 | const factory = jest.fn((start) => start + ++counter);
91 | parent.bindFactory(NUMBER, injectable(factory, START), {
92 | scope: 'singleton',
93 | });
94 |
95 | container.bindFactory(NUMBER, injectable(factory, START), {
96 | scope: 'singleton',
97 | });
98 |
99 | expect(container.get(NUMBER)).toBe(11);
100 | expect(container.get(NUMBER)).toBe(11);
101 | expect(parent.get(NUMBER)).toBe(11);
102 | expect(container.get(NUMBER)).toBe(11);
103 |
104 | expect(factory).toBeCalledTimes(1);
105 | });
106 |
107 | it('should bind a factory with "scoped" scope', () => {
108 | const parent = createContainer();
109 | const container = createContainer(parent);
110 |
111 | const START = token();
112 | parent.bindValue(START, 10);
113 | container.bindValue(START, 20);
114 |
115 | let counter = 0;
116 | const factory = jest.fn((start) => start + ++counter);
117 | parent.bindFactory(NUMBER, injectable(factory, START), {
118 | scope: 'scoped',
119 | });
120 |
121 | expect(container.get(NUMBER)).toBe(11);
122 | expect(container.get(NUMBER)).toBe(11);
123 | expect(parent.get(NUMBER)).toBe(11);
124 | expect(container.get(NUMBER)).toBe(11);
125 |
126 | expect(factory).toBeCalledTimes(1);
127 | });
128 |
129 | it('should inherit a factory with "scoped" scope', async () => {
130 | const parentParent = createContainer();
131 | let counter = 0;
132 | const factory = jest.fn(() => ++counter);
133 |
134 | parentParent.bindFactory(NUMBER, factory);
135 |
136 | expect(parentParent.get(NUMBER)).toBe(1);
137 |
138 | const parent = createContainer(parentParent);
139 |
140 | parent.bindFactory(NUMBER, injectable(factory), {
141 | scope: 'scoped',
142 | });
143 |
144 | expect(parent.get(NUMBER)).toBe(2);
145 |
146 | const container = createContainer(parent);
147 |
148 | expect(container.get(NUMBER)).toBe(2);
149 | expect(parent.get(NUMBER)).toBe(2);
150 |
151 | expect(factory).toBeCalledTimes(2);
152 |
153 | container.removeAll();
154 | expect(parent.get(NUMBER)).toBe(2);
155 | });
156 |
157 | it('should bind a factory with "singleton" scope by default', () => {
158 | const parent = createContainer();
159 | const container = createContainer(parent);
160 |
161 | const START = token();
162 | parent.bindValue(START, 10);
163 | container.bindValue(START, 20);
164 |
165 | let counter = 0;
166 | const factory = jest.fn((start) => start + ++counter);
167 | parent.bindFactory(NUMBER, injectable(factory, START));
168 |
169 | expect(container.get(NUMBER)).toBe(11);
170 | expect(container.get(NUMBER)).toBe(11);
171 | expect(parent.get(NUMBER)).toBe(11);
172 | expect(container.get(NUMBER)).toBe(11);
173 |
174 | expect(factory).toBeCalledTimes(1);
175 | });
176 |
177 | test('order of scoped containers', () => {
178 | const parent = createContainer();
179 | const container1 = createContainer(parent);
180 | const container2 = createContainer(parent);
181 | const container3 = createContainer(parent);
182 | const childContainer1 = createContainer(container1);
183 | const childContainer2 = createContainer(container2);
184 | const childContainer3 = createContainer(container3);
185 |
186 | const START = token();
187 | parent.bindValue(START, 0);
188 | container1.bindValue(START, 10);
189 | container2.bindValue(START, 20);
190 | container3.bindValue(START, 30);
191 |
192 | let counter = 0;
193 | const factory = jest.fn((start) => start + ++counter);
194 | parent.bindFactory(NUMBER, injectable(factory, START), {
195 | scope: 'scoped',
196 | });
197 |
198 | container1.bindFactory(NUMBER, injectable(factory, START), {
199 | scope: 'scoped',
200 | });
201 |
202 | container2.bindFactory(NUMBER, injectable(factory, START), {
203 | scope: 'scoped',
204 | });
205 |
206 | container3.bindFactory(NUMBER, injectable(factory, START), {
207 | scope: 'scoped',
208 | });
209 |
210 | childContainer1.bindValue(START, 1);
211 | childContainer2.bindValue(START, 2);
212 | childContainer3.bindValue(START, 3);
213 |
214 | expect(container1.get(NUMBER)).toBe(11);
215 | expect(container2.get(NUMBER)).toBe(22);
216 | expect(container3.get(NUMBER)).toBe(33);
217 | expect(parent.get(NUMBER)).toBe(4);
218 | expect(container1.get(NUMBER)).toBe(11);
219 | expect(container2.get(NUMBER)).toBe(22);
220 | expect(container3.get(NUMBER)).toBe(33);
221 |
222 | expect(childContainer1.get(NUMBER)).toBe(11);
223 | expect(childContainer2.get(NUMBER)).toBe(22);
224 | expect(childContainer3.get(NUMBER)).toBe(33);
225 |
226 | expect(factory).toBeCalledTimes(4);
227 | });
228 |
229 | it('should bind a factory with "transient" scope', () => {
230 | const parent = createContainer();
231 | const container = createContainer(parent);
232 |
233 | const START = token();
234 | parent.bindValue(START, 10);
235 | container.bindValue(START, 20);
236 |
237 | let counter = 0;
238 | const factory = jest.fn((start) => start + ++counter);
239 | parent.bindFactory(NUMBER, injectable(factory, START), {
240 | scope: 'transient',
241 | });
242 |
243 | expect(container.get(NUMBER)).toBe(21);
244 | expect(container.get(NUMBER)).toBe(22);
245 | expect(parent.get(NUMBER)).toBe(13);
246 | expect(container.get(NUMBER)).toBe(24);
247 |
248 | expect(factory).toBeCalledTimes(4);
249 | });
250 |
251 | it('should bind a factory with "onRemoved" callback', () => {
252 | const container = createContainer();
253 |
254 | const factory = jest.fn(() => 1);
255 | const callback = jest.fn();
256 | container.bindFactory(NUMBER, factory, {onRemoved: callback});
257 |
258 | expect(container.get(NUMBER)).toBe(1);
259 | container.remove(NUMBER);
260 | expect(factory).toBeCalledTimes(1);
261 | expect(callback).toBeCalledTimes(1);
262 | });
263 |
264 | it('should not rebind CONTAINER and PARENT_CONTAINER tokens', () => {
265 | const parent = createContainer();
266 | const container = createContainer(parent);
267 |
268 | const factory = () => createContainer();
269 | container.bindFactory(CONTAINER, factory);
270 | container.bindFactory(PARENT_CONTAINER, factory);
271 |
272 | expect(container.get(CONTAINER)).toBe(container);
273 | expect(container.get(PARENT_CONTAINER)).toBe(parent);
274 | });
275 | });
276 |
277 | describe('remove()', () => {
278 | it('should remove a value', () => {
279 | const container = createContainer();
280 | container.bindValue(NUMBER, 1);
281 | container.remove(NUMBER);
282 | expect(container.get(NUMBER)).toBeUndefined();
283 | });
284 |
285 | it('should remove "singleton" factory silently in case its value has never been resolved', () => {
286 | const container = createContainer();
287 |
288 | const factory = jest.fn(() => 1);
289 | const onRemoved = jest.fn();
290 | container.bindFactory(NUMBER, factory, {scope: 'singleton', onRemoved});
291 | container.remove(NUMBER);
292 |
293 | expect(container.get(NUMBER)).toBeUndefined();
294 | expect(factory).toHaveBeenCalledTimes(0);
295 | expect(onRemoved).toHaveBeenCalledTimes(0);
296 | });
297 |
298 | it('should remove "singleton" factory with calling "onRemoved" in case its value has been resolved', () => {
299 | const container = createContainer();
300 |
301 | const factory = jest.fn(() => 100);
302 | const onRemoved = jest.fn();
303 | container.bindFactory(NUMBER, factory, {scope: 'singleton', onRemoved});
304 |
305 | expect(container.get(NUMBER)).toBe(100);
306 | container.remove(NUMBER);
307 |
308 | expect(container.get(NUMBER)).toBeUndefined();
309 | expect(factory).toHaveBeenCalledTimes(1);
310 | expect(onRemoved).toHaveBeenCalledTimes(1);
311 | expect(onRemoved).toHaveBeenCalledWith(100);
312 | });
313 |
314 | it('should remove "scoped" factory with different values in case of parent-child hierarchy', () => {
315 | const parent = createContainer();
316 | const container = createContainer(parent);
317 |
318 | let count = 1;
319 | const factory = jest.fn(() => count++);
320 | const onRemoved = jest.fn();
321 | parent.bindFactory(NUMBER, factory, {scope: 'scoped', onRemoved});
322 |
323 | expect(parent.get(NUMBER)).toBe(1);
324 | expect(container.get(NUMBER)).toBe(1);
325 |
326 | // Continue the main test
327 | parent.remove(NUMBER);
328 | container.remove(NUMBER);
329 | expect(onRemoved).toHaveBeenNthCalledWith(1, 1);
330 | expect(onRemoved).toHaveBeenCalledTimes(1);
331 | });
332 |
333 | it('should remove "transient" factory in case its value has never been resolved', () => {
334 | const container = createContainer();
335 |
336 | const factory = jest.fn(() => 1);
337 | container.bindFactory(NUMBER, factory, {scope: 'transient'});
338 | container.remove(NUMBER);
339 |
340 | expect(container.get(NUMBER)).toBeUndefined();
341 | expect(factory).toHaveBeenCalledTimes(0);
342 | });
343 |
344 | it('should remove "transient" factory in case its value has been resolved', () => {
345 | const container = createContainer();
346 |
347 | const factory = jest.fn(() => 1);
348 | container.bindFactory(NUMBER, factory, {scope: 'transient'});
349 | expect(container.get(NUMBER)).toBe(1);
350 |
351 | container.remove(NUMBER);
352 | expect(container.get(NUMBER)).toBeUndefined();
353 | expect(factory).toHaveBeenCalledTimes(1);
354 | });
355 |
356 | it('should not remove CONTAINER and PARENT_CONTAINER tokens', () => {
357 | const parent = createContainer();
358 | const container = createContainer(parent);
359 |
360 | container.remove(CONTAINER);
361 | container.remove(PARENT_CONTAINER);
362 |
363 | expect(container.get(CONTAINER)).toBe(container);
364 | expect(container.get(PARENT_CONTAINER)).toBe(parent);
365 | });
366 | });
367 |
368 | describe('removeAll()', () => {
369 | it('should remove all values and factories', () => {
370 | const container = createContainer();
371 |
372 | container.bindValue(NUMBER, 1);
373 | container.bindFactory(STRING, () => '2');
374 | expect(container.get(NUMBER)).toBe(1);
375 | expect(container.get(STRING)).toBe('2');
376 |
377 | container.removeAll();
378 | expect(container.get(NUMBER)).toBeUndefined();
379 | expect(container.get(STRING)).toBeUndefined();
380 | });
381 |
382 | it('should call "onRemoved" callbacks for factories with resolved singleton values', () => {
383 | const F1 = token('f1');
384 | const F2 = token('f2');
385 |
386 | const unbind1 = jest.fn();
387 | const unbind2 = jest.fn();
388 |
389 | const container = createContainer();
390 | container.bindFactory(F1, () => 10, {
391 | scope: 'singleton',
392 | onRemoved: unbind1,
393 | });
394 | container.bindFactory(F2, () => 20, {
395 | scope: 'singleton',
396 | onRemoved: unbind2,
397 | });
398 |
399 | expect(container.get(F2)).toBe(20);
400 |
401 | container.removeAll();
402 | expect(container.get(F1)).toBeUndefined();
403 | expect(container.get(F2)).toBeUndefined();
404 |
405 | expect(unbind1).toHaveBeenCalledTimes(0);
406 | expect(unbind2).toHaveBeenCalledTimes(1);
407 | expect(unbind2).toHaveBeenCalledWith(20);
408 | });
409 |
410 | it('should remain CONTAINER and PARENT_CONTAINER tokens', () => {
411 | const parent = createContainer();
412 | const container = createContainer(parent);
413 |
414 | container.removeAll();
415 |
416 | expect(container.get(CONTAINER)).toBe(container);
417 | expect(container.get(PARENT_CONTAINER)).toBe(parent);
418 | });
419 | });
420 |
421 | describe('hasToken()', () => {
422 | it('should check if a container hierarchy has the token', () => {
423 | const factory = jest.fn();
424 |
425 | const token1 = token();
426 | const token2 = token();
427 | const token3 = token();
428 |
429 | const parent = createContainer();
430 | parent.bindValue(token1, 1);
431 | parent.bindFactory(token2, factory);
432 |
433 | expect(parent.hasToken(token1)).toBe(true);
434 | expect(parent.hasToken(token2)).toBe(true);
435 | expect(parent.hasToken(token3)).toBe(false);
436 |
437 | const child = createContainer(parent);
438 | expect(child.hasToken(token1)).toBe(true);
439 | expect(child.hasToken(token2)).toBe(true);
440 | expect(child.hasToken(token3)).toBe(false);
441 |
442 | child.bindValue(token3, 2);
443 | expect(parent.hasToken(token3)).toBe(false);
444 | expect(child.hasToken(token3)).toBe(true);
445 |
446 | expect(factory).toHaveBeenCalledTimes(0);
447 | });
448 | });
449 |
450 | describe('get()', () => {
451 | it('should return a provided value', () => {
452 | const container = createContainer();
453 | container.bindValue(NUMBER, 1);
454 | expect(container.get(NUMBER)).toBe(1);
455 | });
456 |
457 | it('should return "undefined" in case a value is not provided', () => {
458 | const container = createContainer();
459 | expect(container.get(NUMBER)).toBeUndefined();
460 | });
461 |
462 | it('should return a value from the parent container', () => {
463 | const parent = createContainer();
464 | const container = createContainer(parent);
465 | parent.bindValue(NUMBER, 1);
466 | expect(container.get(NUMBER)).toBe(1);
467 | });
468 |
469 | it('should return "undefined" in case the parent container does not provide a value', () => {
470 | const parent = createContainer();
471 | const container = createContainer(parent);
472 | expect(container.get(NUMBER)).toBeUndefined();
473 | });
474 |
475 | it('should return a value from the container in case it overrides the parent container', () => {
476 | const parent = createContainer();
477 | parent.bindValue(NUMBER, 1);
478 |
479 | const container = createContainer(parent);
480 | container.bindValue(NUMBER, 2);
481 |
482 | expect(container.get(NUMBER)).toBe(2);
483 | });
484 |
485 | it('should return a value from the parent container in case the value was unbound from the current one', () => {
486 | const parent = createContainer();
487 | parent.bindValue(NUMBER, 1);
488 |
489 | const container = createContainer(parent);
490 | container.bindValue(NUMBER, 2);
491 | container.remove(NUMBER);
492 |
493 | expect(container.get(NUMBER)).toBe(1);
494 | });
495 |
496 | it('should return the container for CONTAINER token', () => {
497 | const container = createContainer();
498 | const result = container.get(CONTAINER);
499 | expect(result).toBe(container);
500 | });
501 |
502 | it('should return the parent container for PARENT_CONTAINER token', () => {
503 | const parent = createContainer();
504 | const container = createContainer(parent);
505 | const result = container.get(PARENT_CONTAINER);
506 | expect(result).toBe(parent);
507 | });
508 |
509 | it('should return "undefined" for PARENT_CONTAINER token in case there is no parent container', () => {
510 | const container = createContainer();
511 | const result = container.get(PARENT_CONTAINER);
512 | expect(result).toBeUndefined();
513 | });
514 |
515 | it('should return optional value in case optional token is not provided', () => {
516 | const parent = createContainer();
517 | const container = createContainer(parent);
518 |
519 | const optionalNumber = optional(NUMBER, 1);
520 |
521 | expect(parent.get(optionalNumber)).toBe(1);
522 | expect(container.get(optionalNumber)).toBe(1);
523 | });
524 | });
525 |
526 | describe('resolve()', () => {
527 | it('should resolve a provided value', () => {
528 | const container = createContainer();
529 | container.bindValue(NUMBER, 1);
530 | expect(container.resolve(NUMBER)).toBe(1);
531 | });
532 |
533 | it('should throw ResolverError in case a value is not provided', () => {
534 | const container = createContainer();
535 | expect(() => container.resolve(token('test'))).toThrowError(
536 | new ResolverError(`Token "test" is not provided`),
537 | );
538 | });
539 |
540 | it('should throw ResolverError with an empty name in case a token is not resolved', () => {
541 | const container = createContainer();
542 | expect(() => container.resolve(token())).toThrowError(
543 | new ResolverError(`Token "" is not provided`),
544 | );
545 | });
546 |
547 | it('should resolve a value from the parent container', () => {
548 | const parent = createContainer();
549 | const container = createContainer(parent);
550 | parent.bindValue(NUMBER, 1);
551 | expect(container.resolve(NUMBER)).toBe(1);
552 | });
553 |
554 | it('should resolve a value from the parent container by OptionalToken', () => {
555 | const OPTIONAL_TOKEN = optional(token(), 0);
556 |
557 | const parent = createContainer();
558 | parent.bindValue(OPTIONAL_TOKEN, 1);
559 |
560 | const container = createContainer(parent);
561 | expect(container.resolve(OPTIONAL_TOKEN)).toBe(1);
562 | });
563 |
564 | it('should throw ResolverError in case the parent container does not provide a value', () => {
565 | const parent = createContainer();
566 | const container = createContainer(parent);
567 | expect(() => container.resolve(NUMBER)).toThrowError(ResolverError);
568 | });
569 |
570 | it('should resolve a value from the container in case it overrides the parent container', () => {
571 | const parent = createContainer();
572 | parent.bindValue(NUMBER, 1);
573 |
574 | const container = createContainer(parent);
575 | container.bindValue(NUMBER, 2);
576 |
577 | expect(container.resolve(NUMBER)).toBe(2);
578 | });
579 |
580 | it('should resolve a value from the parent container in case the value was unbound from the current one', () => {
581 | const parent = createContainer();
582 | parent.bindValue(NUMBER, 1);
583 |
584 | const container = createContainer(parent);
585 | container.bindValue(NUMBER, 2);
586 | container.remove(NUMBER);
587 |
588 | expect(container.resolve(NUMBER)).toBe(1);
589 | });
590 |
591 | it('should resolve CONTAINER token as the container', () => {
592 | const container = createContainer();
593 | const result = container.resolve(CONTAINER);
594 | expect(result).toBe(container);
595 | });
596 |
597 | it('should resolve PARENT_CONTAINER as the parent container', () => {
598 | const parent = createContainer();
599 | const container = createContainer(parent);
600 | const result = container.get(PARENT_CONTAINER);
601 | expect(result).toBe(parent);
602 | });
603 |
604 | it('should throw ResolverError for PARENT_CONTAINER token in case there is no parent container', () => {
605 | const container = createContainer();
606 | expect(() => container.resolve(PARENT_CONTAINER)).toThrowError(
607 | new ResolverError(
608 | `Token "${PARENT_CONTAINER.symbol.description}" is not provided`,
609 | ),
610 | );
611 | });
612 |
613 | it('should resolve an optional value in case the optional token is not provided', () => {
614 | const parent = createContainer();
615 | const container = createContainer(parent);
616 |
617 | const optionalNumber = optional(NUMBER, 1);
618 |
619 | expect(parent.resolve(optionalNumber)).toBe(1);
620 | expect(container.resolve(optionalNumber)).toBe(1);
621 | });
622 |
623 | it('should resolve a value by shared tokens', () => {
624 | const key = 'token-' + Date.now();
625 | const t1 = token({key});
626 | const t2 = token({key});
627 | expect(t1).not.toBe(t2);
628 |
629 | const container = createContainer();
630 | container.bindValue(t1, 1);
631 |
632 | expect(container.resolve(t1)).toBe(1);
633 | expect(container.resolve(t2)).toBe(1);
634 | });
635 | });
636 | });
637 |
--------------------------------------------------------------------------------
/packages/ditox/src/container.ts:
--------------------------------------------------------------------------------
1 | import {Token, token} from './tokens';
2 |
3 | /**
4 | * ResolverError is thrown by the resolver when a token is not found in a container.
5 | */
6 | export class ResolverError extends Error {
7 | constructor(message: string) {
8 | super(message);
9 | Object.setPrototypeOf(this, new.target.prototype);
10 | this.name = 'ResolverError';
11 | }
12 | }
13 |
14 | /**
15 | * @see https://github.com/mnasyrov/ditox#factory-lifetimes
16 | */
17 | export type FactoryScope = 'scoped' | 'singleton' | 'transient';
18 |
19 | /**
20 | * Options for factory binding.
21 | *
22 | * `scope` types:
23 | * - `singleton` - **This is the default**. The value is created and cached by the most distant parent container which owns the factory function.
24 | * - `scoped` - The value is created and cached by the nearest container which owns the factory function.
25 | * - `transient` - The value is created every time it is resolved.
26 | *
27 | * `scoped` and `singleton` scopes can have `onRemoved` callback. It is called when a token is removed from the container.
28 | */
29 | export type FactoryOptions =
30 | | {
31 | scope?: 'scoped' | 'singleton';
32 | onRemoved?: (value: T) => void;
33 | }
34 | | {
35 | scope: 'transient';
36 | };
37 |
38 | /**
39 | * Dependency container.
40 | */
41 | export type Container = {
42 | /**
43 | * Binds a value for the token
44 | */
45 | bindValue(token: Token, value: T): void;
46 |
47 | /**
48 | * Binds a factory for the token.
49 | */
50 | bindFactory(
51 | token: Token,
52 | factory: (container: Container) => T,
53 | options?: FactoryOptions,
54 | ): void;
55 |
56 | /**
57 | * Checks if the token is registered in the container hierarchy.
58 | */
59 | hasToken(token: Token): boolean;
60 |
61 | /**
62 | * Returns a resolved value by the token, or returns `undefined` in case the token is not found.
63 | */
64 | get(token: Token): T | undefined;
65 |
66 | /**
67 | * Returns a resolved value by the token, or throws `ResolverError` in case the token is not found.
68 | */
69 | resolve(token: Token): T;
70 |
71 | /**
72 | * Removes a binding for the token.
73 | */
74 | remove(token: Token): void;
75 |
76 | /**
77 | * Removes all bindings in the container.
78 | */
79 | removeAll(): void;
80 | };
81 |
82 | /** @internal */
83 | export const CONTAINER: Token = token('ditox.Container');
84 | /** @internal */
85 | export const PARENT_CONTAINER: Token = token(
86 | 'ditox.ParentContainer',
87 | );
88 | /** @internal */
89 | export const RESOLVER: Token = token('ditox.Resolver');
90 |
91 | /** @internal */
92 | const NOT_FOUND = Symbol();
93 |
94 | /** @internal */
95 | const DEFAULT_SCOPE: FactoryScope = 'singleton';
96 |
97 | /** @internal */
98 | type FactoryContext = {
99 | factory: (container: Container) => T;
100 | options?: FactoryOptions;
101 | };
102 |
103 | /** @internal */
104 | type ValuesMap = Map;
105 |
106 | /** @internal */
107 | export type FactoriesMap = Map>;
108 |
109 | /** @internal */
110 | export const FACTORIES_MAP: Token = token('ditox.FactoriesMap');
111 |
112 | /** @internal */
113 | type Resolver = (token: Token, origin: Container) => T | typeof NOT_FOUND;
114 |
115 | /** @internal */
116 | function getScope(options?: FactoryOptions): FactoryScope {
117 | return options?.scope ?? DEFAULT_SCOPE;
118 | }
119 |
120 | /** @internal */
121 | function getOnRemoved(options: FactoryOptions) {
122 | return options.scope === undefined ||
123 | options.scope === 'scoped' ||
124 | options.scope === 'singleton'
125 | ? options.onRemoved
126 | : undefined;
127 | }
128 |
129 | /** @internal */
130 | function isInternalToken(token: Token): boolean {
131 | return (
132 | token.symbol === CONTAINER.symbol ||
133 | token.symbol === PARENT_CONTAINER.symbol ||
134 | token.symbol === RESOLVER.symbol
135 | );
136 | }
137 |
138 | /**
139 | * Creates a new dependency container.
140 | *
141 | * Container can have an optional parent to chain token resolution. The parent is used in case the current container does not have a registered token.
142 | *
143 | * @param parentContainer - Optional parent container.
144 | */
145 | export function createContainer(parentContainer?: Container): Container {
146 | const values: ValuesMap = new Map();
147 | const factories: FactoriesMap = new Map>();
148 |
149 | const container: Container = {
150 | bindValue(token: Token, value: T): void {
151 | if (isInternalToken(token)) {
152 | return;
153 | }
154 |
155 | values.set(token.symbol, value);
156 | },
157 |
158 | bindFactory(
159 | token: Token,
160 | factory: (container: Container) => T,
161 | options?: FactoryOptions,
162 | ): void {
163 | if (isInternalToken(token)) {
164 | return;
165 | }
166 |
167 | factories.set(token.symbol, {factory, options});
168 | },
169 |
170 | remove(token: Token): void {
171 | if (isInternalToken(token)) {
172 | return;
173 | }
174 |
175 | const options = factories.get(token.symbol)?.options;
176 | if (options) {
177 | executeOnRemoved(token.symbol, options);
178 | }
179 |
180 | values.delete(token.symbol);
181 | factories.delete(token.symbol);
182 | },
183 |
184 | removeAll(): void {
185 | factories.forEach((context, tokenSymbol) => {
186 | if (context.options) {
187 | executeOnRemoved(tokenSymbol, context.options);
188 | }
189 | });
190 |
191 | values.clear();
192 | factories.clear();
193 | bindInternalTokens();
194 | },
195 |
196 | hasToken(token: Token): boolean {
197 | return (
198 | values.has(token.symbol) ||
199 | factories.has(token.symbol) ||
200 | (parentContainer?.hasToken(token) ?? false)
201 | );
202 | },
203 |
204 | get(token: Token): T | undefined {
205 | const value = resolver(token, container);
206 | if (value !== NOT_FOUND) {
207 | return value;
208 | }
209 |
210 | if (token.isOptional) {
211 | return token.optionalValue;
212 | }
213 |
214 | return undefined;
215 | },
216 |
217 | resolve(token: Token): T {
218 | const value = resolver(token, container);
219 | if (value !== NOT_FOUND) {
220 | return value;
221 | }
222 |
223 | if (token.isOptional) {
224 | return token.optionalValue;
225 | }
226 |
227 | throw new ResolverError(
228 | `Token "${token.symbol.description ?? ''}" is not provided`,
229 | );
230 | },
231 | };
232 |
233 | function resolver(
234 | token: Token,
235 | origin: Container,
236 | ): T | typeof NOT_FOUND {
237 | const value = values.get(token.symbol);
238 | const hasValue = value !== undefined || values.has(token.symbol);
239 |
240 | if (hasValue && origin === container) {
241 | return value;
242 | }
243 |
244 | const factoryContext = factories.get(token.symbol);
245 | if (factoryContext) {
246 | const scope = getScope(factoryContext.options);
247 |
248 | switch (scope) {
249 | case 'singleton': {
250 | if (hasValue) {
251 | return value;
252 | } else if (parentContainer?.hasToken(token)) {
253 | break;
254 | } else {
255 | // Cache the value in the same container where the factory is registered.
256 | const value = factoryContext.factory(container);
257 | container.bindValue(token, value);
258 | return value;
259 | }
260 | }
261 |
262 | case 'scoped': {
263 | if (hasValue) {
264 | return value;
265 | } else {
266 | // Create a value within the factory's container and cache it.
267 | const value = factoryContext.factory(container);
268 | container.bindValue(token, value);
269 | return value;
270 | }
271 | }
272 |
273 | case 'transient': {
274 | // Create a value within the origin container and don't cache it.
275 | return factoryContext.factory(origin);
276 | }
277 | }
278 | }
279 |
280 | if (hasValue) {
281 | return value;
282 | }
283 |
284 | const parentResolver = parentContainer?.get(RESOLVER);
285 | if (parentResolver) {
286 | return parentResolver(token, origin);
287 | }
288 |
289 | return NOT_FOUND;
290 | }
291 |
292 | function executeOnRemoved(
293 | tokenSymbol: symbol,
294 | options: FactoryOptions,
295 | ) {
296 | const onRemoved = getOnRemoved(options);
297 | if (onRemoved) {
298 | const value = values.get(tokenSymbol);
299 | if (value !== undefined || values.has(tokenSymbol)) {
300 | onRemoved(value);
301 | }
302 | }
303 | }
304 |
305 | function bindInternalTokens() {
306 | values.set(CONTAINER.symbol, container);
307 | values.set(RESOLVER.symbol, resolver);
308 | values.set(FACTORIES_MAP.symbol, factories);
309 |
310 | if (parentContainer) {
311 | values.set(PARENT_CONTAINER.symbol, parentContainer);
312 | }
313 | }
314 |
315 | bindInternalTokens();
316 | return container;
317 | }
318 |
--------------------------------------------------------------------------------
/packages/ditox/src/index.ts:
--------------------------------------------------------------------------------
1 | export {token, optional} from './tokens';
2 | export type {RequiredToken, OptionalToken, Token} from './tokens';
3 |
4 | export {ResolverError, createContainer} from './container';
5 | export type {FactoryScope, FactoryOptions, Container} from './container';
6 |
7 | export {
8 | isToken,
9 | bindMultiValue,
10 | tryResolveValue,
11 | tryResolveValues,
12 | resolveValue,
13 | resolveValues,
14 | injectable,
15 | injectableClass,
16 | } from './utils';
17 |
18 | export {
19 | bindModule,
20 | bindModules,
21 | declareModule,
22 | declareModuleBindings,
23 | } from './modules';
24 | export type {
25 | Module,
26 | ModuleDeclaration,
27 | BindModuleOptions,
28 | ModuleBindingEntry,
29 | } from './modules';
30 |
--------------------------------------------------------------------------------
/packages/ditox/src/modules.test.ts:
--------------------------------------------------------------------------------
1 | import {createContainer} from './container';
2 | import {
3 | bindModule,
4 | bindModules,
5 | declareModule,
6 | declareModuleBindings,
7 | Module,
8 | ModuleDeclaration,
9 | } from './modules';
10 | import {token} from './tokens';
11 | import {injectable} from './utils';
12 |
13 | describe('bindModule()', () => {
14 | type TestQueries = {getValue: () => number};
15 | type TestModule = Module<{queries: TestQueries}>;
16 |
17 | const MODULE_TOKEN = token();
18 | const QUERIES_TOKEN = token();
19 |
20 | const MODULE: ModuleDeclaration = {
21 | token: MODULE_TOKEN,
22 | factory: () => ({
23 | queries: {
24 | getValue: () => 1,
25 | },
26 | }),
27 | exports: {
28 | queries: QUERIES_TOKEN,
29 | },
30 | };
31 |
32 | it('should bind a module and its provided values', () => {
33 | const container = createContainer();
34 | bindModule(container, MODULE);
35 |
36 | const queries = container.resolve(QUERIES_TOKEN);
37 | const value = queries.getValue();
38 | expect(value).toBe(1);
39 | });
40 |
41 | it('should destroy the module on removing the module token and remove tokens of its exported props', () => {
42 | const destroy = jest.fn();
43 | const container = createContainer();
44 |
45 | bindModule(container, {
46 | ...MODULE,
47 | factory: (container) => ({
48 | ...MODULE.factory(container),
49 | destroy,
50 | }),
51 | });
52 |
53 | container.resolve(MODULE_TOKEN);
54 | container.resolve(QUERIES_TOKEN);
55 |
56 | container.remove(MODULE_TOKEN);
57 | expect(destroy).toBeCalledTimes(1);
58 |
59 | expect(container.hasToken(MODULE_TOKEN)).toBe(false);
60 | expect(container.hasToken(QUERIES_TOKEN)).toBe(false);
61 | });
62 |
63 | it('should call beforeBinding() and afterBinding() during binding the module', () => {
64 | const container = createContainer();
65 | const token1 = token();
66 | const token2 = token();
67 |
68 | const beforeBinding = jest.fn((container) =>
69 | container.bindValue(token1, 'foo'),
70 | );
71 | const afterBinding = jest.fn((container) => {
72 | const value = container.resolve(QUERIES_TOKEN).getValue();
73 | container.bindValue(token2, value * 10);
74 | });
75 |
76 | bindModule(container, {
77 | ...MODULE,
78 | beforeBinding,
79 | afterBinding,
80 | });
81 |
82 | expect(beforeBinding).toBeCalledTimes(1);
83 | expect(afterBinding).toBeCalledTimes(1);
84 |
85 | expect(container.resolve(token1)).toBe('foo');
86 | expect(container.resolve(token2)).toBe(10);
87 | });
88 |
89 | it('should bind the module as singleton by default', () => {
90 | const parent = createContainer();
91 | const container = createContainer(parent);
92 |
93 | bindModule(parent, MODULE);
94 | expect(parent.get(MODULE_TOKEN)).toBe(container.get(MODULE_TOKEN));
95 | });
96 |
97 | it('should bind a "singleton" module to a container', () => {
98 | const parent = createContainer();
99 | const container = createContainer(parent);
100 |
101 | bindModule(parent, MODULE, {scope: 'singleton'});
102 | expect(parent.get(MODULE_TOKEN)).toBe(container.get(MODULE_TOKEN));
103 | });
104 |
105 | it('should bind a "scoped" module to a container', () => {
106 | const parent = createContainer();
107 | const container = createContainer(parent);
108 |
109 | bindModule(parent, MODULE, {scope: 'scoped'});
110 | expect(parent.get(MODULE_TOKEN)).toBe(container.get(MODULE_TOKEN));
111 | expect(parent.get(QUERIES_TOKEN)).toBe(container.get(QUERIES_TOKEN));
112 | });
113 |
114 | it('should remove "singleton" module when a container is cleaning', () => {
115 | const parent = createContainer();
116 | const container = createContainer(parent);
117 |
118 | const destroy = jest.fn();
119 | bindModule(parent, {
120 | token: MODULE_TOKEN,
121 | factory: (container) => ({...MODULE.factory(container), destroy}),
122 | });
123 |
124 | parent.resolve(MODULE_TOKEN);
125 | parent.removeAll();
126 | expect(destroy).toBeCalledTimes(1);
127 |
128 | destroy.mockClear();
129 | container.removeAll();
130 | expect(destroy).toBeCalledTimes(0);
131 | });
132 |
133 | it('should remove "scoped" module when a container is cleaning', () => {
134 | const parent = createContainer();
135 | const container = createContainer(parent);
136 |
137 | const destroy = jest.fn();
138 | bindModule(
139 | parent,
140 | {
141 | token: MODULE_TOKEN,
142 | factory: (container) => ({...MODULE.factory(container), destroy}),
143 | },
144 | {
145 | scope: 'scoped',
146 | },
147 | );
148 |
149 | parent.resolve(MODULE_TOKEN);
150 | container.resolve(MODULE_TOKEN);
151 |
152 | destroy.mockClear();
153 | container.removeAll();
154 | expect(destroy).toBeCalledTimes(0);
155 |
156 | destroy.mockClear();
157 | parent.removeAll();
158 | expect(destroy).toBeCalledTimes(1);
159 | });
160 |
161 | it('should bind modules and binding entries from "imports" to the container', () => {
162 | type TestModule = Module<{value: number}>;
163 |
164 | const MODULE1_TOKEN = token();
165 | const MODULE1: ModuleDeclaration = {
166 | token: MODULE1_TOKEN,
167 | factory: () => ({value: 1}),
168 | };
169 |
170 | const MODULE2_TOKEN = token();
171 | const MODULE2: ModuleDeclaration = {
172 | token: MODULE2_TOKEN,
173 | factory: () => ({value: 2}),
174 | };
175 |
176 | const MODULE2_ALTERED: ModuleDeclaration = {
177 | token: MODULE2_TOKEN,
178 | factory: () => ({value: 22}),
179 | };
180 |
181 | const parent = createContainer();
182 | bindModule(parent, MODULE2);
183 |
184 | const container = createContainer(parent);
185 | bindModule(
186 | container,
187 | declareModule({
188 | factory: () => ({}),
189 | imports: [
190 | MODULE1,
191 | {module: MODULE2_ALTERED, options: {scope: 'scoped'}},
192 | ],
193 | }),
194 | );
195 |
196 | expect(parent.get(MODULE1_TOKEN)).toBeUndefined();
197 | expect(parent.get(MODULE2_TOKEN)?.value).toBe(2);
198 |
199 | expect(container.get(MODULE1_TOKEN)?.value).toBe(1);
200 | expect(container.get(MODULE2_TOKEN)?.value).toBe(22);
201 | });
202 |
203 | it('should call beforeBinding() before importing modules from "imports"', () => {
204 | const ARG_TOKEN = token('arg');
205 | const RESULT_TOKEN = token('result');
206 |
207 | type TestModule = Module<{value: string}>;
208 |
209 | const MODULE1: ModuleDeclaration = declareModule({
210 | beforeBinding: (container) =>
211 | container.bindValue(ARG_TOKEN, container.resolve(ARG_TOKEN) + '2'),
212 | factory: injectable((arg) => ({value: arg + '3'}), ARG_TOKEN),
213 | exports: {value: RESULT_TOKEN},
214 | });
215 |
216 | const container = createContainer();
217 | bindModule(
218 | container,
219 | declareModule({
220 | factory: () => ({}),
221 | beforeBinding: (container) => {
222 | container.bindValue(ARG_TOKEN, '1');
223 | },
224 | imports: [MODULE1],
225 | }),
226 | );
227 |
228 | expect(container.resolve(ARG_TOKEN)).toBe('12');
229 | expect(container.resolve(RESULT_TOKEN)).toBe('123');
230 | });
231 |
232 | it('should call beforeBinding() and afterBinding() in deep-first order', () => {
233 | const route: string[] = [];
234 |
235 | const declareTestModule = (
236 | id: string,
237 | imports?: ModuleDeclaration>[],
238 | ) =>
239 | declareModule({
240 | imports,
241 | beforeBinding: () => route.push(`${id}:before`),
242 | factory: (container) => {
243 | imports?.forEach((m) => container.resolve(m.token));
244 | route.push(`${id}:factory`);
245 | return {};
246 | },
247 | afterBinding: () => route.push(`${id}:after`),
248 | });
249 |
250 | /**
251 | * m1
252 | * / | \
253 | * m2 m3 m4
254 | * / \ \ |
255 | * m5 m6 m7
256 | */
257 | const m7 = declareTestModule('m7');
258 | const m6 = declareTestModule('m6');
259 | const m5 = declareTestModule('m5');
260 | const m4 = declareTestModule('m4', [m7]);
261 | const m3 = declareTestModule('m3', [m7]);
262 | const m2 = declareTestModule('m2', [m5, m6]);
263 | const m1 = declareTestModule('m1', [m2, m3, m4]);
264 |
265 | const container = createContainer();
266 | bindModule(container, m1);
267 | container.resolve(m1.token);
268 |
269 | expect(route).toEqual([
270 | 'm1:before',
271 | 'm2:before',
272 | 'm3:before',
273 | 'm4:before',
274 | 'm5:before',
275 | 'm6:before',
276 | 'm7:before',
277 | 'm7:after',
278 | 'm6:after',
279 | 'm5:after',
280 | 'm4:after',
281 | 'm3:after',
282 | 'm2:after',
283 | 'm1:after',
284 | 'm5:factory',
285 | 'm6:factory',
286 | 'm2:factory',
287 | 'm7:factory',
288 | 'm3:factory',
289 | 'm4:factory',
290 | 'm1:factory',
291 | ]);
292 | });
293 | });
294 |
295 | describe('bindModules()', () => {
296 | type TestModule = Module<{value: number}>;
297 |
298 | const MODULE1_TOKEN = token();
299 | const MODULE1: ModuleDeclaration = {
300 | token: MODULE1_TOKEN,
301 | factory: () => ({value: 1}),
302 | };
303 |
304 | const MODULE2_TOKEN = token();
305 | const MODULE2: ModuleDeclaration = {
306 | token: MODULE2_TOKEN,
307 | factory: () => ({value: 2}),
308 | };
309 |
310 | const MODULE2_ALTERED: ModuleDeclaration = {
311 | token: MODULE2_TOKEN,
312 | factory: () => ({value: 22}),
313 | };
314 |
315 | it('should bind modules and binding entries to the container', () => {
316 | const parent = createContainer();
317 | const container = createContainer(parent);
318 |
319 | bindModules(parent, [MODULE2]);
320 |
321 | bindModules(container, [
322 | MODULE1,
323 | {module: MODULE2_ALTERED, options: {scope: 'scoped'}},
324 | ]);
325 |
326 | expect(parent.get(MODULE1_TOKEN)).toBeUndefined();
327 | expect(parent.get(MODULE2_TOKEN)?.value).toBe(2);
328 |
329 | expect(container.get(MODULE1_TOKEN)?.value).toBe(1);
330 | expect(container.get(MODULE2_TOKEN)?.value).toBe(22);
331 | });
332 | });
333 |
334 | describe('declareModule()', () => {
335 | it('should declare a binding for the module', () => {
336 | const container = createContainer();
337 |
338 | const VALUE_TOKEN = token();
339 | const MODULE_TOKEN = token>();
340 | const MODULE = declareModule({
341 | token: MODULE_TOKEN,
342 | factory: () => ({value: 1}),
343 | exports: {value: VALUE_TOKEN},
344 | });
345 |
346 | bindModule(container, MODULE);
347 | expect(container.get(VALUE_TOKEN)).toBe(1);
348 | expect(container.get(MODULE_TOKEN)).toEqual({value: 1});
349 | });
350 |
351 | it('should declare a binding for the anonymous module', () => {
352 | const container = createContainer();
353 |
354 | const VALUE_TOKEN = token();
355 | const MODULE = declareModule({
356 | factory: () => ({value: 1}),
357 | exports: {value: VALUE_TOKEN},
358 | });
359 |
360 | bindModule(container, MODULE);
361 | expect(container.get(VALUE_TOKEN)).toBe(1);
362 | });
363 | });
364 |
365 | describe('declareModuleBindings()', () => {
366 | it('should declare bindings for the modules', () => {
367 | const container = createContainer();
368 |
369 | const VALUE1_TOKEN = token();
370 | const MODULE1 = declareModule({
371 | factory: () => ({value: 1}),
372 | exports: {value: VALUE1_TOKEN},
373 | });
374 |
375 | const VALUE2_TOKEN = token();
376 | const MODULE2 = declareModule({
377 | factory: () => ({value: 2}),
378 | exports: {value: VALUE2_TOKEN},
379 | });
380 |
381 | const MODULE_BINDINGS = declareModuleBindings([MODULE1, MODULE2]);
382 |
383 | bindModule(container, MODULE_BINDINGS);
384 | expect(container.get(VALUE1_TOKEN)).toBe(1);
385 | expect(container.get(VALUE2_TOKEN)).toBe(2);
386 |
387 | expect(container.get(MODULE_BINDINGS.token)).toEqual({});
388 | });
389 | });
390 |
--------------------------------------------------------------------------------
/packages/ditox/src/modules.ts:
--------------------------------------------------------------------------------
1 | import {Container} from './container';
2 | import {Token, token} from './tokens';
3 | import {injectable} from './utils';
4 |
5 | type AnyObject = Record;
6 | type EmptyObject = Record;
7 |
8 | type ModuleController = {
9 | /** Dispose the module and clean its resources */
10 | destroy?: () => void;
11 | };
12 |
13 | /**
14 | * Dependency module
15 | *
16 | * @example
17 | * ```ts
18 | * type LoggerModule = Module<{
19 | * logger: Logger;
20 | * }>;
21 | * ```
22 | */
23 | export type Module =
24 | ModuleController & ModuleProps;
25 |
26 | type GetModuleProps = T extends Module ? Props : never;
27 |
28 | /**
29 | * Description how to bind the module in declarative way.
30 | *
31 | * @example
32 | * ```ts
33 | * const LOGGER_MODULE: ModuleDeclaration = {
34 | * token: LOGGER_MODULE_TOKEN,
35 | * factory: (container) => {
36 | * const transport = container.resolve(TRANSPORT_TOKEN).open();
37 | * return {
38 | * logger: { log: (message) => transport.write(message) },
39 | * destroy: () => transport.close(),
40 | * }
41 | * },
42 | * exports: {
43 | * logger: LOGGER_TOKEN,
44 | * },
45 | * };
46 | * ```
47 | */
48 | export type ModuleDeclaration> = {
49 | /** Token for the module */
50 | token: Token;
51 |
52 | /** Modules for binding */
53 | imports?: ReadonlyArray;
54 |
55 | /** Factory of the module */
56 | factory: (container: Container) => T;
57 |
58 | /** Dictionary of module properties which are bound to tokens. */
59 | exports?: {
60 | [K in keyof GetModuleProps]?: Token[K]>;
61 | };
62 |
63 | /** Callback could be used to prepare an environment. It is called before binding the module. */
64 | beforeBinding?: (container: Container) => void;
65 |
66 | /** Callback could be used to export complex dependencies from the module. It is called after binding the module. */
67 | afterBinding?: (container: Container) => void;
68 | };
69 |
70 | export type AnyModuleDeclaration = ModuleDeclaration>;
71 |
72 | /**
73 | * Options for module binding.
74 | *
75 | * `scope` types:
76 | * - `singleton` - **This is the default**. The value is created and cached by the most distant parent container which owns the factory function.
77 | * - `scoped` - The value is created and cached by the nearest container which owns the factory function.
78 | */
79 | export type BindModuleOptions = {
80 | scope?: 'scoped' | 'singleton';
81 | };
82 |
83 | type ModuleDeclarationWithOptions = {
84 | module: ModuleDeclaration;
85 | options: BindModuleOptions;
86 | };
87 |
88 | export type ModuleBindingEntry =
89 | | ModuleDeclaration
90 | | ModuleDeclarationWithOptions;
91 |
92 | /**
93 | * Binds the dependency module to the container
94 | * @param container - Dependency container.
95 | * @param moduleDeclaration - Declaration of the dependency module.
96 | * @param options - Options for module binding.
97 | *
98 | * @example
99 | * ```ts
100 | * bindModule(container, LOGGER_MODULE);
101 | * ```
102 | */
103 | export function bindModule>(
104 | container: Container,
105 | moduleDeclaration: ModuleDeclaration,
106 | options?: BindModuleOptions,
107 | ): void {
108 | const rootEntry: ModuleBindingEntry = {
109 | module: moduleDeclaration,
110 | options: options ?? {},
111 | };
112 |
113 | const bfsVisits = new Set([rootEntry]);
114 | const bfsQueue: ModuleBindingEntry[] = [rootEntry];
115 |
116 | let bfsIndex = 0;
117 | while (bfsIndex < bfsQueue.length) {
118 | const entry = bfsQueue[bfsIndex];
119 |
120 | const m = 'module' in entry ? entry.module : entry;
121 |
122 | m.imports?.forEach((depEntry) => {
123 | if (!bfsVisits.has(depEntry)) {
124 | bfsVisits.add(depEntry);
125 | bfsQueue.push(depEntry);
126 | }
127 | });
128 |
129 | bfsIndex++;
130 | }
131 |
132 | for (let i = 0; i < bfsQueue.length; i++) {
133 | const entry = bfsQueue[i];
134 | const m = 'module' in entry ? entry.module : entry;
135 | m.beforeBinding?.(container);
136 | }
137 |
138 | for (let i = 0; i < bfsQueue.length; i++) {
139 | const entry = bfsQueue[i];
140 | bindModuleEntry(container, entry);
141 | }
142 |
143 | for (let i = bfsQueue.length - 1; i >= 0; i--) {
144 | const entry = bfsQueue[i];
145 | const m = 'module' in entry ? entry.module : entry;
146 | m.afterBinding?.(container);
147 | }
148 | }
149 |
150 | function bindModuleEntry(
151 | container: Container,
152 | entry: ModuleBindingEntry,
153 | ): void {
154 | let module: ModuleDeclaration;
155 | let options: BindModuleOptions | undefined;
156 |
157 | if ('module' in entry) {
158 | module = entry.module;
159 | options = entry.options;
160 | } else {
161 | module = entry;
162 | }
163 |
164 | const scope = options?.scope;
165 | const exportedValueTokens = new Set>();
166 | const moduleExports = module.exports;
167 |
168 | if (moduleExports) {
169 | const keys = Object.keys(moduleExports);
170 |
171 | keys.forEach((valueKey) => {
172 | const valueToken = moduleExports[valueKey];
173 | if (valueToken) {
174 | exportedValueTokens.add(valueToken);
175 |
176 | container.bindFactory(
177 | valueToken,
178 | injectable((module) => module[valueKey], module.token),
179 | {scope},
180 | );
181 | }
182 | });
183 | }
184 |
185 | container.bindFactory(module.token, module.factory, {
186 | scope,
187 | onRemoved: (moduleInstance) => {
188 | if (moduleInstance.destroy) {
189 | moduleInstance.destroy();
190 | }
191 |
192 | exportedValueTokens.forEach((valueToken) => container.remove(valueToken));
193 | exportedValueTokens.clear();
194 | },
195 | });
196 | }
197 |
198 | /**
199 | * Binds dependency modules to the container
200 | *
201 | * @param container - Dependency container for binding
202 | * @param modules - Array of module binding entries: module declaration or `{module: ModuleDeclaration, options: BindModuleOptions}` objects.
203 | */
204 | export function bindModules(
205 | container: Container,
206 | modules: ReadonlyArray,
207 | ): void {
208 | modules.forEach((entry) => {
209 | if ('module' in entry) {
210 | bindModule(container, entry.module, entry.options);
211 | } else {
212 | bindModule(container, entry);
213 | }
214 | });
215 | }
216 |
217 | /**
218 | * Declares a module binding
219 | *
220 | * @param declaration - a module declaration
221 | * @param declaration.token - optional field
222 | *
223 | * @example
224 | * ```ts
225 | * const LOGGER_MODULE = declareModule({
226 | * factory: (container) => {
227 | * const transport = container.resolve(TRANSPORT_TOKEN).open();
228 | * return {
229 | * logger: { log: (message) => transport.write(message) },
230 | * destroy: () => transport.close(),
231 | * }
232 | * },
233 | * exports: {
234 | * logger: LOGGER_TOKEN,
235 | * },
236 | * });
237 | * ```
238 | */
239 | export function declareModule>(
240 | declaration: Omit, 'token'> &
241 | Partial, 'token'>>,
242 | ): ModuleDeclaration {
243 | return {...declaration, token: declaration.token ?? token()};
244 | }
245 |
246 | /**
247 | * Declares bindings of several modules
248 | *
249 | * @param modules - module declaration entries
250 | */
251 | export function declareModuleBindings(
252 | modules: ReadonlyArray,
253 | ): ModuleDeclaration {
254 | return declareModule({
255 | factory: () => ({}),
256 | imports: modules,
257 | });
258 | }
259 |
--------------------------------------------------------------------------------
/packages/ditox/src/tokens.test.ts:
--------------------------------------------------------------------------------
1 | import {optional, token} from './tokens';
2 |
3 | describe('token()', () => {
4 | it('should return a token with description it is specified', () => {
5 | expect(token().symbol.description).toBeUndefined();
6 | expect(token('text1').symbol.description).toBe('text1');
7 | expect(token({description: 'text2'}).symbol.description).toBe('text2');
8 | });
9 |
10 | it('should return independent tokens if key is not specified', () => {
11 | const t1 = token();
12 | const t2 = token();
13 |
14 | expect(t1).not.toBe(t2);
15 | expect(t1.symbol).not.toBe(t2.symbol);
16 | expect(t1.isOptional).not.toBeTruthy();
17 | });
18 |
19 | it('should return tokens with the same symbol if key is specified', () => {
20 | const source = token({key: 'test-token'});
21 | const clone = token({key: 'test-token'});
22 | const something = token({key: 'something-else'});
23 |
24 | expect(source).not.toBe(clone);
25 | expect(source.symbol).toBe(clone.symbol);
26 |
27 | expect(something).not.toBe(source);
28 | expect(something.symbol).not.toBe(source);
29 | });
30 | });
31 |
32 | describe('optional()', () => {
33 | it('should decorate a source token to attach an optional value', () => {
34 | const t1 = token();
35 | expect(t1.isOptional).not.toBeTruthy();
36 |
37 | const o1 = optional(t1);
38 | expect(o1.symbol).toBe(t1.symbol);
39 | expect(o1.isOptional).toBe(true);
40 |
41 | const o2 = optional(t1, -1);
42 | expect(o2.symbol).toBe(t1.symbol);
43 | expect(o2.isOptional).toBe(true);
44 | expect(o2.optionalValue).toBe(-1);
45 | });
46 |
47 | it('should reuse symbol from a source token ifits key is specified', () => {
48 | const source = token({key: 'token-key'});
49 | const clone = token({key: 'token-key'});
50 | const optClone = optional(clone);
51 |
52 | expect(optClone.symbol).toBe(source.symbol);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/packages/ditox/src/tokens.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @ignore
3 | * Binding token for mandatory value
4 | */
5 | export type RequiredToken = {
6 | symbol: symbol;
7 | type?: T; // Anchor for Typescript type inference.
8 | isOptional?: false;
9 | };
10 |
11 | /**
12 | * @ignore
13 | * Binding token for optional value
14 | */
15 | export type OptionalToken = {
16 | symbol: symbol;
17 | type?: T; // Anchor for Typescript type inference.
18 | isOptional: true;
19 | optionalValue: T;
20 | };
21 |
22 | /**
23 | * Binding token
24 | */
25 | export type Token = RequiredToken | OptionalToken;
26 |
27 | /**
28 | * Token options
29 | */
30 | export type TokenOptions =
31 | | {
32 | /**
33 | * Key for token's symbol. It allows to create shareable tokens.
34 | */
35 | key: string;
36 |
37 | /** @ignore */
38 | description?: undefined;
39 | }
40 | | {
41 | /** Description for better error messages */
42 | description?: string;
43 |
44 | /** @ignore */
45 | key?: undefined;
46 | };
47 |
48 | /**
49 | * Creates a new binding token.
50 | * @param description - Token description for better error messages.
51 | */
52 | export function token(description?: string): Token;
53 | /**
54 | * Creates a new binding token.
55 | * @param options - Token description for better error messages.
56 | */ export function token(options?: TokenOptions): Token;
57 | export function token(options?: TokenOptions | string): Token {
58 | const normalized: TokenOptions | undefined =
59 | typeof options === 'string' ? {description: options} : options;
60 |
61 | const symbol: symbol = normalized?.key
62 | ? Symbol.for(normalized.key)
63 | : Symbol(normalized?.description);
64 |
65 | return {symbol};
66 | }
67 |
68 | /**
69 | * Decorate a token with an optional value.
70 | * This value is be used as default value in case a container does not have registered token.
71 | * @param token - Existed token.
72 | * @param optionalValue - Default value for the resolver.
73 | */
74 | export function optional(
75 | token: Token,
76 | optionalValue: T,
77 | ): OptionalToken;
78 | export function optional(token: Token): OptionalToken;
79 | export function optional(
80 | token: Token,
81 | optionalValue?: T,
82 | ): OptionalToken {
83 | return {
84 | symbol: token.symbol,
85 | isOptional: true,
86 | optionalValue,
87 | };
88 | }
89 |
--------------------------------------------------------------------------------
/packages/ditox/src/utils.test.ts:
--------------------------------------------------------------------------------
1 | import {createContainer, ResolverError} from './container';
2 | import {
3 | bindMultiValue,
4 | injectable,
5 | injectableClass,
6 | resolveValue,
7 | resolveValues,
8 | tryResolveValue,
9 | tryResolveValues,
10 | } from './utils';
11 | import {optional, token} from './tokens';
12 |
13 | const NUMBER = token('number');
14 | const STRING = token('string');
15 |
16 | describe('bindMultiValue()', () => {
17 | const NUMBERS = token>('numbers');
18 |
19 | it('should append a value to an array declared by a token', () => {
20 | const container = createContainer();
21 |
22 | bindMultiValue(container, NUMBERS, 1);
23 | bindMultiValue(container, NUMBERS, 2);
24 |
25 | const values: Array = container.resolve(NUMBERS);
26 | expect(values).toEqual([1, 2]);
27 | });
28 |
29 | it('should append new values to an array declared by a token', () => {
30 | const container = createContainer();
31 |
32 | bindMultiValue(container, NUMBERS, 1);
33 | bindMultiValue(container, NUMBERS, 2);
34 | expect(container.resolve(NUMBERS)).toEqual([1, 2]);
35 |
36 | bindMultiValue(container, NUMBERS, 3);
37 | bindMultiValue(container, NUMBERS, 4);
38 | expect(container.resolve(NUMBERS)).toEqual([1, 2, 3, 4]);
39 | });
40 |
41 | it('should add new values to a copy of array from the parent container', () => {
42 | const parent = createContainer();
43 | parent.bindValue(NUMBERS, [1, 2]);
44 | const container = createContainer(parent);
45 |
46 | bindMultiValue(container, NUMBERS, 3);
47 | bindMultiValue(container, NUMBERS, 4);
48 | expect(parent.resolve(NUMBERS)).toEqual([1, 2]);
49 | expect(container.resolve(NUMBERS)).toEqual([1, 2, 3, 4]);
50 |
51 | container.remove(NUMBERS);
52 | expect(parent.resolve(NUMBERS)).toEqual([1, 2]);
53 | expect(container.resolve(NUMBERS)).toEqual([1, 2]);
54 | });
55 | });
56 |
57 | describe('tryResolveValue()', () => {
58 | it('should return an object with values by the tokens', () => {
59 | const container = createContainer();
60 | container.bindValue(NUMBER, 1);
61 | container.bindValue(STRING, 'abc');
62 |
63 | const props: {a: number; b: string} = tryResolveValue(container, {
64 | a: NUMBER,
65 | b: STRING,
66 | });
67 | expect(props).toEqual({a: 1, b: 'abc'});
68 | });
69 |
70 | it('should return "undefined" item in case a value is not provided', () => {
71 | const container = createContainer();
72 | container.bindValue(NUMBER, 1);
73 |
74 | const props = tryResolveValue(container, {a: NUMBER, b: STRING});
75 | expect(props).toEqual({a: 1, b: undefined});
76 | });
77 |
78 | it('should return values of optional tokens in case they are not provided', () => {
79 | const container = createContainer();
80 | container.bindValue(NUMBER, 1);
81 |
82 | const OPTIONAL_STRING = optional(STRING, 'value');
83 |
84 | const props = tryResolveValue(container, {
85 | a: NUMBER,
86 | b: STRING,
87 | c: OPTIONAL_STRING,
88 | });
89 | expect(props).toEqual({a: 1, b: undefined, c: 'value'});
90 | });
91 | });
92 |
93 | describe('tryResolveValues()', () => {
94 | it('should return values for the tokens', () => {
95 | const container = createContainer();
96 | container.bindValue(NUMBER, 1);
97 | container.bindValue(STRING, 'abc');
98 |
99 | const values: [number, string] = tryResolveValues(
100 | container,
101 | NUMBER,
102 | STRING,
103 | );
104 | expect(values).toEqual([1, 'abc']);
105 | });
106 |
107 | it('should return "undefined" item in case a value is not provided', () => {
108 | const container = createContainer();
109 | container.bindValue(NUMBER, 1);
110 |
111 | const values = tryResolveValues(container, NUMBER, STRING);
112 | expect(values).toEqual([1, undefined]);
113 | });
114 |
115 | it('should return values of optional tokens in case they are not provided', () => {
116 | const container = createContainer();
117 | container.bindValue(NUMBER, 1);
118 |
119 | const OPTIONAL_STRING = optional(STRING, 'value');
120 |
121 | const values = tryResolveValues(container, NUMBER, STRING, OPTIONAL_STRING);
122 | expect(values).toEqual([1, undefined, 'value']);
123 | });
124 |
125 | it('should works with a big arity', () => {
126 | const container = createContainer();
127 | container.bindValue(NUMBER, 1);
128 |
129 | const values = tryResolveValues(
130 | container,
131 | NUMBER,
132 | NUMBER,
133 | NUMBER,
134 | NUMBER,
135 | NUMBER,
136 | NUMBER,
137 | NUMBER,
138 | NUMBER,
139 | NUMBER,
140 | NUMBER,
141 | );
142 | expect(values).toEqual([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
143 | });
144 | });
145 |
146 | describe('resolveValue()', () => {
147 | it('should an object with resolved values from the container for the specified token props', () => {
148 | const container = createContainer();
149 | container.bindValue(NUMBER, 1);
150 | container.bindValue(STRING, 'abc');
151 |
152 | const props: {a: number; b: string} = resolveValue(container, {
153 | a: NUMBER,
154 | b: STRING,
155 | });
156 | expect(props).toEqual({a: 1, b: 'abc'});
157 | });
158 |
159 | it('should throw an error in case a value is not provided', () => {
160 | const container = createContainer();
161 | container.bindValue(NUMBER, 1);
162 |
163 | expect(() => {
164 | resolveValue(container, {a: NUMBER, b: STRING});
165 | }).toThrowError(
166 | new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
167 | );
168 | });
169 |
170 | it('should resolve values of optional tokens in case they are not provided', () => {
171 | const container = createContainer();
172 | container.bindValue(NUMBER, 1);
173 |
174 | const props = resolveValue(container, {
175 | a: NUMBER,
176 | b: optional(STRING, 'value'),
177 | });
178 | expect(props).toEqual({a: 1, b: 'value'});
179 | });
180 | });
181 |
182 | describe('resolveValues()', () => {
183 | it('should resolve tokens from the container', () => {
184 | const container = createContainer();
185 | container.bindValue(NUMBER, 1);
186 | container.bindValue(STRING, 'abc');
187 |
188 | const values: [number, string] = resolveValues(container, NUMBER, STRING);
189 | expect(values).toEqual([1, 'abc']);
190 | });
191 |
192 | it('should throw an error in case a value is not provided', () => {
193 | const container = createContainer();
194 | container.bindValue(NUMBER, 1);
195 |
196 | expect(() => {
197 | resolveValues(container, NUMBER, STRING);
198 | }).toThrowError(
199 | new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
200 | );
201 | });
202 |
203 | it('should resolve values of optional tokens in case they are not provided', () => {
204 | const container = createContainer();
205 | container.bindValue(NUMBER, 1);
206 |
207 | const values = resolveValues(container, NUMBER, optional(STRING, 'value'));
208 | expect(values).toEqual([1, 'value']);
209 | });
210 |
211 | it('should resolve a dictionary of tokens', () => {
212 | const container = createContainer();
213 | container.bindValue(NUMBER, 1);
214 |
215 | const values = resolveValue(container, {
216 | a: NUMBER,
217 | b: optional(STRING, 'value'),
218 | });
219 | expect(values).toEqual({
220 | a: 1,
221 | b: 'value',
222 | });
223 | });
224 | });
225 |
226 | describe('injectable()', () => {
227 | function join({a, b}: {a: number; b: string}): string {
228 | return `${a}/${b}`;
229 | }
230 |
231 | it('should inject values from Container as arguments of the factory', () => {
232 | const container = createContainer();
233 | container.bindValue(NUMBER, 1);
234 | container.bindValue(STRING, '2');
235 |
236 | const decoratedFactory = injectable(
237 | (a: number, b: string) => a + b,
238 | NUMBER,
239 | STRING,
240 | );
241 |
242 | expect(decoratedFactory(container)).toBe('12');
243 | });
244 |
245 | it('should throw ResolverError error in case a value is not provided', () => {
246 | const container = createContainer();
247 | container.bindValue(NUMBER, 1);
248 |
249 | const decoratedFactory = injectable(
250 | (a: number, b: string) => a + b,
251 | NUMBER,
252 | STRING,
253 | );
254 |
255 | expect(() => decoratedFactory(container)).toThrowError(
256 | new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
257 | );
258 | });
259 |
260 | it('should inject values of optional tokens in case values are not provided', () => {
261 | const container = createContainer();
262 | container.bindValue(NUMBER, 1);
263 |
264 | const decoratedFactory = injectable(
265 | (a, b) => a + b,
266 | NUMBER,
267 | optional(STRING, 'value'),
268 | );
269 |
270 | expect(decoratedFactory(container)).toBe('1value');
271 | });
272 |
273 | it('should inject values from Container as an object for the first argument of the factory', () => {
274 | const container = createContainer();
275 | container.bindValue(NUMBER, 1);
276 | container.bindValue(STRING, '2');
277 |
278 | const decoratedFactory = injectable(join, {a: NUMBER, b: STRING});
279 |
280 | expect(decoratedFactory(container)).toBe('1/2');
281 | });
282 |
283 | it('should throw ResolverError error in case a value is not provided', () => {
284 | const container = createContainer();
285 | container.bindValue(NUMBER, 1);
286 |
287 | const decoratedFactory = injectable(join, {a: NUMBER, b: STRING});
288 |
289 | expect(() => decoratedFactory(container)).toThrowError(
290 | new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
291 | );
292 | });
293 |
294 | it('should inject values of optional tokens in case values are not provided', () => {
295 | const container = createContainer();
296 | container.bindValue(NUMBER, 1);
297 |
298 | const decoratedFactory = injectable(join, {
299 | a: NUMBER,
300 | b: optional(STRING, 'value'),
301 | });
302 |
303 | expect(decoratedFactory(container)).toBe('1/value');
304 | });
305 | });
306 |
307 | describe('injectableClass()', () => {
308 | class TestClass {
309 | a: number;
310 | b: string;
311 |
312 | constructor(a: number, b: string) {
313 | this.a = a;
314 | this.b = b;
315 | }
316 |
317 | concat(): string {
318 | return this.a + this.b;
319 | }
320 | }
321 |
322 | it('should inject values from Container as arguments of the constructor', () => {
323 | const container = createContainer();
324 | container.bindValue(NUMBER, 1);
325 | container.bindValue(STRING, '2');
326 |
327 | const factory = injectableClass(TestClass, NUMBER, STRING);
328 |
329 | expect(factory(container).concat()).toBe('12');
330 | });
331 |
332 | it('should throw ResolverError error in case a value is not provided', () => {
333 | const container = createContainer();
334 | container.bindValue(NUMBER, 1);
335 |
336 | const factory = injectableClass(TestClass, NUMBER, STRING);
337 |
338 | expect(() => factory(container)).toThrowError(
339 | new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
340 | );
341 | });
342 |
343 | it('should inject values of optional tokens in case values are not provided', () => {
344 | const container = createContainer();
345 | container.bindValue(NUMBER, 1);
346 |
347 | const factory = injectableClass(
348 | TestClass,
349 | NUMBER,
350 | optional(STRING, 'value'),
351 | );
352 |
353 | expect(factory(container).concat()).toBe('1value');
354 | });
355 | });
356 |
357 | describe('injectableClass()', () => {
358 | class TestClass {
359 | a: number;
360 | b: string;
361 |
362 | constructor(props: {a: number; b: string}) {
363 | this.a = props.a;
364 | this.b = props.b;
365 | }
366 |
367 | join(): string {
368 | return `${this.a}/${this.b}`;
369 | }
370 | }
371 |
372 | it('should inject values from Container as an object for the first argument of the factory', () => {
373 | const container = createContainer();
374 | container.bindValue(NUMBER, 1);
375 | container.bindValue(STRING, '2');
376 |
377 | const factory = injectableClass(TestClass, {a: NUMBER, b: STRING});
378 |
379 | expect(factory(container).join()).toBe('1/2');
380 | });
381 |
382 | it('should throw ResolverError error in case a value is not provided', () => {
383 | const container = createContainer();
384 | container.bindValue(NUMBER, 1);
385 |
386 | const factory = injectableClass(TestClass, {a: NUMBER, b: STRING});
387 |
388 | expect(() => factory(container)).toThrowError(
389 | new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
390 | );
391 | });
392 |
393 | it('should inject values of optional tokens in case values are not provided', () => {
394 | const container = createContainer();
395 | container.bindValue(NUMBER, 1);
396 |
397 | const factory = injectableClass(TestClass, {
398 | a: NUMBER,
399 | b: optional(STRING, 'value'),
400 | });
401 |
402 | expect(factory(container).join()).toBe('1/value');
403 | });
404 | });
405 |
--------------------------------------------------------------------------------
/packages/ditox/src/utils.ts:
--------------------------------------------------------------------------------
1 | import type {Container} from './container';
2 | import {Token} from './tokens';
3 |
4 | type ValuesProps = {[key: string]: unknown};
5 | type TokenProps = {
6 | [K in keyof Props]: Token;
7 | };
8 |
9 | /**
10 | * Checks if a value is the token
11 | */
12 | export function isToken(value: unknown): value is Token {
13 | return (
14 | value !== undefined &&
15 | value !== null &&
16 | typeof value === 'object' &&
17 | 'symbol' in value &&
18 | typeof (value as any).symbol === 'symbol'
19 | );
20 | }
21 |
22 | /**
23 | * Rebinds the array by the token with added new value.
24 | * @param container - Dependency container.
25 | * @param token - Token for an array of values.
26 | * @param value - New value which is added to the end of the array.
27 | */
28 | export function bindMultiValue(
29 | container: Container,
30 | token: Token>,
31 | value: T,
32 | ): void {
33 | const prevValues = container.get(token) ?? [];
34 | const nextValues = [...prevValues, value];
35 | container.bindValue(token, nextValues);
36 | }
37 |
38 | /**
39 | * Tries to resolve a value by the provided token.
40 | *
41 | * If an argument is an object which has tokens as its properties,
42 | * then returns an object containing resolved values as properties.
43 |
44 | * If a token is not found, then `undefined` value is used.
45 | *
46 | * @example
47 | * ```ts
48 | * const value = tryResolveValue(container, tokenA);
49 | * console.log(value); // 1
50 | *
51 | * const props = tryResolveValue(container, {a: tokenA, b: tokenB});
52 | * console.log(props); // {a: 1, b: 2}
53 | * ```
54 | */
55 | export function tryResolveValue<
56 | Tokens extends Token | {[key: string]: Token},
57 | Values extends Tokens extends Token
58 | ? V | undefined
59 | : Tokens extends TokenProps
60 | ? Partial
61 | : never,
62 | >(container: Container, token: Tokens): Values {
63 | if (isToken(token)) {
64 | return container.get(token) as Values;
65 | }
66 |
67 | const obj: any = {};
68 | Object.keys(token).forEach((key) => (obj[key] = container.get(token[key])));
69 | return obj;
70 | }
71 |
72 | /**
73 | * Returns an array of resolved values or objects with resolved values.
74 | *
75 | * If an item of the array is an object which has tokens as its properties,
76 | * then returns an object containing resolved values as properties.
77 |
78 | * If a token is not found, then `undefined` value is used.
79 | *
80 | * @example
81 | * ```ts
82 | * const items1 = tryResolveValues(container, tokenA);
83 | * console.log(items1); // [1]
84 | *
85 | * const items2 = tryResolveValues(container, tokenA, {a: tokenA, b: tokenB});
86 | * console.log(items2); // [1, {a: 1, b: 2}]
87 | * ```
88 | */
89 | export function tryResolveValues<
90 | Tokens extends (Token | {[key: string]: Token})[],
91 | Values extends {
92 | [K in keyof Tokens]: Tokens[K] extends Token
93 | ? V | undefined
94 | : Tokens[K] extends TokenProps
95 | ? Partial
96 | : never;
97 | },
98 | >(container: Container, ...tokens: Tokens): Values {
99 | return tokens.map((item) => tryResolveValue(container, item)) as Values;
100 | }
101 |
102 | /**
103 | * Resolves a value by the provided token.
104 | *
105 | * If an argument is an object which has tokens as its properties,
106 | * then returns an object containing resolved values as properties.
107 |
108 | * If a value is not found by the token, then `ResolverError` is thrown.
109 | *
110 | * @example
111 | * ```ts
112 | * const value = resolveValue(container, tokenA);
113 | * console.log(value); // 1
114 | *
115 | * const props = resolveValue(container, {a: tokenA, b: tokenB});
116 | * console.log(props); // {a: 1, b: 2}
117 | * ```
118 | */
119 | export function resolveValue<
120 | Tokens extends Token | {[key: string]: Token},
121 | Values extends Tokens extends Token
122 | ? V
123 | : Tokens extends TokenProps
124 | ? Props
125 | : never,
126 | >(container: Container, token: Tokens): Values {
127 | if (isToken(token)) {
128 | return container.resolve(token) as Values;
129 | }
130 |
131 | const obj: any = {};
132 | Object.keys(token).forEach(
133 | (key) => (obj[key] = container.resolve(token[key])),
134 | );
135 | return obj;
136 | }
137 |
138 | /**
139 | * Returns an array of resolved values or objects with resolved values.
140 | *
141 | * If an item of the array is an object which has tokens as its properties,
142 | * then returns an object containing resolved values as properties.
143 |
144 | * If a token is not found, then `ResolverError` is thrown.
145 | *
146 | * @example
147 | * ```ts
148 | * const items1 = resolveValues(container, tokenA);
149 | * console.log(items1); // [1]
150 | *
151 | * const items2 = resolveValues(container, tokenA, {a: tokenA, b: tokenB});
152 | * console.log(items2); // [1, {a: 1, b: 2}]
153 | * ```
154 | */
155 | export function resolveValues<
156 | Tokens extends (Token | {[key: string]: Token})[],
157 | Values extends {
158 | [K in keyof Tokens]: Tokens[K] extends Token
159 | ? V
160 | : Tokens[K] extends TokenProps
161 | ? Props
162 | : never;
163 | },
164 | >(container: Container, ...tokens: Tokens): Values {
165 | return tokens.map((item) => resolveValue(container, item)) as Values;
166 | }
167 |
168 | /**
169 | * Decorates a factory by passing resolved values as factory arguments.
170 | *
171 | * If an argument is an object which has tokens as its properties,
172 | * then returns an object containing resolved values as properties.
173 | *
174 | * @param factory - A factory.
175 | * @param tokens - Tokens which correspond to factory arguments.
176 | *
177 | * @return Decorated factory which takes a dependency container as a single argument.
178 | */
179 | export function injectable<
180 | Tokens extends (Token | {[key: string]: Token})[],
181 | Values extends {
182 | [K in keyof Tokens]: Tokens[K] extends Token
183 | ? V
184 | : Tokens[K] extends TokenProps
185 | ? Props
186 | : never;
187 | },
188 | Result,
189 | >(
190 | this: unknown,
191 | factory: (...params: Values) => Result,
192 | ...tokens: Tokens
193 | ): (container: Container) => Result {
194 | return (container) => {
195 | const values: Values = resolveValues(container, ...tokens);
196 | return factory.apply(this, values);
197 | };
198 | }
199 |
200 | /**
201 | * Decorates a class by passing resolved values as arguments to its constructor.
202 | *
203 | * If an argument is an object which has tokens as its properties,
204 | * then returns an object containing resolved values as properties.
205 | *
206 | * @param constructor - Constructor of a class
207 | * @param tokens - Tokens which correspond to constructor arguments
208 | *
209 | * @return A factory function which takes a dependency container as a single argument
210 | * and returns a new created class.
211 | */
212 | export function injectableClass<
213 | Tokens extends (Token | {[key: string]: Token})[],
214 | Values extends {
215 | [K in keyof Tokens]: Tokens[K] extends Token
216 | ? V
217 | : Tokens[K] extends TokenProps
218 | ? Props
219 | : never;
220 | },
221 | Result,
222 | >(
223 | this: unknown,
224 | constructor: new (...params: Values) => Result,
225 | ...tokens: Tokens
226 | ): (container: Container) => Result {
227 | return injectable(
228 | (...values) => new constructor(...values),
229 | ...tokens,
230 | );
231 | }
232 |
--------------------------------------------------------------------------------
/packages/ditox/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "files": ["src/index.ts"],
4 | "compilerOptions": {
5 | "noEmit": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/ditox/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["./mod.ts"],
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "target": "ES2019",
6 | "module": "ES2015",
7 | "lib": ["ES2019"],
8 | "jsx": "react-jsx",
9 | "importHelpers": false,
10 | "declaration": true,
11 | "sourceMap": true,
12 | "strict": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "moduleResolution": "node",
16 | "esModuleInterop": true,
17 | "skipLibCheck": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "isolatedModules": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 |
4 | "entryPointStrategy": "packages",
5 | "entryPoints": ["packages/ditox", "packages/ditox-react"],
6 | "exclude": ["**/*.test.ts"],
7 | "out": "build/docs",
8 |
9 | "name": "Ditox.js",
10 | "cname": "ditox.js.org",
11 | "visibilityFilters": {},
12 | "navigationLinks": {
13 | "GitHub": "https://github.com/mnasyrov/ditox"
14 | },
15 | "media": "media",
16 |
17 | "customDescription": "Dependency injection for modular web applications",
18 | "favicon": "media/lemon.svg",
19 | "replaceText": {
20 | "replacements": [
21 | {
22 | "pattern": "src=\"../../media/",
23 | "replace": "src=\"/media/"
24 | }
25 | ]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------