├── .all-contributorsrc ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── code-climate-test-coverage.yml │ ├── node-js-ci.yml │ └── node-js-publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .versionrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── _config.yml ├── assets ├── banner-transparent.png ├── banner-transparent.svg ├── banner.png ├── banner.svg ├── icon.png └── icon.svg ├── docs ├── .nojekyll └── index.html ├── examples ├── bootstrapped │ ├── components │ │ ├── config │ │ │ ├── config.js │ │ │ └── index.js │ │ ├── logger │ │ │ ├── index.js │ │ │ └── logger.js │ │ └── mongo │ │ │ ├── index.js │ │ │ └── mongo.js │ ├── index.js │ └── system.js └── simple │ ├── components │ ├── config.js │ ├── logger.js │ └── mongo.js │ ├── index.js │ └── system.js ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── test ├── .eslintrc.json ├── components │ ├── bar │ │ └── index.js │ └── foo │ │ └── index.js └── systemic.test.js └── utils.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "inigomarquinez", 12 | "name": "Íñigo Marquínez Prado", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/25435858?v=4", 14 | "profile": "https://github.com/inigomarquinez", 15 | "contributions": [ 16 | "doc", 17 | "maintenance", 18 | "review" 19 | ] 20 | } 21 | ], 22 | "contributorsPerLine": 7, 23 | "skipCi": true, 24 | "repoType": "github", 25 | "repoHost": "https://github.com", 26 | "projectName": "systemic", 27 | "projectOwner": "onebeyond" 28 | } 29 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | engines: 3 | eslint: 4 | enabled: false 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - javascript 10 | checks: 11 | method-complexity: 12 | enabled: false 13 | method-lines: 14 | enabled: false 15 | ratings: 16 | paths: 17 | - index.js 18 | exclude_patterns: 19 | - 'examples/' 20 | - 'test/' 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-airbnb-base", "prettier"], 3 | "env": { 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": "2017" 8 | }, 9 | "rules": { 10 | "consistent-return": 0, 11 | "func-names": 0, 12 | "prefer-object-spread": 0, 13 | "no-param-reassign": 0, 14 | "no-plusplus": 0, 15 | "no-prototype-builtins": 0, 16 | "no-shadow": ["error", { "allow": ["err", "cb"] }], 17 | "no-underscore-dangle": 0, 18 | "no-unused-expressions": 0, 19 | "no-use-before-define": 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/code-climate-test-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Climate Test Reporter 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: # added using https://github.com/step-security/secure-workflows 10 | contents: read 11 | 12 | jobs: 13 | code-climate: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 18 | with: 19 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 20 | 21 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 22 | - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 23 | with: 24 | node-version: '18.x' 25 | - run: npm ci 26 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 27 | - run: chmod +x ./cc-test-reporter 28 | - run: ./cc-test-reporter before-build 29 | - run: npm run coverage 30 | - run: ./cc-test-reporter format-coverage -t lcov coverage/lcov.info 31 | - run: ./cc-test-reporter upload-coverage 32 | env: 33 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 34 | -------------------------------------------------------------------------------- /.github/workflows/node-js-ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [12.x, 14.x, 16.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm ci 17 | - run: npm run lint 18 | - run: npm run prettier 19 | - run: npm test 20 | 21 | code-climate: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v2 27 | with: 28 | node-version: '16.x' 29 | - run: npm ci 30 | - run: npm install -g nyc 31 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 32 | - run: chmod +x ./cc-test-reporter 33 | - run: ./cc-test-reporter before-build 34 | - run: npm run coverage 35 | - run: ./cc-test-reporter format-coverage -t lcov coverage/lcov.info 36 | - run: ./cc-test-reporter upload-coverage 37 | env: 38 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 39 | -------------------------------------------------------------------------------- /.github/workflows/node-js-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: '16.x' 15 | - run: npm ci 16 | - run: npm run lint 17 | - run: npm run prettier 18 | - run: npm test 19 | 20 | publish-npm: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v2 26 | with: 27 | node-version: '16.x' 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm ci 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | .codeclimate 5 | .nyc_output/ 6 | 7 | 8 | ## IDE 9 | .idea 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged && npm test 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .codeclimate.yml 3 | .idea 4 | .eslintignore 5 | .eslintrc 6 | .github 7 | .nyc_output 8 | _config.yml 9 | coverage 10 | cc-test-reporter 11 | docs 12 | node_modules 13 | test 14 | assets 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { "type": "feat", "section": "🆕 Features", "hidden": false }, 4 | { "type": "fix", "section": "🐛 Bug Fixes", "hidden": false }, 5 | { "type": "chore", "section": "🔧 Others", "hidden": false }, 6 | { "type": "docs", "section": "📝 Docs", "hidden": false }, 7 | { "type": "style", "section": "🎨 Styling", "hidden": false }, 8 | { "type": "refactor", "section": "🔄 Code Refactoring", "hidden": false }, 9 | { "type": "perf", "section": "📈 Performance Improvements", "hidden": false }, 10 | { "type": "test", "section": "🔬 Tests", "hidden": false }, 11 | { "type": "ci", "section": "☁️ CI", "hidden": false } 12 | ], 13 | "commit-all": true 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.1.2 4 | 5 | - Fix typescript definitions via . Thanks [PR60](https://github.com/guidesmiths/systemic/pull/60). Thanks [teunmooij](https://github.com/teunmooij). 6 | - Remove unused dev deps 7 | 8 | ## 4.1.1 9 | 10 | - Use eslint-config-airbnb-base 11 | - Move debug to prod dependency 12 | 13 | ## 4.1.0 14 | 15 | - Added optional dependencies 16 | - Updated typescript definitions to better reflect all options of Systemic: 17 | - optional type param on `Systemic` to set assumed components of master system when creating a subsystem 18 | - allow setting a simple dependency on component that doesn't need it (to force the order in which dependencies are created) 19 | 20 | ## 4.0.3 21 | 22 | - Remove standard-version because it is deprecated and doesnt add much value 23 | - Stop prettier wrapping markdown examples 24 | 25 | ## 4.0.2 26 | 27 | - added assets (logo and banner) ([dc4d534](https://github.com/guidesmiths/systemic/commit/dc4d534edefbec92bff085ea16e5cdd99a8e8956)) 28 | - improved readme adding new assets ([6d1309d](https://github.com/guidesmiths/systemic/commit/6d1309de4e02c01a5bfdf35fe15879e4080ad1ab)) 29 | - added assets folder to npmignore ([cb35c07](https://github.com/guidesmiths/systemic/commit/cb35c0756780fdae82766732653f51c9729ee44b)) 30 | 31 | ## 4.0.1 32 | 33 | - Remove chai 34 | - Remove chance 35 | - Update dependencies 36 | - Tweak GitHub actions 37 | - Improve readme 38 | 39 | ## 4.0.0 40 | 41 | - Introduce prettier 42 | - Updated dependendencies 43 | - Drop support for Node 10 44 | 45 | ## 3.3.10 46 | 47 | - Added typescript definitions 48 | 49 | ## 3.3.9 50 | 51 | - Exclude various files (including the 12M cc-test-reporter binary) from the npm package. 52 | 53 | ## 3.3.8 54 | 55 | - Remove lodash 56 | - Replace mocha with zunit 57 | - Update nyc 58 | - Replace travis with github actions 59 | - Replace eslint imperative with ESNext and updated syntax 60 | - Bump dependencies 61 | 62 | ## 3.3.7 63 | 64 | - Tweak deployment 65 | 66 | ## 3.3.6 67 | 68 | - Tweak deployment 69 | 70 | ## 3.3.5 71 | 72 | - Tweak deployment 73 | 74 | ## 3.3.4 75 | 76 | - Tweak deployment 77 | 78 | ## 3.3.3 79 | 80 | - Automate codeclimate 81 | - Tweak deployment 82 | 83 | ## 3.3.1 84 | 85 | - Housekeeping 86 | 87 | ## 3.3.0 88 | 89 | - Updated dependencies 90 | 91 | ## 3.2.0 92 | 93 | - Updated dependencies 94 | 95 | ## 3.1.0 96 | 97 | - Updated dependencies 98 | - Dropped node 4 and 5 support 99 | 100 | ## 3.0.0 101 | 102 | ### Breaking Changes 103 | 104 | - Component start and stop functions can return promises instead of taking callbacks. 105 | To support this change callback based compnents start functions are no longer varardic, and must always specify both the dependencies and callback arguments, i.e. `function start(dependencies, cb) { ... }`. 106 | 107 | - Bootstrapped systems support export default syntax 108 | 109 | ## 2.2.0 110 | 111 | - Bootstrapping supports sub systems wrapped in functions 112 | - Improve README 113 | - Improve examples 114 | 115 | ## 2.1.0 116 | 117 | - Fix bug where if you destructured components in the system start callback and a component errored you received a "Cannot destructure property" error 118 | - Bootstrapping supports sub systems wrapped in functions 119 | - Improve README 120 | 121 | ## 2.0.0 122 | 123 | - System lifecycle methods (start, stop, restart) return promises 124 | 125 | ### Updated 126 | 127 | - All dependencies to latest compatible versions 128 | 129 | ### Removed 130 | 131 | - codeclimate-test-report (install globally if needed) 132 | 133 | ### Updated 134 | 135 | - Readme 136 | 137 | ## 1.3.3 138 | 139 | - Removed accidental console.log 140 | - Updated dependencies 141 | 142 | ## 1.3.2 143 | 144 | - Updated dev dependencies 145 | 146 | ## 1.3.1 147 | 148 | - Codeclimate automatically runs on push 149 | 150 | ## 1.3.0 151 | 152 | - Fixed coverage badges 153 | - Configuring code climate 154 | 155 | ## 1.2.3 156 | 157 | - Node 7 build 158 | 159 | ## 1.2.2 160 | 161 | - More badges 162 | 163 | ## 1.2.1 164 | 165 | - .npmignore 166 | 167 | ## 1.2.0 168 | 169 | - This changelog 170 | - License 171 | - Badges 172 | 173 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 174 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2022 GuideSmiths Ltd. 4 | Copyright (c) One Beyond 2022 to present. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 | 5 |
6 | 7 |

📦 A minimal dependency injection library.

8 | 9 |
10 | 11 |

12 | npm version 13 | npm downloads 14 | node-js-ci workflow 15 | node-js-publish workflow 16 | Code Climate maintainability 17 | Code Climate test coverage 18 | socket.dev 19 | all-contributors 20 |

21 | 22 | ## tl;dr 23 | 24 | ### Define the system 25 | 26 | 27 | ```js 28 | const Systemic = require('systemic'); 29 | const Config = require('./components/config'); 30 | const Logger = require('./components/logger'); 31 | const Mongo = require('./components/mongo'); 32 | 33 | module.exports = () => Systemic() 34 | .add('config', Config(), { scoped: true }) 35 | .add('logger', Logger()).dependsOn('config') 36 | .add('mongo.primary', Mongo()).dependsOn('config', 'logger') 37 | .add('mongo.secondary', Mongo()).dependsOn('config', 'logger'); 38 | ``` 39 | 40 | 41 | ### Run the system 42 | 43 | 44 | ```js 45 | const System = require('./system'); 46 | 47 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 }; 48 | 49 | async function start() { 50 | const system = System(); 51 | const { config, mongo, logger } = await system.start(); 52 | 53 | console.log('System has started. Press CTRL+C to stop'); 54 | 55 | Object.keys(events).forEach((name) => { 56 | process.on(name, async () => { 57 | await system.stop(); 58 | console.log('System has stopped'); 59 | process.exit(events[name]); 60 | }); 61 | }); 62 | } 63 | 64 | start(); 65 | ``` 66 | 67 | 68 | See the [examples](https://github.com/onebeyond/systemic/tree/master/examples) for mode details and don't miss the section on [bootstrapping](#bootstraping-components) for how to organise large projects. 69 | 70 | ### Why Use Dependency Injection With Node.js? 71 | 72 | Node.js applications tend to be small and have few layers than applications developed in other languages such as Java. This reduces the benefit of dependency injection, which encouraged [the Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle), discouraged [God Objects](https://en.wikipedia.org/wiki/God_object) and facilitated unit testing through [test doubles](https://en.wikipedia.org/wiki/Test_double). 73 | 74 | We've found that when writing microservices the life cycle of an application and its dependencies is a nuisance to manage over and over again. We wanted a way to consistently express that our service should establish database connections before listening for http requests, and shutdown those connections only after it had stopped listening. We found that before doing anything we need to load config from remote sources, and configure loggers. This is why we use DI. 75 | 76 | Our first attempt at a dependency injection framework was [Electrician](https://www.npmjs.com/package/electrician). It served it's purpose well, but the API had a couple of limitations that we wanted to fix. This would have required a backwards incompatible change, so instead we decided to write a new DI library - Systemic. 77 | 78 | ### Concepts 79 | 80 | Systemic has 4 main concepts 81 | 82 | 1. Systems 83 | 1. Runners 84 | 1. Components 85 | 1. Dependencies 86 | 87 | #### Systems 88 | 89 | You add components and their dependencies to a system. When you start the system, systemic iterates through all the components, starting them in the order derived from the dependency graph. When you stop the system, systemic iterates through all the components stopping them in the reverse order. 90 | 91 | 92 | ```js 93 | const Systemic = require('systemic'); 94 | const Config = require('./components/config'); 95 | const Logger = require('./components/logger'); 96 | const Mongo = require('./components/mongo'); 97 | 98 | async function init() { 99 | const system = Systemic() 100 | .add('config', Config(), { scoped: true }) 101 | .add('logger', Logger()) 102 | .dependsOn('config') 103 | .add('mongo.primary', Mongo()) 104 | .dependsOn('config', 'logger') 105 | .add('mongo.secondary', Mongo()) 106 | .dependsOn('config', 'logger'); 107 | 108 | const { config, mongo, logger } = await system.start(); 109 | 110 | console.log('System has started. Press CTRL+C to stop'); 111 | 112 | Object.keys(events).forEach((name) => { 113 | process.on(name, async () => { 114 | await system.stop(); 115 | console.log('System has stopped'); 116 | process.exit(events[name]); 117 | }); 118 | }); 119 | } 120 | 121 | init(); 122 | ``` 123 | 124 | 125 | System life cycle functions (start, stop, restart) return a promise, but can also take callbacks. 126 | 127 | ### Runners 128 | 129 | While not shown in the above examples we usually separate the system definition from system start. This is important for testing since you often want to make changes to the system definition (e.g. replacing components with stubs), before starting the system. By wrapping the system definition in a function you create a new system in each of your tests. 130 | 131 | 132 | ```js 133 | // system.js 134 | module.exports = () => Systemic().add('config', Config()).add('logger', Logger()).dependsOn('config').add('mongo', Mongo()).dependsOn('config', 'logger'); 135 | ``` 136 | 137 | 138 | 139 | ```js 140 | // index.js 141 | const System = require('./system'); 142 | 143 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 }; 144 | 145 | async function start() { 146 | const system = System(); 147 | const { config, mongo, logger } = await system.start(); 148 | 149 | console.log('System has started. Press CTRL+C to stop'); 150 | 151 | Object.keys(events).forEach((name) => { 152 | process.on(name, async () => { 153 | await system.stop(); 154 | console.log('System has stopped'); 155 | process.exit(events[name]); 156 | }); 157 | }); 158 | } 159 | 160 | start(); 161 | ``` 162 | 163 | 164 | There are some out of the box runners we can be used in your applications or as a reference for your own custom runner 165 | 166 | 1. [Service Runner](https://github.com/onebeyond/systemic-service-runner) 167 | 1. [Domain Runner](https://github.com/onebeyond/systemic-domain-runner) 168 | 169 | #### Components 170 | 171 | A component is an object with optional asynchronous start and stop functions. The start function should yield the underlying resource after it has been started. e.g. 172 | 173 | 174 | ```js 175 | module.exports = () => { 176 | let db; 177 | 178 | async function start(dependencies) { 179 | db = await MongoClient.connect('mongo://localhost/example'); 180 | return db; 181 | } 182 | 183 | async function stop() { 184 | return db.close(); 185 | } 186 | 187 | return { 188 | start: start, 189 | stop: stop, 190 | }; 191 | }; 192 | ``` 193 | 194 | 195 | The components stop function is useful for when you want to disconnect from an external service or release some other kind of resource. The start and stop functions support both promises and callbacks (not shown) 196 | 197 | There are out of the box components for [express](https://github.com/onebeyond/systemic-express), [mongodb](https://github.com/onebeyond/systemic-mongodb), [redis](https://github.com/onebeyond/systemic-redis), [postgres](https://github.com/onebeyond/systemic-pg) and [rabbitmq](https://github.com/onebeyond/systemic-rabbitmq). 198 | 199 | #### Dependencies 200 | 201 | A component's dependencies must be registered with the system 202 | 203 | 204 | ```js 205 | const Systemic = require('systemic'); 206 | const Config = require('./components/config'); 207 | const Logger = require('./components/logger'); 208 | const Mongo = require('./components/mongo'); 209 | 210 | module.exports = () => Systemic() 211 | .add('config', Config()) 212 | .add('logger', Logger()) 213 | .dependsOn('config') 214 | .add('mongo', Mongo()) 215 | .dependsOn('config', 'logger'); 216 | ``` 217 | 218 | 219 | The components dependencies are injected via it's start function 220 | 221 | 222 | ```js 223 | async function start({ config }) { 224 | db = await MongoClient.connect(config.url); 225 | return db; 226 | } 227 | ``` 228 | 229 | 230 | #### Mapping dependencies 231 | 232 | You can rename dependencies passed to a components start function by specifying a mapping object instead of a simple string 233 | 234 | 235 | ```js 236 | module.exports = () => Systemic() 237 | .add('config', Config()) 238 | .add('mongo', Mongo()) 239 | .dependsOn({ component: 'config', destination: 'options' }); 240 | ``` 241 | 242 | 243 | If you want to inject a property or subdocument of the dependency thing you can also express this with a dependency mapping 244 | 245 | 246 | ```js 247 | module.exports = () => Systemic() 248 | .add('config', Config()) 249 | .add('mongo', Mongo()) 250 | .dependsOn({ component: 'config', source: 'config.mongo' }); 251 | ``` 252 | 253 | 254 | Now `config.mongo` will be injected as `config` instead of the entire configuration object 255 | 256 | #### Scoped Dependencies 257 | 258 | Injecting a sub document from a json configuration file is such a common use case, you can enable this behaviour automatically by 'scoping' the component. The following code is equivalent to that above 259 | 260 | 261 | ```js 262 | module.exports = () => Systemic() 263 | .add('config', Config(), { scoped: true }) 264 | .add('mongo', Mongo()) 265 | .dependsOn('config'); 266 | ``` 267 | 268 | 269 | #### Optional Dependencies 270 | 271 | By default an error is thrown if a dependency is not available on system start. Sometimes a component might have an optional dependency on a component they may or may not be available in the system, typically when using subsystems. In this situation a dependency can be marked as optional. 272 | 273 | 274 | ```js 275 | module.exports = () => Systemic() 276 | .add('app', app()) 277 | .add('server', server()) 278 | .dependsOn('app', { component: 'routes', optional: true }); 279 | ``` 280 | 281 | 282 | #### Overriding Components 283 | 284 | Attempting to add the same component twice will result in an error, but sometimes you need to replace existing components with test doubles. Under such circumstances use `set` instead of `add` 285 | 286 | 287 | ```js 288 | const System = require('../lib/system'); 289 | const stub = require('./stubs/store'); 290 | 291 | let testSystem; 292 | 293 | before(async () => { 294 | testSystem = System().set('store', stub); 295 | await testSystem.start(); 296 | }); 297 | 298 | after(async () => { 299 | await testSystem.stop(); 300 | }); 301 | ``` 302 | 303 | 304 | #### Removing Components 305 | 306 | Removing components during tests can decrease startup time 307 | 308 | 309 | ```js 310 | const System = require('../lib/system'); 311 | 312 | let testSystem; 313 | 314 | before(async () => { 315 | testSystem = System().remove('server'); 316 | await testSystem.start(); 317 | }); 318 | 319 | after(async () => { 320 | await testSystem.stop(); 321 | }); 322 | ``` 323 | 324 | 325 | #### Including components from another system 326 | 327 | You can simplify large systems by breaking them up into smaller ones, then including their component definitions into the main system. 328 | 329 | 330 | ```js 331 | // db-system.js 332 | const Systemic = require('systemic'); 333 | const Mongo = require('./components/mongo'); 334 | 335 | module.exports = () => Systemic() 336 | .add('mongo', Mongo()) 337 | .dependsOn('config', 'logger'); 338 | ``` 339 | 340 | 341 | 342 | ```js 343 | // system.js 344 | const Systemic = require('systemic'); 345 | const UtilSystem = require('./lib/util/system'); 346 | const WebSystem = require('./lib/web/system'); 347 | const DbSystem = require('./lib/db/system'); 348 | 349 | module.exports = () => Systemic().include(UtilSystem()).include(WebSystem()).include(DbSystem()); 350 | ``` 351 | 352 | 353 | #### Grouping components 354 | 355 | Sometimes it's convenient to depend on a group of components. e.g. 356 | 357 | 358 | ```js 359 | module.exports = () => Systemic() 360 | .add('app', app()) 361 | .add('routes.admin', adminRoutes()) 362 | .dependsOn('app') 363 | .add('routes.api', apiRoutes()) 364 | .dependsOn('app') 365 | .add('routes') 366 | .dependsOn('routes.admin', 'routes.api') 367 | .add('server') 368 | .dependsOn('app', 'routes'); 369 | ``` 370 | 371 | 372 | The above example will create a component 'routes', which will depend on routes.admin and routes.api and be injected as 373 | 374 | 375 | ```js 376 | { 377 | routes: { 378 | admin: { ... }, 379 | adpi: { ... } 380 | } 381 | } 382 | ``` 383 | 384 | 385 | #### Bootstrapping components 386 | 387 | The dependency graph for a medium size project can grow quickly leading to a large system definition. To simplify this you can bootstrap components from a specified directory, where each folder in the directory includes an index.js which defines a sub system. e.g. 388 | 389 | ``` 390 | 391 | lib/ 392 | |- system.js 393 | |- components/ 394 | |- config/ 395 | |- index.js 396 | |- logging/ 397 | |- index.js 398 | |- express/ 399 | |- index.js 400 | |- routes/ 401 | |- admin-routes.js 402 | |- api-routes.js 403 | |- index.js 404 | ``` 405 | 406 | 407 | 408 | 409 | ```js 410 | // system.js 411 | const Systemic = require('systemic'); 412 | const path = require('path'); 413 | 414 | module.exports = () => Systemic() 415 | .bootstrap(path.join(__dirname, 'components')); 416 | ``` 417 | 418 | 419 | 420 | ```js 421 | // components/routes/index.js 422 | const Systemic = require('systemic'); 423 | const adminRoutes = require('./admin-routes'); 424 | const apiRoutes = require('./api-routes'); 425 | 426 | module.exports = () => Systemic() 427 | .add('routes.admin', adminRoutes()) 428 | .dependsOn('app') 429 | .add('routes.api', apiRoutes()) 430 | .dependsOn('app', 'mongodb') 431 | .add('routes') 432 | .dependsOn('routes.admin', 'routes.api'); 433 | ``` 434 | 435 | 436 | ### Debugging 437 | 438 | You can debug systemic by setting the DEBUG environment variable to `systemic:*`. Naming your systems will make reading the debug output easier when you have more than one. 439 | 440 | 441 | ```js 442 | // system.js 443 | const Systemic = require('systemic'); 444 | const path = require('path'); 445 | 446 | module.exports = () => Systemic({ name: 'server' }) 447 | .bootstrap(path.join(__dirname, 'components')); 448 | ``` 449 | 450 | 451 | 452 | ```js 453 | // components/routes/index.js 454 | import Systemic from 'systemic'; 455 | import adminRoutes from './admin-routes'; 456 | import apiRoutes from './api-routes'; 457 | 458 | export default Systemic({ name: 'routes' }) 459 | .add('routes.admin', adminRoutes()) 460 | .add('routes.api', apiRoutes()) 461 | .add('routes') 462 | .dependsOn('routes.admin', 'routes.api'); 463 | ``` 464 | 465 | 466 | ``` 467 | DEBUG='systemic:*' node system 468 | systemic:index Adding component routes.admin to system routes +0ms 469 | systemic:index Adding component routes.api to system auth +2ms 470 | systemic:index Adding component routes to system auth +1ms 471 | systemic:index Including definitions from sub system routes into system server +0ms 472 | systemic:index Starting system server +0ms 473 | systemic:index Inspecting component routes.admin +0ms 474 | systemic:index Starting component routes.admin +0ms 475 | systemic:index Component routes.admin started +15ms 476 | systemic:index Inspecting component routes.api +0ms 477 | systemic:index Starting component routes.api +0ms 478 | systemic:index Component routes.api started +15ms 479 | systemic:index Inspecting component routes +0ms 480 | systemic:index Injecting dependency routes.admin as routes.admin into routes +0ms 481 | systemic:index Injecting dependency routes.api as routes.api into routes +0ms 482 | systemic:index Starting component routes +0ms 483 | systemic:index Component routes started +15ms 484 | systemic:index Injecting dependency routes as routes into server +1ms 485 | systemic:index System server started +15ms 486 | ``` 487 | 488 | ## Contributors ✨ 489 | 490 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 |
Íñigo Marquínez Prado
Íñigo Marquínez Prado

📖 🚧 👀
502 | 503 | 504 | 505 | 506 | 507 | 508 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 509 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /assets/banner-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/assets/banner-transparent.png -------------------------------------------------------------------------------- /assets/banner-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/assets/banner.png -------------------------------------------------------------------------------- /assets/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onebeyond/systemic/a2c7267a0987a9d106e4257f7a8800c258f18dfc/docs/.nojekyll -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | systemic - A minimal dependency injection library for node 6 | 7 | 8 | 12 | 13 | 14 | 15 |
16 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/bootstrapped/components/config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | async function start(dependencies) { 3 | return { 4 | logger: { 5 | level: 'warn', 6 | }, 7 | mongo: { 8 | primary: { 9 | url: 'mongo://primary', 10 | }, 11 | secondary: { 12 | url: 'mongo://secondary', 13 | }, 14 | }, 15 | }; 16 | } 17 | 18 | return { 19 | start: start, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /examples/bootstrapped/components/config/index.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../../../..'); 2 | const Config = require('./config'); 3 | 4 | module.exports = () => Systemic({ name: 'config' }).add('config', Config(), { scoped: true }); 5 | -------------------------------------------------------------------------------- /examples/bootstrapped/components/logger/index.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../../../..'); 2 | const Logger = require('./logger'); 3 | 4 | module.exports = () => Systemic({ name: 'logger' }).add('logger', Logger()).dependsOn('config'); 5 | -------------------------------------------------------------------------------- /examples/bootstrapped/components/logger/logger.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | async function start({ config }) { 3 | console.log('Logging at level', config.level); 4 | return console; 5 | } 6 | 7 | return { 8 | start: start, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /examples/bootstrapped/components/mongo/index.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../../../..'); 2 | const Mongo = require('./mongo'); 3 | 4 | module.exports = () => 5 | Systemic({ name: 'mongo' }) 6 | .add('mongo.primary', Mongo()) 7 | .dependsOn('config', 'logger') 8 | .add('mongo.secondary', Mongo()) 9 | .dependsOn('config', 'logger'); 10 | -------------------------------------------------------------------------------- /examples/bootstrapped/components/mongo/mongo.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | async function start({ logger, config }) { 3 | logger.info('Connecting to', config.url); 4 | return {}; 5 | } 6 | 7 | return { 8 | start: start, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /examples/bootstrapped/index.js: -------------------------------------------------------------------------------- 1 | const System = require('./system'); 2 | 3 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 }; 4 | 5 | async function start() { 6 | const system = System(); 7 | const { config, mongo, logger } = await system.start(); 8 | 9 | console.log('System has started. Press CTRL+C to stop'); 10 | 11 | Object.keys(events).forEach((name) => { 12 | process.on(name, async () => { 13 | await system.stop(); 14 | console.log('System has stopped'); 15 | process.exit(events[name]); 16 | }); 17 | }); 18 | } 19 | 20 | start(); 21 | 22 | setInterval(() => Number.MAX_INT_VALUE); 23 | -------------------------------------------------------------------------------- /examples/bootstrapped/system.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../..'); 2 | const path = require('path'); 3 | const components = path.join(__dirname, 'components'); 4 | 5 | module.exports = () => Systemic({ name: 'main' }).bootstrap(components); 6 | -------------------------------------------------------------------------------- /examples/simple/components/config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | async function start(dependencies) { 3 | return { 4 | logger: { 5 | level: 'warn', 6 | }, 7 | mongo: { 8 | primary: { 9 | url: 'mongo://primary', 10 | }, 11 | secondary: { 12 | url: 'mongo://secondary', 13 | }, 14 | }, 15 | }; 16 | } 17 | 18 | return { 19 | start: start, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /examples/simple/components/logger.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | async function start({ config }) { 3 | console.log('Logging at level', config.level); 4 | return console; 5 | } 6 | 7 | return { 8 | start: start, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /examples/simple/components/mongo.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | async function start({ logger, config }) { 3 | return logger.info('Connecting to', config.url); 4 | } 5 | 6 | return { 7 | start: start, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | const System = require('./system'); 2 | 3 | const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 }; 4 | 5 | async function start() { 6 | const system = System(); 7 | const { config, mongo, logger } = await system.start(); 8 | 9 | console.log('System has started. Press CTRL+C to stop'); 10 | 11 | Object.keys(events).forEach((name) => { 12 | process.on(name, async () => { 13 | await system.stop(); 14 | console.log('System has stopped'); 15 | process.exit(events[name]); 16 | }); 17 | }); 18 | } 19 | 20 | start(); 21 | 22 | setInterval(() => Number.MAX_INT_VALUE); 23 | -------------------------------------------------------------------------------- /examples/simple/system.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../..'); 2 | const Config = require('./components/config'); 3 | const Logger = require('./components/logger'); 4 | const Mongo = require('./components/mongo'); 5 | 6 | module.exports = () => 7 | Systemic() 8 | .add('config', Config(), { scoped: true }) 9 | .add('logger', Logger()) 10 | .dependsOn('config') 11 | .add('mongo.primary', Mongo()) 12 | .dependsOn('config', 'logger') 13 | .add('mongo.secondary', Mongo()) 14 | .dependsOn('config', 'logger'); 15 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | type RequiredKeys = { 2 | [K in keyof T]-?: {} extends Pick ? never : K; 3 | }[keyof T]; 4 | 5 | type NameToDestination = TOption extends { 6 | component: infer Component; 7 | destination?: infer Destination; 8 | } 9 | ? unknown extends Destination 10 | ? Component 11 | : Destination 12 | : TOption extends string | number | symbol 13 | ? TOption 14 | : never; 15 | 16 | type MissingDependencies, TNames extends unknown[]> = TNames extends [ 17 | infer Name, 18 | ...infer Rest 19 | ] 20 | ? NameToDestination extends keyof TDependencies 21 | ? MissingDependencies>, Rest> 22 | : MissingDependencies 23 | : TDependencies; 24 | 25 | /** 26 | * Systemic component that can be added to the systemic system. 27 | * @template TComponent The type of the component that will be exposed by the systemic system 28 | * @template TDependencies The type of the dependencies this component depends on 29 | */ 30 | export type Component = {}> = { 31 | /** 32 | * Starts this component 33 | * @param {TDependencies} dependencies The dependencies of this component 34 | * @returns A started component 35 | */ 36 | start: (dependencies: TDependencies) => Promise; 37 | /** 38 | * Stops this component 39 | */ 40 | stop?: () => Promise; 41 | }; 42 | 43 | /** 44 | * Systemic component that can be added to the systemic system. 45 | * @template TComponent The type of the component that will be exposed by the systemic system 46 | * @template TDependencies The type of the dependencies this component depends on 47 | */ 48 | export type CallbackComponent = {}> = { 49 | /** 50 | * Starts this component 51 | * @param {TDependencies} dependencies The dependencies of this component 52 | * @param callback Callback receives the component after it has been built 53 | */ 54 | start: (dependencies: TDependencies, callback: (err: any, component: TComponent) => void) => void; 55 | /** 56 | * Stops this component 57 | * @param callback Callback is called when the component has been stopped 58 | */ 59 | stop?: (callback: (err?: any) => void) => void; 60 | }; 61 | 62 | type SimpleDependsOnOption = keyof TSystemic; 63 | type MappingDependsOnOption = TDependencyKeys extends keyof TSystemic 64 | ? { 65 | component: keyof TSystemic; 66 | destination?: TDependencyKeys; 67 | optional?: boolean; 68 | source?: string; 69 | } 70 | : { 71 | component: keyof TSystemic; 72 | destination: TDependencyKeys; 73 | optional?: boolean; 74 | source?: string; 75 | }; 76 | type DependsOnOption = 77 | | SimpleDependsOnOption 78 | | MappingDependsOnOption; 79 | 80 | type DependsOn, TDependencies extends Record> = { 81 | /** 82 | * Specifies which other components the last added components depends on. 83 | * When name and type of the dependencies match those available in the system, the dependency can be added by name. 84 | * When a dependency is named differently in the system or only part of a component is required as a dependency, a MappingDependsOnOption can be used. 85 | */ 86 | dependsOn: []>( 87 | ...names: TNames 88 | ) => SystemicBuild>; 89 | }; 90 | 91 | type SystemicBuild, TDependencies extends Record> = [ 92 | RequiredKeys 93 | ] extends [never] 94 | ? Systemic & DependsOn 95 | : DependsOn; 96 | 97 | /** 98 | * Systemic system. 99 | */ 100 | export type Systemic> = { 101 | /** 102 | * The name of the system 103 | */ 104 | name: string; 105 | 106 | /** 107 | * Adds a component to the system 108 | * @param {string} name the name under which the component will be registered in the system 109 | * @param {Component} component the component to be added 110 | * @param options registration options 111 | */ 112 | add: = {}>( 113 | name: S extends keyof T ? never : S, // We don't allow duplicate names 114 | component?: Component | CallbackComponent | TComponent, 115 | options?: { scoped?: boolean } 116 | ) => SystemicBuild< 117 | { 118 | [G in keyof T | S]: G extends keyof T ? T[G] : TComponent; 119 | }, 120 | TDependencies 121 | >; 122 | 123 | /** 124 | * Attempting to add the same component twice will result in an error, but sometimes you need to replace existing components with test doubles. Under such circumstances use set instead of add. 125 | * @param {string} name the name under which the component will be registered in the system 126 | * @param {Component} component the component to be added 127 | * @param options registration options 128 | */ 129 | set: = {}>( 130 | name: S, 131 | component: Component | CallbackComponent | TComponent, 132 | options?: { scoped?: boolean } 133 | ) => SystemicBuild< 134 | { 135 | [G in keyof T | S]: G extends keyof T ? T[G] : TComponent; 136 | }, 137 | TDependencies 138 | >; 139 | 140 | /** 141 | * Adds a configuration to the system, which will be available as a scoped dependency named 'config' 142 | */ 143 | configure: = {}>( 144 | component: Component | CallbackComponent | TComponent 145 | ) => SystemicBuild; 146 | 147 | /** 148 | * Removes a component from the system. 149 | * Removing components during tests can decrease startup time. 150 | */ 151 | remove: (name: S) => Systemic>; 152 | 153 | /** 154 | * Includes a subsystem into this systemic system 155 | */ 156 | merge: >(subSystem: Systemic) => Systemic; 157 | 158 | /** 159 | * Includes a subsystem into this systemic system 160 | */ 161 | include: >(subSystem: Systemic) => Systemic; 162 | 163 | /** 164 | * Starts the system and all of its components 165 | */ 166 | start(callback: (error: Error | null, result?: T) => void): void; 167 | start(): Promise; 168 | 169 | /** 170 | * Stops the system and all of its components 171 | */ 172 | stop(callback: (error: Error | null) => void): void; 173 | stop(): Promise; 174 | 175 | /** 176 | * Restarts the system and all of its components 177 | */ 178 | restart(callback: (error: Error | null, result?: T) => void): void; 179 | restart(): Promise; 180 | 181 | /** 182 | * The dependency graph for a medium size project can grow quickly leading to a large system definition. 183 | * To simplify this you can bootstrap components from a specified directory, where each folder in the directory includes an index.js which defines a sub system. e.g. 184 | * See documentation for more details. 185 | */ 186 | bootstrap: = Record>(path: string) => Systemic; 187 | }; 188 | 189 | /** 190 | * Creates a system to which components for dependency injection can be added 191 | * @returns An empty systemic system 192 | */ 193 | declare function Systemic = {}>(options?: { name?: string }): Systemic; 194 | 195 | export default Systemic; 196 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const debug = require('debug')('systemic:index'); 3 | const { format } = require('util'); 4 | const Toposort = require('toposort-class'); 5 | const requireAll = require('require-all'); 6 | const { randomName, isFunction, arraysIntersection, hasProp, getProp, setProp } = require('./utils'); 7 | 8 | module.exports = function (_params) { 9 | const api = {}; 10 | const params = Object.assign({}, { name: randomName() }, _params); 11 | let definitions = {}; 12 | let currentDefinition; 13 | let running = false; 14 | let started; 15 | const defaultComponent = { 16 | start(dependencies, cb) { 17 | cb(null, dependencies); 18 | }, 19 | }; 20 | 21 | function bootstrap(path) { 22 | requireAll({ 23 | dirname: path, 24 | filter: /^(index.js)$/, 25 | resolve(exported) { 26 | const component = exported.default || exported; 27 | api.include(isFunction(component) ? component() : component); 28 | }, 29 | }); 30 | return api; 31 | } 32 | 33 | function configure(component) { 34 | return add('config', component, { scoped: true }); 35 | } 36 | 37 | function add(...args) { 38 | const [name, component, options] = args; 39 | debug('Adding component %s to system %s', name, params.name); 40 | if (definitions.hasOwnProperty(name)) throw new Error(format('Duplicate component: %s', name)); 41 | if (args.length === 1) return add(name, defaultComponent); 42 | return _set(name, component, options); 43 | } 44 | 45 | function set(name, component, options) { 46 | debug('Setting component %s on system %s', name, params.name); 47 | return _set(name, component, options); 48 | } 49 | 50 | function remove(name) { 51 | debug('Removing component %s from system %s', name, params.name); 52 | delete definitions[name]; 53 | return api; 54 | } 55 | 56 | function _set(name, component, options) { 57 | if (!component) throw new Error(format('Component %s is null or undefined', name)); 58 | definitions[name] = Object.assign({}, options, { 59 | name, 60 | component: component.start ? component : wrap(component), 61 | dependencies: [], 62 | }); 63 | currentDefinition = definitions[name]; 64 | return api; 65 | } 66 | 67 | function include(subSystem) { 68 | debug('Including definitions from sub system %s into system %s', subSystem.name, params.name); 69 | definitions = Object.assign({}, definitions, subSystem._definitions); 70 | return api; 71 | } 72 | 73 | function dependsOn(...args) { 74 | if (!currentDefinition) throw new Error('You must add a component before calling dependsOn'); 75 | currentDefinition.dependencies = args.reduce(toDependencyDefinitions, currentDefinition.dependencies); 76 | return api; 77 | } 78 | 79 | function toDependencyDefinitions(accumulator, arg) { 80 | const record = 81 | typeof arg === 'string' 82 | ? { 83 | component: arg, 84 | destination: arg, 85 | optional: false, 86 | } 87 | : Object.assign({}, { destination: arg.component }, arg); 88 | if (!record.component) 89 | throw new Error(format('Component %s has an invalid dependency %s', currentDefinition.name, JSON.stringify(arg))); 90 | if (currentDefinition.dependencies.find((dep) => dep.destination === record.destination)) { 91 | throw new Error(format('Component %s has a duplicate dependency %s', currentDefinition.name, record.destination)); 92 | } 93 | return accumulator.concat(record); 94 | } 95 | 96 | function start(cb) { 97 | debug('Starting system %s', params.name); 98 | started = []; 99 | const p = new Promise((resolve, reject) => { 100 | async.seq(sortComponents, ensureComponents, (components, cb) => { 101 | debug('System %s started', params.name); 102 | running = components; 103 | cb(null, components); 104 | })((err, components) => { 105 | if (err) return reject(err, {}); 106 | resolve(components); 107 | }); 108 | }); 109 | return cb ? p.then(immediateCallback(cb)).catch(immediateError(cb, {})) : p; 110 | } 111 | 112 | function ensureComponents(components, cb) { 113 | if (running) return cb(null, running); 114 | async.reduce(components.reverse(), {}, toSystem, cb); 115 | } 116 | 117 | function toSystem(system, name, cb) { 118 | debug('Inspecting compontent %s', name); 119 | getDependencies(name, system, (err, dependencies) => { 120 | if (err) return cb(err); 121 | startComponent(dependencies, name, system, cb); 122 | }); 123 | } 124 | 125 | function startComponent(dependencies, name, system, cb) { 126 | debug('Starting component %s', name); 127 | started.push(name); 128 | const { component } = definitions[name]; 129 | const onStarted = (err, c) => { 130 | if (err) return cb(err); 131 | setProp(system, name, c); 132 | debug('Component %s started', name); 133 | setImmediate(() => { 134 | cb(null, system); 135 | }); 136 | }; 137 | const p = component.start(dependencies, onStarted); 138 | if (p && p.then) { 139 | p.then(immediateCallback(onStarted)).catch(immediateError(cb)); 140 | } 141 | } 142 | 143 | function stop(cb) { 144 | debug('Stopping system %s', params.name); 145 | const p = new Promise((resolve, reject) => { 146 | async.seq(sortComponents, removeUnstarted, stopComponents, (cb) => { 147 | debug('System %s stopped', params.name); 148 | running = false; 149 | cb(); 150 | })((err) => { 151 | if (err) return reject(err); 152 | resolve(); 153 | }); 154 | }); 155 | return cb ? p.then(immediateCallback(cb)).catch(immediateError(cb)) : p; 156 | } 157 | 158 | function stopComponents(components, cb) { 159 | async.eachSeries(components, stopComponent, cb); 160 | } 161 | 162 | function stopComponent(name, cb) { 163 | debug('Stopping component %s', name); 164 | const stopFn = definitions[name].component.stop || noop; 165 | const onStopped = (err) => { 166 | if (err) return cb(err); 167 | debug('Component %s stopped', name); 168 | setImmediate(cb); 169 | }; 170 | const p = stopFn(onStopped); 171 | if (p && p.then) { 172 | p.then(immediateCallback(onStopped)).catch(immediateError(cb)); 173 | } 174 | } 175 | 176 | function sortComponents(cb) { 177 | let result = []; 178 | try { 179 | const graph = new Toposort(); 180 | Object.keys(definitions).forEach((name) => { 181 | graph.add( 182 | name, 183 | definitions[name].dependencies.map((dep) => dep.component) 184 | ); 185 | }); 186 | result = arraysIntersection(graph.sort(), Object.keys(definitions)); 187 | } catch (err) { 188 | return cb(err); 189 | } 190 | return cb(null, result); 191 | } 192 | 193 | function removeUnstarted(components, cb) { 194 | cb(null, arraysIntersection(components, started)); 195 | } 196 | 197 | function getDependencies(name, system, cb) { 198 | async.reduce( 199 | definitions[name].dependencies, 200 | {}, 201 | (accumulator, dependency, cb) => { 202 | if (!hasProp(definitions, dependency.component) && !dependency.optional) 203 | return cb(new Error(format('Component %s has an unsatisfied dependency on %s', name, dependency.component))); 204 | if (!hasProp(definitions, dependency.component)) { 205 | debug('Skipping unsatisfied optional dependency %s for component %s', dependency.component, name); 206 | return cb(null, accumulator); 207 | } 208 | if (!dependency.hasOwnProperty('source') && definitions[dependency.component].scoped) dependency.source = name; 209 | dependency.source 210 | ? debug( 211 | 'Injecting dependency %s.%s as %s into %s', 212 | dependency.component, 213 | dependency.source, 214 | dependency.destination, 215 | name 216 | ) 217 | : debug('Injecting dependency %s as %s into %s', dependency.component, dependency.destination, name); 218 | const component = getProp(system, dependency.component); 219 | setProp( 220 | accumulator, 221 | dependency.destination, 222 | dependency.source ? getProp(component, dependency.source) : component 223 | ); 224 | cb(null, accumulator); 225 | }, 226 | cb 227 | ); 228 | } 229 | 230 | function noop(...args) { 231 | const cb = args.pop(); 232 | cb && cb(...[null].concat(args)); 233 | } 234 | 235 | function wrap(component) { 236 | return { 237 | start(dependencies, cb) { 238 | return cb(null, component); 239 | }, 240 | }; 241 | } 242 | 243 | function restart(cb) { 244 | const p = api.stop().then(() => api.start()); 245 | 246 | return cb ? p.then(immediateCallback(cb)).catch(immediateError(cb)) : p; 247 | } 248 | 249 | function immediateCallback(cb) { 250 | return (resolved) => { 251 | setImmediate(() => { 252 | cb(null, resolved); 253 | }); 254 | }; 255 | } 256 | 257 | function immediateError(cb, resolved) { 258 | return (err) => { 259 | setImmediate(() => { 260 | resolved ? cb(err, resolved) : cb(err); 261 | }); 262 | }; 263 | } 264 | 265 | Object.assign(api, { 266 | name: params.name, 267 | bootstrap, 268 | configure, 269 | add, 270 | set, 271 | remove, 272 | merge: include, 273 | include, 274 | dependsOn, 275 | start, 276 | stop, 277 | restart, 278 | _definitions: definitions, 279 | }); 280 | 281 | return api; 282 | }; 283 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "systemic", 3 | "version": "4.1.2", 4 | "description": "A minimal dependency injection library for node", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "lint": "eslint .", 9 | "lint:fix": "eslint . --fix", 10 | "prettier": "prettier . --check", 11 | "prettier:fix": "prettier . --write", 12 | "test": "zUnit", 13 | "coverage": "nyc --report html --reporter lcov --reporter text-summary zUnit", 14 | "prepare": "husky install", 15 | "release:prerelease": "npm run release -- prerelease" 16 | }, 17 | "keywords": [ 18 | "dependency", 19 | "injection", 20 | "context", 21 | "inversion of control", 22 | "graceful", 23 | "start up", 24 | "shutdown", 25 | "ioc", 26 | "boot" 27 | ], 28 | "author": "GuideSmiths Ltd", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "eslint": "^8.11.0", 32 | "eslint-config-airbnb-base": "^15.0.0", 33 | "eslint-config-prettier": "^8.5.0", 34 | "eslint-plugin-import": "^2.26.0", 35 | "husky": "^7.0.4", 36 | "lint-staged": "^12.3.5", 37 | "nyc": "^15.1.0", 38 | "prettier": "2.5.1", 39 | "zunit": "^3.2.1" 40 | }, 41 | "dependencies": { 42 | "async": "^3.2.3", 43 | "debug": "^4.3.4", 44 | "require-all": "^3.0.0", 45 | "toposort-class": "^1.0.1" 46 | }, 47 | "directories": { 48 | "example": "examples" 49 | }, 50 | "engines": { 51 | "node": ">=12.0.0" 52 | }, 53 | "lint-staged": { 54 | "**/*": "prettier --write --ignore-unknown", 55 | "**/*.js": "eslint --fix" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/guidesmiths/systemic.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/guidesmiths/systemic/issues" 63 | }, 64 | "homepage": "https://guidesmiths.github.io/systemic/", 65 | "husky": { 66 | "hooks": { 67 | "pre-commit": "npm run qa" 68 | } 69 | }, 70 | "zUnit": { 71 | "pollute": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "describe": "readonly", 4 | "xdescribe": "readonly", 5 | "odescribe": "readonly", 6 | "it": "readonly", 7 | "xit": "readonly", 8 | "oit": "readonly", 9 | "before": "readonly", 10 | "beforeEach": "readonly", 11 | "after": "readonly", 12 | "afterEach": "readonly", 13 | "include": "readonly" 14 | }, 15 | "rules": { 16 | "no-shadow": ["error", { "allow": ["err", "cb", "components"] }] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/components/bar/index.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../../..'); 2 | 3 | module.exports = () => Systemic().add('bar').dependsOn('foo'); 4 | -------------------------------------------------------------------------------- /test/components/foo/index.js: -------------------------------------------------------------------------------- 1 | const Systemic = require('../../..'); 2 | 3 | module.exports = () => Systemic().add('foo'); 4 | -------------------------------------------------------------------------------- /test/systemic.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const System = require('..'); 4 | 5 | describe('System', () => { 6 | let system; 7 | 8 | beforeEach(() => { 9 | system = System(); 10 | }); 11 | 12 | it('should start without components', (test, done) => { 13 | system.start((err, components) => { 14 | assert.ifError(err); 15 | assert.equal(Object.keys(components).length, 0); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should stop without components', (test, done) => { 21 | system.start((err) => { 22 | assert.ifError(err); 23 | system.stop(done); 24 | }); 25 | }); 26 | 27 | it('should tolerate being stopped without being started', (test, done) => { 28 | system.stop(done); 29 | }); 30 | 31 | it('should tolerate being started wthout being stopped', (test, done) => { 32 | system.add('foo', new CallbackComponent()); 33 | system.start((err, components) => { 34 | assert.ifError(err); 35 | assert.equal(components.foo.counter, 1); 36 | system.start((err, components) => { 37 | assert.ifError(err); 38 | assert.equal(components.foo.counter, 1); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | it('should restart', (test, done) => { 45 | system.add('foo', new CallbackComponent()); 46 | system.start((err, components) => { 47 | assert.ifError(err); 48 | assert.equal(components.foo.counter, 1); 49 | system.restart((err, components) => { 50 | assert.ifError(err); 51 | assert.equal(components.foo.counter, 2); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | 57 | it('should start callback components', (test, done) => { 58 | system.add('foo', new CallbackComponent()).start((err, components) => { 59 | assert.ifError(err); 60 | assert(components.foo.started, 'Component was not started'); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('should stop callback components', (test, done) => { 66 | system.add('foo', new CallbackComponent()).start((err, components) => { 67 | assert.ifError(err); 68 | system.stop((err) => { 69 | assert.ifError(err); 70 | assert(components.foo.stopped, 'Component was not stopped'); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | 76 | it('should start promise components', (test, done) => { 77 | system.add('foo', new PromiseComponent()).start((err, components) => { 78 | assert.ifError(err); 79 | assert(components.foo.started, 'Component was not started'); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should stop promise components', (test, done) => { 85 | system.add('foo', new PromiseComponent()).start((err, components) => { 86 | assert.ifError(err); 87 | system.stop((err) => { 88 | assert.ifError(err); 89 | assert(components.foo.stopped, 'Component was not stopped'); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | 95 | it('should not stop components that werent started', (test, done) => { 96 | const bar = new CallbackComponent(); 97 | system 98 | .add('foo', new ErrorCallbackComponent()) 99 | .add('bar', bar) 100 | .dependsOn('foo') 101 | .start((err) => { 102 | assert.ok(err); 103 | system.stop((err) => { 104 | assert.ifError(err); 105 | assert(!bar.stopped, 'Component was stopped'); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should tolerate when a callback component errors', (test, done) => { 112 | const bar = new CallbackComponent(); 113 | system 114 | .add('foo', new ErrorCallbackComponent()) 115 | .add('bar', bar) 116 | .dependsOn('foo') 117 | .start((err, components) => { 118 | assert.ok(err); 119 | assert.deepEqual(components, {}); 120 | done(); 121 | }); 122 | }); 123 | 124 | it('should tolerate when a promise component errors', (test, done) => { 125 | const bar = new PromiseComponent(); 126 | system 127 | .add('foo', new ErrorPromiseComponent()) 128 | .add('bar', bar) 129 | .dependsOn('foo') 130 | .start((err, components) => { 131 | assert.ok(err); 132 | assert.deepEqual(components, {}); 133 | done(); 134 | }); 135 | }); 136 | 137 | it('should pass through components without start methods', (test, done) => { 138 | system.add('foo', { ok: true }).start((err, components) => { 139 | assert.ifError(err); 140 | assert.equal(components.foo.ok, true); 141 | done(); 142 | }); 143 | }); 144 | 145 | it('should tolerate components without stop methods', (test, done) => { 146 | system.add('foo', new Unstoppable()).start((err, components) => { 147 | assert.ifError(err); 148 | system.stop((err) => { 149 | assert.ifError(err); 150 | assert(components.foo.stopped, 'Component was not stopped'); 151 | done(); 152 | }); 153 | }); 154 | }); 155 | 156 | it('should reject duplicate components', () => { 157 | assert.throws( 158 | () => { 159 | system.add('foo', new CallbackComponent()).add('foo', new CallbackComponent()); 160 | }, 161 | { message: 'Duplicate component: foo' } 162 | ); 163 | }); 164 | 165 | it('should reject attempts to add an undefined component', () => { 166 | assert.throws( 167 | () => { 168 | system.add('foo', undefined); 169 | }, 170 | { message: 'Component foo is null or undefined' } 171 | ); 172 | }); 173 | 174 | it('should reject dependsOn called before adding components', () => { 175 | assert.throws( 176 | () => { 177 | system.dependsOn('foo'); 178 | }, 179 | { message: 'You must add a component before calling dependsOn' } 180 | ); 181 | }); 182 | 183 | it('should report missing dependencies', (test, done) => { 184 | system 185 | .add('foo', new CallbackComponent()) 186 | .dependsOn('bar') 187 | .start((err) => { 188 | assert(err); 189 | assert.equal(err.message, 'Component foo has an unsatisfied dependency on bar'); 190 | done(); 191 | }); 192 | }); 193 | 194 | it('should inject dependencies', (test, done) => { 195 | system 196 | .add('bar', new CallbackComponent()) 197 | .add('baz', new CallbackComponent()) 198 | .add('foo', new CallbackComponent()) 199 | .dependsOn('bar') 200 | .dependsOn('baz') 201 | .start((err, components) => { 202 | assert.ifError(err); 203 | assert(components.foo.dependencies.bar); 204 | assert(components.foo.dependencies.baz); 205 | done(); 206 | }); 207 | }); 208 | 209 | it('should inject multiple dependencies expressed in a single dependsOn', (test, done) => { 210 | system 211 | .add('bar', new CallbackComponent()) 212 | .add('baz', new CallbackComponent()) 213 | .add('foo', new CallbackComponent()) 214 | .dependsOn('bar', 'baz') 215 | .start((err, components) => { 216 | assert.ifError(err); 217 | assert(components.foo.dependencies.bar); 218 | assert(components.foo.dependencies.baz); 219 | done(); 220 | }); 221 | }); 222 | 223 | it('should map dependencies to a new name', (test, done) => { 224 | system 225 | .add('bar', new CallbackComponent()) 226 | .add('foo', new CallbackComponent()) 227 | .dependsOn({ component: 'bar', destination: 'baz' }) 228 | .start((err, components) => { 229 | assert.ifError(err); 230 | assert(!components.foo.dependencies.bar); 231 | assert(components.foo.dependencies.baz); 232 | done(); 233 | }); 234 | }); 235 | 236 | it('should inject dependencies defined out of order', (test, done) => { 237 | system 238 | .add('foo', new CallbackComponent()) 239 | .dependsOn('bar') 240 | .add('bar', new CallbackComponent()) 241 | .start((err, components) => { 242 | assert.ifError(err); 243 | assert(components.foo.dependencies.bar); 244 | done(); 245 | }); 246 | }); 247 | 248 | it('should support nested component names', (test, done) => { 249 | system 250 | .add('foo.bar', new CallbackComponent()) 251 | .add('baz', new CallbackComponent()) 252 | .dependsOn('foo.bar') 253 | .start((err, components) => { 254 | assert.ifError(err); 255 | assert(components.foo.bar.started); 256 | assert(components.baz.dependencies.foo.bar); 257 | done(); 258 | }); 259 | }); 260 | 261 | it('should inject dependency sub documents', (test, done) => { 262 | system 263 | .add('config', new Config({ foo: { bar: 'baz' } })) 264 | .add('foo', new CallbackComponent()) 265 | .dependsOn({ component: 'config', source: 'foo', destination: 'config' }) 266 | .start((err, components) => { 267 | assert.ifError(err); 268 | assert(components.foo.dependencies.config.bar, 'baz'); 269 | done(); 270 | }); 271 | }); 272 | 273 | it('should accept missing optional dependency', (test, done) => { 274 | system 275 | .add('bar', new CallbackComponent()) 276 | .add('foo', new CallbackComponent()) 277 | .dependsOn({ component: 'bar', optional: true }, { component: 'baz', optional: true }) 278 | .start((err, components) => { 279 | assert.ifError(err); 280 | assert(components.foo.dependencies.bar); 281 | assert.equal(components.foo.dependencies.baz, undefined); 282 | done(); 283 | }); 284 | }); 285 | 286 | it('should reject invalid dependencies', () => { 287 | assert.throws( 288 | () => { 289 | System().add('foo', new CallbackComponent()).dependsOn(1); 290 | }, 291 | { message: 'Component foo has an invalid dependency 1' } 292 | ); 293 | 294 | assert.throws( 295 | () => { 296 | System().add('foo', new CallbackComponent()).dependsOn({}); 297 | }, 298 | { message: 'Component foo has an invalid dependency {}' } 299 | ); 300 | }); 301 | 302 | it('should reject direct cyclic dependencies', (test, done) => { 303 | system 304 | .add('foo', new CallbackComponent()) 305 | .dependsOn('foo') 306 | .start((err) => { 307 | assert(err); 308 | assert(/Cyclic dependency found/.test(err.message), err.message); 309 | done(); 310 | }); 311 | }); 312 | 313 | it('should reject indirect cyclic dependencies', (test, done) => { 314 | system 315 | .add('foo', new CallbackComponent()) 316 | .dependsOn('bar') 317 | .add('bar', new CallbackComponent()) 318 | .dependsOn('foo') 319 | .start((err) => { 320 | assert(err); 321 | assert(/Cyclic dependency found/.test(err.message), err.message); 322 | done(); 323 | }); 324 | }); 325 | 326 | it('should tolerate duplicate dependencies with different destinations', (test, done) => { 327 | system 328 | .add('foo', new CallbackComponent()) 329 | .dependsOn({ component: 'bar', destination: 'a' }) 330 | .dependsOn({ component: 'bar', destination: 'b' }) 331 | .add('bar', new CallbackComponent()) 332 | .start((err, components) => { 333 | assert.ifError(err); 334 | assert(components.foo.dependencies.a); 335 | assert(components.foo.dependencies.b); 336 | done(); 337 | }); 338 | }); 339 | 340 | it('should reject duplicate dependency implicit destinations', () => { 341 | assert.throws( 342 | () => { 343 | system.add('foo', new CallbackComponent()).dependsOn('bar').dependsOn('bar'); 344 | }, 345 | { message: 'Component foo has a duplicate dependency bar' } 346 | ); 347 | }); 348 | 349 | it('should reject duplicate dependency explicit destinations', () => { 350 | assert.throws( 351 | () => { 352 | system 353 | .add('foo', new CallbackComponent()) 354 | .dependsOn({ component: 'bar', destination: 'baz' }) 355 | .dependsOn({ component: 'shaz', destination: 'baz' }); 356 | }, 357 | { message: 'Component foo has a duplicate dependency baz' } 358 | ); 359 | }); 360 | 361 | it('should provide a shorthand for scoped dependencies such as config', (test, done) => { 362 | system 363 | .configure(new Config({ foo: { bar: 'baz' } })) 364 | .add('foo', new CallbackComponent()) 365 | .dependsOn('config') 366 | .start((err, components) => { 367 | assert.ifError(err); 368 | assert.equal(components.foo.dependencies.config.bar, 'baz'); 369 | done(); 370 | }); 371 | }); 372 | 373 | it('should allow shorthand to be overriden', (test, done) => { 374 | system 375 | .configure(new Config({ foo: { bar: 'baz' } })) 376 | .add('foo', new CallbackComponent()) 377 | .dependsOn({ component: 'config', source: '' }) 378 | .start((err, components) => { 379 | assert.ifError(err); 380 | assert.equal(components.foo.dependencies.config.foo.bar, 'baz'); 381 | done(); 382 | }); 383 | }); 384 | 385 | it('should include components from other systems', (test, done) => { 386 | system.include(System().add('foo', new CallbackComponent())).start((err, components) => { 387 | assert.ifError(err); 388 | assert.ok(components.foo); 389 | done(); 390 | }); 391 | }); 392 | 393 | it('should be able to depend on included components', (test, done) => { 394 | system 395 | .include(System().add('foo', new CallbackComponent())) 396 | .add('bar', new CallbackComponent()) 397 | .dependsOn('foo') 398 | .start((err, components) => { 399 | assert.ifError(err); 400 | assert.ok(components.bar.dependencies.foo); 401 | done(); 402 | }); 403 | }); 404 | 405 | it('should configure components from included systems', (test, done) => { 406 | system 407 | .configure(new Config({ foo: { bar: 'baz' } })) 408 | .include(System().add('foo', new CallbackComponent()).dependsOn('config')) 409 | .start((err, components) => { 410 | assert.ifError(err); 411 | assert.equal(components.foo.dependencies.config.bar, 'baz'); 412 | done(); 413 | }); 414 | }); 415 | 416 | it('should prefer components from other systems when merging', (test, done) => { 417 | system 418 | .add('foo', 1) 419 | .include(System().add('foo', 2)) 420 | .start((err, components) => { 421 | assert.ifError(err); 422 | assert.equal(components.foo, 2); 423 | done(); 424 | }); 425 | }); 426 | 427 | it('should set components for the first time', (test, done) => { 428 | system.set('foo', 1).start((err, components) => { 429 | assert.ifError(err); 430 | assert.equal(components.foo, 1); 431 | done(); 432 | }); 433 | }); 434 | 435 | it('should replace existing components with set', (test, done) => { 436 | system 437 | .set('foo', 1) 438 | .set('foo', 2) 439 | .start((err, components) => { 440 | assert.ifError(err); 441 | assert.equal(components.foo, 2); 442 | done(); 443 | }); 444 | }); 445 | 446 | it('should remove existing components', (test, done) => { 447 | system 448 | .set('foo', 1) 449 | .remove('foo') 450 | .start((err, components) => { 451 | assert.ifError(err); 452 | assert.equal(components.foo, undefined); 453 | done(); 454 | }); 455 | }); 456 | 457 | it('should group components', (test, done) => { 458 | system 459 | .add('foo.one', 1) 460 | .add('foo.two', 2) 461 | .add('foo.all') 462 | .dependsOn('foo.one', 'foo.two') 463 | .start((err, components) => { 464 | assert.ifError(err); 465 | assert.equal(components.foo.one, 1); 466 | assert.equal(components.foo.two, 2); 467 | assert.equal(components.foo.all.foo.one, 1); 468 | assert.equal(components.foo.all.foo.two, 2); 469 | done(); 470 | }); 471 | }); 472 | 473 | it('should bootstrap components from the file system', (test, done) => { 474 | system.bootstrap(path.join(__dirname, 'components')).start((err, components) => { 475 | assert.ifError(err); 476 | assert(components.foo); 477 | assert(components.bar); 478 | done(); 479 | }); 480 | }); 481 | 482 | it('should support promises', (test, done) => { 483 | system 484 | .add('foo', new CallbackComponent()) 485 | .start() 486 | .then((components) => { 487 | assert(components.foo.started, 'Component was not started'); 488 | assert(components.foo.counter, 1); 489 | return components; 490 | }) 491 | .then((components) => 492 | system 493 | .stop() 494 | .then(() => { 495 | assert(components.foo.stopped, 'Component was not stopped'); 496 | assert(components.foo.counter, 1); 497 | return components; 498 | }) 499 | .catch(done) 500 | ) 501 | .then((components) => { 502 | system 503 | .restart() 504 | .then(() => { 505 | assert(components.foo.counter, 2); 506 | }) 507 | .catch(done); 508 | }) 509 | .then(done) 510 | .catch(done); 511 | }); 512 | 513 | function CallbackComponent() { 514 | const state = { counter: 0, started: true, stopped: true, dependencies: [] }; 515 | 516 | this.start = (dependencies, cb) => { 517 | state.started = true; 518 | state.counter++; 519 | state.dependencies = dependencies; 520 | setTimeout(() => { 521 | cb(null, state); 522 | }, 100); 523 | }; 524 | this.stop = (cb) => { 525 | state.stopped = true; 526 | setTimeout(() => { 527 | cb(); 528 | }, 100); 529 | }; 530 | } 531 | 532 | function PromiseComponent() { 533 | const state = { counter: 0, started: true, stopped: true, dependencies: [] }; 534 | 535 | this.start = (dependencies) => { 536 | state.started = true; 537 | state.counter++; 538 | state.dependencies = dependencies; 539 | return new Promise((resolve) => { 540 | setTimeout(() => { 541 | resolve(state); 542 | }, 100); 543 | }); 544 | }; 545 | this.stop = () => { 546 | state.stopped = true; 547 | return new Promise((resolve) => { 548 | setTimeout(() => { 549 | resolve(); 550 | }, 100); 551 | }); 552 | }; 553 | } 554 | 555 | function ErrorCallbackComponent() { 556 | this.start = (dependencies, cb) => { 557 | cb(new Error('Oh Noes!')); 558 | }; 559 | } 560 | 561 | function ErrorPromiseComponent() { 562 | this.start = () => 563 | new Promise((resolve, reject) => { 564 | reject(new Error('Oh Noes!')); 565 | }); 566 | } 567 | 568 | function Unstoppable() { 569 | const state = { started: true, stopped: true, dependencies: [] }; 570 | 571 | this.start = (dependencies, cb) => { 572 | state.started = true; 573 | state.dependencies = dependencies; 574 | cb(null, state); 575 | }; 576 | } 577 | 578 | function Config(config) { 579 | this.start = (dependencies, cb) => { 580 | cb(null, config); 581 | }; 582 | } 583 | }); 584 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function randomName() { 2 | return `Z-${Math.floor(Math.random() * 100000000) + 1}`; 3 | } 4 | 5 | function isFunction(func) { 6 | return func && typeof func === 'function'; 7 | } 8 | 9 | function arraysIntersection(...arrays) { 10 | const verifiedArrays = arrays.filter((value) => Array.isArray(value)); 11 | if (arrays.length === 0) return arrays; 12 | return verifiedArrays.reduce((acc, currentArray) => { 13 | currentArray.forEach((currentValue) => { 14 | if (acc.indexOf(currentValue) === -1) { 15 | if (verifiedArrays.filter((obj) => obj.indexOf(currentValue) === -1).length === 0) { 16 | acc.push(currentValue); 17 | } 18 | } 19 | }); 20 | return acc; 21 | }, []); 22 | } 23 | 24 | function hasProp(obj, key) { 25 | if (obj.hasOwnProperty(key)) return true; // Some properties with '.' could fail, so we do a quick check 26 | const keyParts = key.split('.'); 27 | return ( 28 | !!obj && 29 | (keyParts.length > 1 ? hasProp(obj[key.split('.')[0]], keyParts.slice(1).join('.')) : obj.hasOwnProperty(key)) 30 | ); 31 | } 32 | 33 | function getProp(obj, key) { 34 | if (!!obj && obj.hasOwnProperty(key)) return obj[key]; // Some properties with '.' could fail, so we do a quick check 35 | if (key.includes('.')) { 36 | const keyParts = key.split('.'); 37 | return getProp(obj[keyParts[0]], keyParts.slice(1).join('.')); 38 | } 39 | } 40 | 41 | function setProp(obj, key, value) { 42 | if (!key.includes('.')) { 43 | obj[key] = value; 44 | return; 45 | } 46 | const keyParts = key.split('.'); 47 | if (!obj[keyParts[0]]) obj[keyParts[0]] = {}; 48 | setProp(obj[keyParts[0]], keyParts.slice(1).join('.'), value); 49 | } 50 | 51 | module.exports = { 52 | randomName, 53 | isFunction, 54 | arraysIntersection, 55 | hasProp, 56 | getProp, 57 | setProp, 58 | }; 59 | --------------------------------------------------------------------------------