├── .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 | lemon 4 | 5 | **Dependency injection for modular web applications** 6 | 7 | [![npm](https://img.shields.io/npm/v/ditox)](https://www.npmjs.com/package/ditox) 8 | [![stars](https://img.shields.io/github/stars/mnasyrov/ditox)](https://github.com/mnasyrov/ditox/stargazers) 9 | [![types](https://img.shields.io/npm/types/ditox)](https://www.npmjs.com/package/ditox) 10 | [![licence](https://img.shields.io/github/license/mnasyrov/ditox)](https://github.com/mnasyrov/ditox/blob/master/LICENSE) 11 | [![coverage](https://coveralls.io/repos/github/mnasyrov/ditox/badge)](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 | 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 | 5 | 117 | 118 | 119 | 122 | 126 | 129 | 131 | 133 | 135 | 137 | 139 | 141 | 142 | 143 | 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 | ClientContainerTokenOptional TokenDitox.jsValueProduced ValueFactoryDefault valuecreates a containerbinds a valuebinds a factoryresolves a valuestores and invokesuses as a keystores and resolvesstores and resolvesproducesis resolved byis resolved byis resolved byextendshas -------------------------------------------------------------------------------- /media/lemon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 117 | 118 | 119 | 122 | 126 | 129 | 131 | 133 | 135 | 137 | 139 | 141 | 142 | 143 | 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 | lemon 4 | 5 | **Dependency injection container for React.js** 6 | 7 | Please see the documentation at [ditox.js.org](https://ditox.js.org) 8 | 9 | [![npm](https://img.shields.io/npm/v/ditox)](https://www.npmjs.com/package/ditox) 10 | [![stars](https://img.shields.io/github/stars/mnasyrov/ditox)](https://github.com/mnasyrov/ditox/stargazers) 11 | [![types](https://img.shields.io/npm/types/ditox)](https://www.npmjs.com/package/ditox) 12 | [![licence](https://img.shields.io/github/license/mnasyrov/ditox)](https://github.com/mnasyrov/ditox/blob/master/LICENSE) 13 | [![coverage](https://coveralls.io/repos/github/mnasyrov/ditox/badge)](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 | 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 | lemon 4 | 5 | **Dependency injection for modular web applications** 6 | 7 | Please see the documentation at [ditox.js.org](https://ditox.js.org) 8 | 9 | [![npm](https://img.shields.io/npm/v/ditox)](https://www.npmjs.com/package/ditox) 10 | [![stars](https://img.shields.io/github/stars/mnasyrov/ditox)](https://github.com/mnasyrov/ditox/stargazers) 11 | [![types](https://img.shields.io/npm/types/ditox)](https://www.npmjs.com/package/ditox) 12 | [![licence](https://img.shields.io/github/license/mnasyrov/ditox)](https://github.com/mnasyrov/ditox/blob/master/LICENSE) 13 | [![coverage](https://coveralls.io/repos/github/mnasyrov/ditox/badge)](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 | 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 | diagram 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 | --------------------------------------------------------------------------------