├── .github └── workflows │ └── CI.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.js ├── lib ├── annotation.js ├── index.d.ts ├── index.js ├── injector.js └── util.js ├── package-lock.json ├── package.json ├── renovate.json ├── rollup.config.js ├── test ├── annotation.spec.js ├── injector.spec.js └── integration │ ├── node.spec.cjs │ └── ts.spec.ts └── tsconfig.json /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | strategy: 8 | matrix: 9 | node-version: [18, 20, 22] 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Use Node.js ${{matrix.node-version}} 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{matrix.node-version}} 18 | cache: 'npm' 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Build 22 | run: npm run all 23 | - name: Upload Coverage 24 | if: matrix.node-version == 20 25 | uses: codecov/codecov-action@v4 26 | with: 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | dist -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to [didi](https://github.com/nikku/didi) are documented here. We use [semantic versioning](http://semver.org/) for releases. 4 | 5 | ## Unreleased 6 | 7 | ___Note:__ Yet to be released changes appear here._ 8 | 9 | ## 10.2.2 10 | 11 | _Reverts `v10.2.1`._ 12 | 13 | * `FIX`: restore `main` entry ([#36](https://github.com/nikku/didi/issues/36)) 14 | 15 | ## 10.2.1 16 | 17 | * `FIX`: remove broken `main` entry 18 | 19 | ## 10.2.0 20 | 21 | * `FEAT`: add ability provide wellknown services ([#33](https://github.com/nikku/didi/pull/33)) 22 | 23 | ## 10.1.0 24 | 25 | * `FEAT`: add `exports` field 26 | * `CHORE`: internal typing improvements 27 | 28 | ## 10.0.1 29 | 30 | * `FIX`: relax `Injector#get` type definitions 31 | 32 | ## 10.0.0 33 | 34 | * `FEAT`: turn into module 35 | * `CHORE`: require `Node >= 16` 36 | * `DOCS`: significantly improve typescript documentation 37 | 38 | ## 9.0.2 39 | 40 | * `FIX`: correct `Injector#instantiate` type definitions 41 | 42 | ## 9.0.1 43 | 44 | * `FIX`: correct `Injector#invoke` type definitions ([#20](https://github.com/nikku/didi/issues/20)) 45 | 46 | ## 9.0.0 47 | 48 | _Migrates the code base to ES2018._ 49 | 50 | * `FIX`: do not alter input in `annotate` 51 | * `CHORE`: migrate codebase to ES2018 52 | * `CHORE`: drop `UMD` prebuilt distribution 53 | 54 | ## 8.0.2 55 | 56 | * `FIX`: correct dependency detection for annonymous classes ([#17](https://github.com/nikku/didi/issues/17)) 57 | 58 | ## 8.0.1 59 | 60 | * `CHORE`: simplify initialization logic 61 | * `FIX`: drop usage of `AggregateError` due to poor inspection support 62 | 63 | ## 8.0.0 64 | 65 | * `FEAT`: separate bootstrapping and initialization 66 | 67 | ### Breaking Changes 68 | 69 | * Injector must be manually initialized via `Injector#init()` 70 | 71 | ## 7.0.1 72 | 73 | * `FIX`: make core `ES5`, again 74 | 75 | ## 7.0.0 76 | 77 | * `FEAT`: add support for module dependencies and intialization ([#13](https://github.com/nikku/didi/pull/13)) 78 | * `FEAT`: retain stack traces when throwing errors ([`999b821b`](https://github.com/nikku/didi/commit/999b821b2f630a8d74fade566281875ef628a6d3)) 79 | * `FIX`: parse single arg lambda shorthand ([`d53f6310`](https://github.com/nikku/didi/commit/d53f631023daa547ae9eb17dbbd5abae08573051)) 80 | * `CHORE`: remove `Module` from public API 81 | * `CHORE`: drop `Node@10` support 82 | 83 | ### Breaking Changes 84 | 85 | * Removed `Module` export. Use documented `ModuleDeclaration` to define a didi module 86 | * Improved `ModuleDeclaration` typings to clearly reflect API used 87 | * `__init__` and `__depends__` are now part of the built-in module exports accounted for ([#13](https://github.com/nikku/didi/pull/13)) 88 | 89 | ## 6.1.0 90 | 91 | * `FEAT`: move to pre-built type definitions 92 | 93 | ## 6.0.0 94 | 95 | * `FEAT`: add type definitions 96 | 97 | ## 5.2.1 98 | 99 | * `FIX`: detect arguments in (async) closures, too 100 | 101 | ## 5.2.0 102 | 103 | * `CHORE`: expose `parseAnnotations` 104 | 105 | ## 5.1.0 106 | 107 | * `DOCS`: improve 108 | 109 | ## 5.0.1 110 | 111 | * `FIX`: remove async injector from main bundle, will be released seperately 112 | 113 | ## 5.0.0 114 | 115 | * `FEAT`: add async injector :tada: 116 | * `CHORE`: no-babel build 117 | * `CHORE`: minify using `terser` 118 | 119 | ## 4.0.0 120 | 121 | ### Breaking Changes 122 | 123 | * `FIX`: remove browser field again; it confuses modern module bundlers. This partially reverts `v3.1.0` 124 | 125 | ## 3.2.0 126 | 127 | * `CHORE`: mark library as side-effect free via `sideEffects: false` 128 | 129 | ## 3.1.0 130 | 131 | * `CHORE`: add `browser` field 132 | 133 | ## 3.0.0 134 | 135 | ### Breaking Changes 136 | 137 | * `CHORE`: don't expose `lib` folder; library consumers should use API exposed via bundled artifacts 138 | 139 | ### Other Improvements 140 | 141 | * `FEAT`: allow local overrides on `Injector#invoke` 142 | * `CHORE`: babelify all produced bundles 143 | 144 | ## 2.0.1 145 | 146 | * `FIX`: make injection work on constructor less ES2015 `class` 147 | 148 | ## 2.0.0 149 | 150 | * `FEAT`: support ES2015 `class` as injection targets, too 151 | * `FEAT`: always instantiate `type` using `new` 152 | * `CHORE`: bundle `es`, `cjs` and `umd` distributions via rollup 153 | 154 | ## 1.0.1 - 1.0.3 155 | 156 | * `FIX`: properly include resources in bundle 157 | 158 | ## 1.0.0 159 | 160 | * `FEAT`: port to ES2015 161 | 162 | ## ... 163 | 164 | Check `git log` for earlier history. 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2013 Vojta Jína. 4 | Copyright (C) 2015-present Nico Rehwaldt. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | 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, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `didi` 2 | 3 | [![CI](https://github.com/nikku/didi/actions/workflows/CI.yml/badge.svg)](https://github.com/nikku/didi/actions/workflows/CI.yml) 4 | 5 | A tiny [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) container for JavaScript. 6 | 7 | 8 | ## About 9 | 10 | Using [`didi`](https://github.com/nikku/didi) you follow the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) / [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) pattern, decoupling component declaration from instantiation. Once declared, `didi` instantiates components as needed, transitively resolves their dependencies, and caches instances for re-use. 11 | 12 | 13 | ## Example 14 | 15 | ```ts 16 | import { Injector } from 'didi'; 17 | 18 | function Car(engine) { 19 | this.start = function() { 20 | engine.start(); 21 | }; 22 | } 23 | 24 | function createPetrolEngine(power) { 25 | return { 26 | start: function() { 27 | console.log('Starting engine with ' + power + 'hp'); 28 | } 29 | }; 30 | } 31 | 32 | // define a (didi) module - it declares available 33 | // components by name and specifies how these are provided 34 | const carModule = { 35 | 36 | // asked for 'car', the injector will call new Car(...) to produce it 37 | 'car': ['type', Car], 38 | 39 | // asked for 'engine', the injector will call createPetrolEngine(...) to produce it 40 | 'engine': ['factory', createPetrolEngine], 41 | 42 | // asked for 'power', the injector will give it number 1184 43 | 'power': ['value', 1184] // probably Bugatti Veyron 44 | }; 45 | 46 | // instantiate an injector with a set of (didi) modules 47 | const injector = new Injector([ 48 | carModule 49 | ]); 50 | 51 | // use the injector API to retrieve components 52 | injector.get('car').start(); 53 | 54 | // alternatively invoke a function, injecting the arguments 55 | injector.invoke(function(car) { 56 | console.log('started', car); 57 | }); 58 | 59 | // if you work with a TypeScript code base, retrieve 60 | // a typed instance of a component 61 | const car: Car = injector.get('car'); 62 | 63 | car.start(); 64 | ``` 65 | 66 | For real-world examples, check out [Karma](https://github.com/karma-runner/karma), [diagram-js](https://github.com/bpmn-io/diagram-js) or [Wuffle](https://github.com/nikku/wuffle/tree/main/packages/app)—libraries that heavily use dependency injection at their core. You can also check out [the tests](https://github.com/nikku/didi/blob/master/test/injector.spec.js) to learn about all supported use cases. 67 | 68 | 69 | ## Usage 70 | 71 | Learn how to [declare](#declaring-components), [inject](#injecting-components) and [initialize](#initializing-components) your components. 72 | 73 | 74 | ### Declaring Components 75 | 76 | By declaring a component as part of a `didi` module, you make it available to other components. 77 | 78 | #### `type(token, Constructor)` 79 | 80 | `Constructor` will be called with `new` operator to produce the instance: 81 | 82 | ```js 83 | const module = { 84 | 'engine': ['type', DieselEngine] 85 | }; 86 | ``` 87 | 88 | #### `factory(token, factoryFn)` 89 | 90 | The injector produces the instance by calling `factoryFn` without any context. It uses the factory's return value: 91 | 92 | ```js 93 | const module = { 94 | 'engine': ['factory', createDieselEngine] 95 | }; 96 | ``` 97 | 98 | #### `value(token, value)` 99 | 100 | Register a static value: 101 | 102 | ```js 103 | const module = { 104 | 'power': ['value', 1184] 105 | }; 106 | ``` 107 | 108 | 109 | ### Injecting Components 110 | 111 | The injector looks up dependencies based on explicit annotations, comments, or function argument names. 112 | 113 | #### Argument Names 114 | 115 | If no further details are provided the injector parses dependency names from function arguments: 116 | 117 | ```js 118 | function Car(engine, license) { 119 | // will inject components bound to 'engine' and 'license' 120 | } 121 | ``` 122 | 123 | #### Function Comments 124 | 125 | You can use comments to encode names: 126 | 127 | ```js 128 | function Car(/* engine */ e, /* x._weird */ x) { 129 | // will inject components bound to 'engine' and 'x._weird' 130 | } 131 | ``` 132 | 133 | #### `$inject` Annotation 134 | 135 | You can use a static `$inject` annotation to declare dependencies in a minification safe manner: 136 | 137 | ```js 138 | function Car(e, license) { 139 | // will inject components bound to 'engine' and 'license' 140 | } 141 | 142 | Car.$inject = [ 'engine', 'license' ]; 143 | ``` 144 | 145 | #### Array Notation 146 | 147 | You can also the minification save array notation known from [AngularJS][AngularJS]: 148 | 149 | ```js 150 | const Car = [ 'engine', 'trunk', function(e, t) { 151 | // will inject components bound to 'engine' and 'trunk' 152 | }]; 153 | ``` 154 | 155 | #### Partial Injection 156 | 157 | Sometimes it is helpful to inject only a specific property of some object: 158 | 159 | ```js 160 | function Engine(/* config.engine.power */ power) { 161 | // will inject 1184 (config.engine.power), 162 | // assuming there is no direct binding for 'config.engine.power' token 163 | } 164 | 165 | const engineModule = { 166 | 'config': ['value', {engine: {power: 1184}, other : {}}] 167 | }; 168 | ``` 169 | 170 | 171 | ### Initializing Components 172 | 173 | Modules can use an `__init__` hook to declare components that shall eagerly load or functions to be invoked, i.e., trigger side-effects during initialization: 174 | 175 | ```javascript 176 | import { Injector } from 'didi'; 177 | 178 | function HifiComponent(events) { 179 | events.on('toggleHifi', this.toggle.bind(this)); 180 | 181 | this.toggle = function(mode) { 182 | console.log(`Toggled Hifi ${mode ? 'ON' : 'OFF'}`); 183 | }; 184 | } 185 | 186 | const injector = new Injector([ 187 | { 188 | __init__: [ 'hifiComponent' ], 189 | hifiComponent: [ 'type', HifiComponent ] 190 | }, 191 | ... 192 | ]); 193 | 194 | // initializes all modules as defined 195 | injector.init(); 196 | ``` 197 | 198 | 199 | ### Overriding Components 200 | 201 | You can override components by name. That can be beneficial for testing but also for customizing: 202 | 203 | ```js 204 | import { Injector } from 'didi'; 205 | 206 | import coreModule from './core'; 207 | import HttpBackend from './test/mocks'; 208 | 209 | const injector = new Injector([ 210 | coreModule, 211 | { 212 | // overrides already declared `httpBackend` 213 | httpBackend: [ 'type', HttpBackend ] 214 | } 215 | ]); 216 | ``` 217 | 218 | 219 | ### Type-safety 220 | 221 | [`didi`](https://github.com/nikku/didi) ships type declarations that allow you to use it in a type safe manner. 222 | 223 | #### Explicit Typing 224 | 225 | Pass a type attribute to `Injector#get` to retrieve a service as a known type: 226 | 227 | ```typescript 228 | const hifiComponent = injector.get('hifiComponent'); 229 | 230 | // typed as 231 | hifiComponent.toggle(); 232 | ``` 233 | 234 | #### Implicit Typing 235 | 236 | Configure the `Injector` through a service map and automatically cast services 237 | to known types: 238 | 239 | ```typescript 240 | type ServiceMap = { 241 | 'hifiComponent': HifiComponent 242 | }; 243 | 244 | const injector = new Injector(...); 245 | 246 | const hifiComponent = injector.get('hifiComponent'); 247 | // typed as 248 | ``` 249 | 250 | 251 | ## Credits 252 | 253 | This library builds on top of the (now unmaintained) [node-di][node-di] library. `didi` is a maintained fork that adds support for ES6, the minification safe array notation, and other features. 254 | 255 | 256 | ## Differences to [node-di][node-di] 257 | 258 | - supports array notation 259 | - supports [ES2015](http://babeljs.io/learn-es2015/) 260 | - bundles type definitions 261 | - module initialization + module dependencies 262 | 263 | 264 | ## License 265 | 266 | MIT 267 | 268 | 269 | [AngularJS]: http://angularjs.org/ 270 | [node-di]: https://github.com/vojtajina/node-di 271 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import bpmnIoPlugin from 'eslint-plugin-bpmn-io'; 2 | 3 | import typescriptPlugin from 'typescript-eslint'; 4 | 5 | const files = { 6 | build: [ 7 | '*.js' 8 | ], 9 | test: [ 10 | 'test/**/*.js', 11 | 'test/**/*.ts' 12 | ], 13 | node_test: [ 14 | 'test/**/*.cjs' 15 | ], 16 | ignored: [ 17 | 'dist', 18 | 'coverage', 19 | '.nyc_output' 20 | ] 21 | }; 22 | 23 | export default [ 24 | { 25 | 'ignores': files.ignored 26 | }, 27 | ...bpmnIoPlugin.configs.recommended.map(config => { 28 | 29 | return { 30 | ...config, 31 | ignores: [ 32 | ...files.build, 33 | ...files.node_test 34 | ] 35 | }; 36 | }), 37 | ...bpmnIoPlugin.configs.node.map(config => { 38 | 39 | return { 40 | ...config, 41 | files: [ 42 | ...files.build, 43 | ...files.node_test 44 | ] 45 | }; 46 | }), 47 | ...bpmnIoPlugin.configs.mocha.map(config => { 48 | 49 | return { 50 | ...config, 51 | files: [ 52 | ...files.test, 53 | ...files.node_test 54 | ] 55 | }; 56 | }), 57 | ...typescriptPlugin.configs.recommended, 58 | { 59 | rules: { 60 | '@typescript-eslint/no-explicit-any': [ 'warn', { 'ignoreRestArgs': true } ], 61 | }, 62 | languageOptions: { 63 | parserOptions: { 64 | tsconfigRootDir: import.meta.dirname 65 | } 66 | } 67 | }, 68 | { 69 | rules: { 70 | '@typescript-eslint/no-unused-vars': 'off', 71 | '@typescript-eslint/no-empty-function': 'off', 72 | '@typescript-eslint/no-var-requires': 'off', 73 | '@typescript-eslint/ban-ts-comment': 'off', 74 | '@typescript-eslint/no-unused-expressions': 'off' 75 | }, 76 | files: [ 77 | ...files.test, 78 | ...files.node_test 79 | ] 80 | }, 81 | { 82 | rules: { 83 | '@typescript-eslint/no-require-imports': 'off' 84 | }, 85 | files: [ 86 | ...files.node_test 87 | ] 88 | } 89 | ]; -------------------------------------------------------------------------------- /lib/annotation.js: -------------------------------------------------------------------------------- 1 | import { 2 | isArray, 3 | isClass 4 | } from './util.js'; 5 | 6 | /** 7 | * @typedef {import('./index.js').InjectAnnotated } InjectAnnotated 8 | */ 9 | 10 | /** 11 | * @template T 12 | * 13 | * @params {[...string[], T] | ...string[], T} args 14 | * 15 | * @return {T & InjectAnnotated} 16 | */ 17 | export function annotate(...args) { 18 | 19 | if (args.length === 1 && isArray(args[0])) { 20 | args = args[0]; 21 | } 22 | 23 | args = [ ...args ]; 24 | 25 | const fn = args.pop(); 26 | 27 | fn.$inject = args; 28 | 29 | return fn; 30 | } 31 | 32 | 33 | // Current limitations: 34 | // - can't put into "function arg" comments 35 | // function /* (no parenthesis like this) */ (){} 36 | // function abc( /* xx (no parenthesis like this) */ a, b) {} 37 | // 38 | // Just put the comment before function or inside: 39 | // /* (((this is fine))) */ function(a, b) {} 40 | // function abc(a) { /* (((this is fine))) */} 41 | // 42 | // - can't reliably auto-annotate constructor; we'll match the 43 | // first constructor(...) pattern found which may be the one 44 | // of a nested class, too. 45 | 46 | const CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m; 47 | const FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m; 48 | const FN_ARG = /\/\*([^*]*)\*\//m; 49 | 50 | /** 51 | * @param {unknown} fn 52 | * 53 | * @return {string[]} 54 | */ 55 | export function parseAnnotations(fn) { 56 | 57 | if (typeof fn !== 'function') { 58 | throw new Error(`Cannot annotate "${fn}". Expected a function!`); 59 | } 60 | 61 | const match = fn.toString().match(isClass(fn) ? CONSTRUCTOR_ARGS : FN_ARGS); 62 | 63 | // may parse class without constructor 64 | if (!match) { 65 | return []; 66 | } 67 | 68 | const args = match[1] || match[2]; 69 | 70 | return args && args.split(',').map(arg => { 71 | const argMatch = arg.match(FN_ARG); 72 | return (argMatch && argMatch[1] || arg).trim(); 73 | }) || []; 74 | } -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export type ValueType = 'value'; 2 | export type FactoryType = 'factory'; 3 | export type TypeType = 'type'; 4 | 5 | export type ProviderType = ValueType | FactoryType | TypeType; 6 | 7 | export type InjectAnnotated = { 8 | $inject?: string[]; 9 | }; 10 | 11 | export type ScopeAnnotated = { 12 | $scope?: string[]; 13 | }; 14 | 15 | export type Annotated = InjectAnnotated & ScopeAnnotated; 16 | 17 | export type Constructor = ( 18 | { new (...args: any[]): T } | 19 | { (...args: any[]): T } 20 | ); 21 | 22 | export type InitializerFunction = { 23 | (...args: any[]): unknown 24 | } & Annotated; 25 | 26 | export type FactoryFunction = { 27 | (...args: any[]): T; 28 | } & Annotated; 29 | 30 | export type ArrayArgs = [ ...string[], T ]; 31 | 32 | export type ArrayFunc = [ ...string[], FactoryFunction ]; 33 | 34 | export type ArrayConstructor = [ ...string[], Constructor ]; 35 | 36 | export type ServiceProvider = { 37 | (name: string): T; 38 | }; 39 | 40 | export type Initializer = InitializerFunction | ArrayArgs; 41 | 42 | export type FactoryDefinition = FactoryFunction | ArrayArgs>; 43 | 44 | export type TypeDefinition = Constructor | ArrayArgs>; 45 | 46 | export type ValueDefinition = T; 47 | 48 | export type ServiceDefinition = FactoryDefinition | TypeDefinition | ValueDefinition; 49 | 50 | export type TypedDeclaration = [ T, D ] | [ T, D, 'private' ]; 51 | 52 | export type ServiceDeclaration = 53 | TypedDeclaration> | 54 | TypedDeclaration> | 55 | TypedDeclaration>; 56 | 57 | export type ModuleDeclaration = { 58 | [name: string]: ServiceDeclaration | unknown; 59 | __init__?: Array; 60 | __depends__?: Array; 61 | __exports__?: Array; 62 | __modules__?: Array; 63 | }; 64 | 65 | // injector.js 66 | 67 | export type InjectionContext = unknown; 68 | export type LocalsMap = { 69 | [name: string]: unknown 70 | }; 71 | 72 | export type ModuleDefinition = ModuleDeclaration; 73 | 74 | 75 | export class Injector< 76 | ServiceMap = null 77 | > { 78 | 79 | /** 80 | * Create an injector from a set of modules. 81 | */ 82 | constructor(modules: ModuleDefinition[], parent?: InjectorContext); 83 | 84 | /** 85 | * Return a named service, looked up from the existing service map. 86 | */ 87 | get(name: Name): ServiceMap[Name]; 88 | 89 | /** 90 | * Return a named service, and throws if it is not found. 91 | */ 92 | get(name: string): T; 93 | 94 | /** 95 | * Return a named service. 96 | */ 97 | get(name: string, strict: true): T; 98 | 99 | /** 100 | * Return a named service or `null`. 101 | */ 102 | get(name: string, strict: boolean): T | null; 103 | 104 | /** 105 | * Invoke the given function, injecting dependencies. Return the result. 106 | * 107 | * @example 108 | * 109 | * ```javascript 110 | * injector.invoke(function(car) { 111 | * console.log(car.started); 112 | * }); 113 | * ``` 114 | */ 115 | invoke(func: FactoryFunction, context?: InjectionContext, locals?: LocalsMap): T; 116 | 117 | /** 118 | * Invoke the given function, injecting dependencies provided in 119 | * array notation. Return the result. 120 | * 121 | * @example 122 | * 123 | * ```javascript 124 | * injector.invoke([ 'car', function(car) { 125 | * console.log(car.started); 126 | * } ]); 127 | * ``` 128 | */ 129 | invoke(func: ArrayFunc, context?: InjectionContext, locals?: LocalsMap): T; 130 | 131 | /** 132 | * Instantiate the given type, injecting dependencies. 133 | * 134 | * @example 135 | * 136 | * ```javascript 137 | * injector.instantiate(Car); 138 | * ``` 139 | */ 140 | instantiate(constructor: Constructor): T; 141 | 142 | /** 143 | * Instantiate the given type, injecting dependencies provided in array notation. 144 | * 145 | * @example 146 | * 147 | * ```javascript 148 | * injector.instantiate([ 'hifi', Car ]); 149 | * ``` 150 | */ 151 | instantiate(constructor: ArrayConstructor): T; 152 | 153 | /** 154 | * Create a child injector. 155 | */ 156 | createChild(modules: ModuleDefinition[], forceNewInstances?: string[]): Injector; 157 | 158 | /** 159 | * Initializes the injector once, calling `__init__` 160 | * hooks on registered injector modules. 161 | */ 162 | init(): void; 163 | 164 | /** 165 | * @internal 166 | */ 167 | _providers: object; 168 | } 169 | 170 | export type InjectorContext = { 171 | get(name: string, strict?: boolean): T; 172 | 173 | /** 174 | * @internal 175 | */ 176 | _providers?: object 177 | }; 178 | 179 | // annotation.js 180 | 181 | export function annotate(...args: unknown[]): T & InjectAnnotated; 182 | 183 | export function parseAnnotations(fn: unknown) : string[]; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | annotate, 3 | parseAnnotations 4 | } from './annotation.js'; 5 | 6 | export { default as Injector } from './injector.js'; -------------------------------------------------------------------------------- /lib/injector.js: -------------------------------------------------------------------------------- 1 | import { 2 | parseAnnotations, 3 | annotate 4 | } from './annotation.js'; 5 | 6 | import { 7 | isArray, 8 | hasOwnProp 9 | } from './util.js'; 10 | 11 | /** 12 | * @typedef { import('./index.js').ModuleDeclaration } ModuleDeclaration 13 | * @typedef { import('./index.js').ModuleDefinition } ModuleDefinition 14 | * @typedef { import('./index.js').InjectorContext } InjectorContext 15 | * 16 | * @typedef { import('./index.js').TypedDeclaration } TypedDeclaration 17 | */ 18 | 19 | /** 20 | * Create a new injector with the given modules. 21 | * 22 | * @param {ModuleDefinition[]} modules 23 | * @param {InjectorContext} [_parent] 24 | */ 25 | export default function Injector(modules, _parent) { 26 | 27 | const parent = _parent || /** @type InjectorContext */ ({ 28 | get: function(name, strict) { 29 | currentlyResolving.push(name); 30 | 31 | if (strict === false) { 32 | return null; 33 | } else { 34 | throw error(`No provider for "${ name }"!`); 35 | } 36 | } 37 | }); 38 | 39 | const currentlyResolving = []; 40 | const providers = this._providers = Object.create(parent._providers || null); 41 | const instances = this._instances = Object.create(null); 42 | 43 | const self = instances.injector = this; 44 | 45 | const error = function(msg) { 46 | const stack = currentlyResolving.join(' -> '); 47 | currentlyResolving.length = 0; 48 | return new Error(stack ? `${ msg } (Resolving: ${ stack })` : msg); 49 | }; 50 | 51 | /** 52 | * Return a named service. 53 | * 54 | * @param {string} name 55 | * @param {boolean} [strict=true] if false, resolve missing services to null 56 | * 57 | * @return {any} 58 | */ 59 | function get(name, strict) { 60 | if (!providers[name] && name.includes('.')) { 61 | 62 | const parts = name.split('.'); 63 | let pivot = get(/** @type { string } */ (parts.shift())); 64 | 65 | while (parts.length) { 66 | pivot = pivot[/** @type { string } */ (parts.shift())]; 67 | } 68 | 69 | return pivot; 70 | } 71 | 72 | if (hasOwnProp(instances, name)) { 73 | return instances[name]; 74 | } 75 | 76 | if (hasOwnProp(providers, name)) { 77 | if (currentlyResolving.indexOf(name) !== -1) { 78 | currentlyResolving.push(name); 79 | throw error('Cannot resolve circular dependency!'); 80 | } 81 | 82 | currentlyResolving.push(name); 83 | instances[name] = providers[name][0](providers[name][1]); 84 | currentlyResolving.pop(); 85 | 86 | return instances[name]; 87 | } 88 | 89 | return parent.get(name, strict); 90 | } 91 | 92 | function fnDef(fn, locals) { 93 | 94 | if (typeof locals === 'undefined') { 95 | locals = {}; 96 | } 97 | 98 | if (typeof fn !== 'function') { 99 | if (isArray(fn)) { 100 | fn = annotate(fn.slice()); 101 | } else { 102 | throw error(`Cannot invoke "${ fn }". Expected a function!`); 103 | } 104 | } 105 | 106 | /** 107 | * @type {string[]} 108 | */ 109 | const inject = fn.$inject || parseAnnotations(fn); 110 | const dependencies = inject.map(dep => { 111 | if (hasOwnProp(locals, dep)) { 112 | return locals[dep]; 113 | } else { 114 | return get(dep); 115 | } 116 | }); 117 | 118 | return { 119 | fn: fn, 120 | dependencies 121 | }; 122 | } 123 | 124 | /** 125 | * Instantiate the given type, injecting dependencies. 126 | * 127 | * @template T 128 | * 129 | * @param { Function | [...string[], Function ]} type 130 | * 131 | * @return T 132 | */ 133 | function instantiate(type) { 134 | const { 135 | fn, 136 | dependencies 137 | } = fnDef(type); 138 | 139 | // instantiate var args constructor 140 | const Constructor = Function.prototype.bind.call(fn, null, ...dependencies); 141 | 142 | return new Constructor(); 143 | } 144 | 145 | /** 146 | * Invoke the given function, injecting dependencies. Return the result. 147 | * 148 | * @template T 149 | * 150 | * @param { Function | [...string[], Function ]} func 151 | * @param { Object } [context] 152 | * @param { Object } [locals] 153 | * 154 | * @return {T} invocation result 155 | */ 156 | function invoke(func, context, locals) { 157 | const { 158 | fn, 159 | dependencies 160 | } = fnDef(func, locals); 161 | 162 | return fn.apply(context, dependencies); 163 | } 164 | 165 | /** 166 | * @param {Injector} childInjector 167 | * 168 | * @return {Function} 169 | */ 170 | function createPrivateInjectorFactory(childInjector) { 171 | return annotate(key => childInjector.get(key)); 172 | } 173 | 174 | /** 175 | * @param {ModuleDefinition[]} modules 176 | * @param {string[]} [forceNewInstances] 177 | * 178 | * @return {Injector} 179 | */ 180 | function createChild(modules, forceNewInstances) { 181 | if (forceNewInstances && forceNewInstances.length) { 182 | const fromParentModule = Object.create(null); 183 | const matchedScopes = Object.create(null); 184 | 185 | const privateInjectorsCache = []; 186 | const privateChildInjectors = []; 187 | const privateChildFactories = []; 188 | 189 | let provider; 190 | let cacheIdx; 191 | let privateChildInjector; 192 | let privateChildInjectorFactory; 193 | 194 | for (let name in providers) { 195 | provider = providers[name]; 196 | 197 | if (forceNewInstances.indexOf(name) !== -1) { 198 | if (provider[2] === 'private') { 199 | cacheIdx = privateInjectorsCache.indexOf(provider[3]); 200 | if (cacheIdx === -1) { 201 | privateChildInjector = provider[3].createChild([], forceNewInstances); 202 | privateChildInjectorFactory = createPrivateInjectorFactory(privateChildInjector); 203 | privateInjectorsCache.push(provider[3]); 204 | privateChildInjectors.push(privateChildInjector); 205 | privateChildFactories.push(privateChildInjectorFactory); 206 | fromParentModule[name] = [ privateChildInjectorFactory, name, 'private', privateChildInjector ]; 207 | } else { 208 | fromParentModule[name] = [ privateChildFactories[cacheIdx], name, 'private', privateChildInjectors[cacheIdx] ]; 209 | } 210 | } else { 211 | fromParentModule[name] = [ provider[2], provider[1] ]; 212 | } 213 | matchedScopes[name] = true; 214 | } 215 | 216 | if ((provider[2] === 'factory' || provider[2] === 'type') && provider[1].$scope) { 217 | /* jshint -W083 */ 218 | forceNewInstances.forEach(scope => { 219 | if (provider[1].$scope.indexOf(scope) !== -1) { 220 | fromParentModule[name] = [ provider[2], provider[1] ]; 221 | matchedScopes[scope] = true; 222 | } 223 | }); 224 | } 225 | } 226 | 227 | forceNewInstances.forEach(scope => { 228 | if (!matchedScopes[scope]) { 229 | throw new Error('No provider for "' + scope + '". Cannot use provider from the parent!'); 230 | } 231 | }); 232 | 233 | modules.unshift(fromParentModule); 234 | } 235 | 236 | return new Injector(modules, self); 237 | } 238 | 239 | const factoryMap = { 240 | factory: invoke, 241 | type: instantiate, 242 | value: function(value) { 243 | return value; 244 | } 245 | }; 246 | 247 | /** 248 | * @param {ModuleDefinition} moduleDefinition 249 | * @param {Injector} injector 250 | */ 251 | function createInitializer(moduleDefinition, injector) { 252 | 253 | const initializers = moduleDefinition.__init__ || []; 254 | 255 | return function() { 256 | initializers.forEach(initializer => { 257 | 258 | // eagerly resolve component (fn or string) 259 | if (typeof initializer === 'string') { 260 | injector.get(initializer); 261 | } else { 262 | injector.invoke(initializer); 263 | } 264 | }); 265 | }; 266 | } 267 | 268 | /** 269 | * @param {ModuleDefinition} moduleDefinition 270 | */ 271 | function loadModule(moduleDefinition) { 272 | 273 | const moduleExports = moduleDefinition.__exports__; 274 | 275 | // private module 276 | if (moduleExports) { 277 | const nestedModules = moduleDefinition.__modules__; 278 | 279 | const clonedModule = Object.keys(moduleDefinition).reduce((clonedModule, key) => { 280 | 281 | if (key !== '__exports__' && key !== '__modules__' && key !== '__init__' && key !== '__depends__') { 282 | clonedModule[key] = moduleDefinition[key]; 283 | } 284 | 285 | return clonedModule; 286 | }, Object.create(null)); 287 | 288 | const childModules = (nestedModules || []).concat(clonedModule); 289 | 290 | const privateInjector = createChild(childModules); 291 | const getFromPrivateInjector = annotate(function(key) { 292 | return privateInjector.get(key); 293 | }); 294 | 295 | moduleExports.forEach(function(key) { 296 | providers[key] = [ getFromPrivateInjector, key, 'private', privateInjector ]; 297 | }); 298 | 299 | // ensure child injector initializes 300 | const initializers = (moduleDefinition.__init__ || []).slice(); 301 | 302 | initializers.unshift(function() { 303 | privateInjector.init(); 304 | }); 305 | 306 | moduleDefinition = Object.assign({}, moduleDefinition, { 307 | __init__: initializers 308 | }); 309 | 310 | return createInitializer(moduleDefinition, privateInjector); 311 | } 312 | 313 | // normal module 314 | Object.keys(moduleDefinition).forEach(function(key) { 315 | 316 | if (key === '__init__' || key === '__depends__') { 317 | return; 318 | } 319 | 320 | const typeDeclaration = /** @type { TypedDeclaration } */ ( 321 | moduleDefinition[key] 322 | ); 323 | 324 | if (typeDeclaration[2] === 'private') { 325 | providers[key] = typeDeclaration; 326 | return; 327 | } 328 | 329 | const type = typeDeclaration[0]; 330 | const value = typeDeclaration[1]; 331 | 332 | providers[key] = [ factoryMap[type], arrayUnwrap(type, value), type ]; 333 | }); 334 | 335 | return createInitializer(moduleDefinition, self); 336 | } 337 | 338 | /** 339 | * @param {ModuleDefinition[]} moduleDefinitions 340 | * @param {ModuleDefinition} moduleDefinition 341 | * 342 | * @return {ModuleDefinition[]} 343 | */ 344 | function resolveDependencies(moduleDefinitions, moduleDefinition) { 345 | 346 | if (moduleDefinitions.indexOf(moduleDefinition) !== -1) { 347 | return moduleDefinitions; 348 | } 349 | 350 | moduleDefinitions = (moduleDefinition.__depends__ || []).reduce(resolveDependencies, moduleDefinitions); 351 | 352 | if (moduleDefinitions.indexOf(moduleDefinition) !== -1) { 353 | return moduleDefinitions; 354 | } 355 | 356 | return moduleDefinitions.concat(moduleDefinition); 357 | } 358 | 359 | /** 360 | * @param {ModuleDefinition[]} moduleDefinitions 361 | * 362 | * @return { () => void } initializerFn 363 | */ 364 | function bootstrap(moduleDefinitions) { 365 | 366 | const initializers = moduleDefinitions 367 | .reduce(resolveDependencies, []) 368 | .map(loadModule); 369 | 370 | let initialized = false; 371 | 372 | return function() { 373 | 374 | if (initialized) { 375 | return; 376 | } 377 | 378 | initialized = true; 379 | 380 | initializers.forEach(initializer => initializer()); 381 | }; 382 | } 383 | 384 | // public API 385 | this.get = get; 386 | this.invoke = invoke; 387 | this.instantiate = instantiate; 388 | this.createChild = createChild; 389 | 390 | // setup 391 | this.init = bootstrap(modules); 392 | } 393 | 394 | 395 | // helpers /////////////// 396 | 397 | function arrayUnwrap(type, value) { 398 | if (type !== 'value' && isArray(value)) { 399 | value = annotate(value.slice()); 400 | } 401 | 402 | return value; 403 | } -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const CLASS_PATTERN = /^class[ {]/; 2 | 3 | 4 | /** 5 | * @param {function} fn 6 | * 7 | * @return {boolean} 8 | */ 9 | export function isClass(fn) { 10 | return CLASS_PATTERN.test(fn.toString()); 11 | } 12 | 13 | /** 14 | * @param {any} obj 15 | * 16 | * @return {boolean} 17 | */ 18 | export function isArray(obj) { 19 | return Array.isArray(obj); 20 | } 21 | 22 | /** 23 | * @param {any} obj 24 | * @param {string} prop 25 | * 26 | * @return {boolean} 27 | */ 28 | export function hasOwnProp(obj, prop) { 29 | return Object.prototype.hasOwnProperty.call(obj, prop); 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "didi", 3 | "version": "10.2.2", 4 | "description": "Dependency Injection for JavaScript", 5 | "types": "dist/index.d.ts", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.cjs", 12 | "types": "./dist/index.d.ts" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "type": "module", 17 | "scripts": { 18 | "all": "run-s lint bundle test check-types integration-test", 19 | "bundle": "cross-env NODE_ENV=production rollup -c --bundleConfigAsCjs", 20 | "lint": "eslint .", 21 | "check-types": "tsc --pretty --noEmit", 22 | "test": "c8 --reporter=lcov mocha test/*.spec.js", 23 | "integration-test": "(cd test/integration && mocha --import=tsx *.spec.{cjs,ts})", 24 | "prepare": "run-s bundle" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/nikku/didi.git" 29 | }, 30 | "keywords": [ 31 | "di", 32 | "inversion of control", 33 | "dependency", 34 | "injection", 35 | "injector" 36 | ], 37 | "engines": { 38 | "node": ">= 16" 39 | }, 40 | "devDependencies": { 41 | "@types/chai": "^4.3.20", 42 | "@types/mocha": "^10.0.10", 43 | "@types/node": "^20.17.30", 44 | "@web/rollup-plugin-copy": "^0.5.1", 45 | "c8": "^10.1.3", 46 | "chai": "^4.5.0", 47 | "cross-env": "^7.0.3", 48 | "eslint": "^9.24.0", 49 | "eslint-plugin-bpmn-io": "^2.2.0", 50 | "mocha": "^10.8.2", 51 | "npm-run-all2": "^8.0.0", 52 | "rollup": "^4.40.0", 53 | "tsx": "^4.19.3", 54 | "typescript": "^5.8.3", 55 | "typescript-eslint": "^8.30.0" 56 | }, 57 | "author": "Nico Rehwaldt ", 58 | "license": "MIT", 59 | "sideEffects": false, 60 | "files": [ 61 | "dist" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>bpmn-io/renovate-config:recommended" 5 | ] 6 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { copy } from '@web/rollup-plugin-copy'; 2 | 3 | import pkg from './package.json' with { type: 'json' }; 4 | 5 | 6 | const pkgExport = pkg.exports['.']; 7 | 8 | export default [ 9 | { 10 | input: 'lib/index.js', 11 | output: [ 12 | { file: pkgExport.require, format: 'cjs', sourcemap: true }, 13 | { file: pkgExport.import, format: 'es', sourcemap: true } 14 | ], 15 | plugins: [ 16 | copy({ 17 | patterns: '**/*.d.ts', rootDir: './lib' 18 | }) 19 | ] 20 | } 21 | ]; -------------------------------------------------------------------------------- /test/annotation.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { 4 | annotate, 5 | parseAnnotations 6 | } from 'didi'; 7 | 8 | /** 9 | * @typedef {import('didi').InjectAnnotated } InjectAnnotated 10 | */ 11 | 12 | 13 | describe('annotation', function() { 14 | 15 | describe('annotate', function() { 16 | 17 | it('should set $inject property on the last argument', function() { 18 | const fn = function(a, b) { 19 | return null; 20 | }; 21 | 22 | annotate('aa', 'bb', fn); 23 | 24 | expect(/** @type InjectAnnotated */ (fn).$inject).to.eql([ 'aa', 'bb' ]); 25 | }); 26 | 27 | 28 | it('should return the function', function() { 29 | const fn = function(a, b) { 30 | return null; 31 | }; 32 | expect(annotate('aa', 'bb', fn)).to.equal(fn); 33 | }); 34 | 35 | 36 | it('should inject using array args', function() { 37 | const fn = function(a, b) { 38 | return null; 39 | }; 40 | expect(annotate([ 'aa', 'bb', fn ])).to.equal(fn); 41 | }); 42 | 43 | 44 | it('should annotate class constructor', function() { 45 | class Foo { 46 | constructor(a, b) { } 47 | } 48 | 49 | expect(annotate('aa', 'bb', Foo).$inject).to.eql([ 'aa', 'bb' ]); 50 | 51 | expect(annotate('aa', 'bb', Foo)).to.equal(Foo); 52 | }); 53 | 54 | 55 | it('should annotate arrow function', function() { 56 | 57 | const fn = (a, b) => a + b; 58 | 59 | expect(annotate('aa', 'bb', fn).$inject).to.eql([ 'aa', 'bb' ]); 60 | 61 | expect(annotate('aa', 'bb', fn)).to.equal(fn); 62 | }); 63 | 64 | }); 65 | 66 | 67 | describe('parseAnnotations', function() { 68 | 69 | it('should parse function', function() { 70 | expect( 71 | parseAnnotations(function(one, two) {}) 72 | ).to.eql([ 'one', 'two' ]); 73 | 74 | expect( 75 | parseAnnotations(function(one, two) {}) 76 | ).to.eql([ 'one', 'two' ]); 77 | }); 78 | 79 | 80 | describe('should parse lambda', function() { 81 | 82 | it('default', function() { 83 | expect( 84 | parseAnnotations((a, b) => {}) 85 | ).to.eql([ 'a', 'b' ]); 86 | 87 | expect( 88 | parseAnnotations((a, b) => a + b) 89 | ).to.eql([ 'a', 'b' ]); 90 | 91 | expect( 92 | parseAnnotations(a => a + 1) 93 | ).to.eql([ 'a' ]); 94 | 95 | expect( 96 | parseAnnotations(a => { 97 | return a + 1; 98 | }) 99 | ).to.eql([ 'a' ]); 100 | 101 | expect( 102 | parseAnnotations(() => 1) 103 | ).to.eql([ ]); 104 | }); 105 | 106 | 107 | it('async', function() { 108 | expect( 109 | parseAnnotations(async (a, b) => {}) 110 | ).to.eql([ 'a', 'b' ]); 111 | 112 | expect( 113 | parseAnnotations(async (a, b) => a + b) 114 | ).to.eql([ 'a', 'b' ]); 115 | 116 | expect( 117 | parseAnnotations(async a => a + 1) 118 | ).to.eql([ 'a' ]); 119 | 120 | expect( 121 | parseAnnotations(async a => { 122 | return a + 1; 123 | }) 124 | ).to.eql([ 'a' ]); 125 | 126 | expect( 127 | parseAnnotations(async () => 1) 128 | ).to.eql([ ]); 129 | 130 | expect( 131 | parseAnnotations(async () => {}) 132 | ).to.eql([ ]); 133 | }); 134 | 135 | }); 136 | 137 | 138 | describe('should parse class', function() { 139 | 140 | it('with constructor', function() { 141 | class Foo { 142 | constructor(one, two) {} 143 | } 144 | 145 | expect(parseAnnotations(Foo)).to.eql([ 'one', 'two' ]); 146 | }); 147 | 148 | 149 | it('without constructor', function() { 150 | class Car { 151 | start() { 152 | this.started = true; 153 | } 154 | } 155 | 156 | expect(parseAnnotations(Car)).to.eql([ ]); 157 | }); 158 | 159 | 160 | it('without class name', function() { 161 | 162 | // Disable keyword-spacing to reproduce #17 163 | // eslint-disable-next-line keyword-spacing 164 | expect(parseAnnotations(class{})).to.eql([ ]); 165 | }); 166 | 167 | }); 168 | 169 | 170 | describe('should parse comment annotation', function() { 171 | 172 | /* eslint-disable spaced-comment */ 173 | 174 | it('function', function() { 175 | 176 | // when 177 | const fn = function(/* one */ a, /*two*/ b,/* three*/c) {}; 178 | 179 | // then 180 | expect(parseAnnotations(fn)).to.eql([ 'one', 'two', 'three' ]); 181 | }); 182 | 183 | 184 | it('lambda', function() { 185 | 186 | // when 187 | const arrowFn = (/* one */ a, /*two*/ b,/* three*/c) => {}; 188 | 189 | // then 190 | expect(parseAnnotations(arrowFn)).to.eql([ 'one', 'two', 'three' ]); 191 | }); 192 | 193 | 194 | it('class', function() { 195 | class Foo { 196 | constructor(/*one*/ a, /* two*/ b) {} 197 | } 198 | 199 | expect(parseAnnotations(Foo)).to.eql([ 'one', 'two' ]); 200 | }); 201 | 202 | }); 203 | 204 | 205 | it('should parse mixed comments with argument names', function() { 206 | const fn = function(/* one */ a, b,/* three*/c) {}; 207 | 208 | expect(parseAnnotations(fn)).to.eql([ 'one', 'b', 'three' ]); 209 | }); 210 | 211 | 212 | it('should throw error if a non function given', function() { 213 | expect(function() { 214 | 215 | // @ts-ignore-next-line 216 | return parseAnnotations(123); 217 | }).to.throw('Cannot annotate "123". Expected a function!'); 218 | 219 | expect(function() { 220 | 221 | // @ts-ignore-next-line 222 | return parseAnnotations('abc'); 223 | }).to.throw('Cannot annotate "abc". Expected a function!'); 224 | 225 | expect(function() { 226 | return parseAnnotations(null); 227 | }).to.throw('Cannot annotate "null". Expected a function!'); 228 | 229 | expect(function() { 230 | return parseAnnotations(void 0); 231 | }).to.throw('Cannot annotate "undefined". Expected a function!'); 232 | 233 | expect(function() { 234 | 235 | // @ts-ignore-next-line 236 | return parseAnnotations({}); 237 | }).to.throw('Cannot annotate "[object Object]". Expected a function!'); 238 | }); 239 | 240 | }); 241 | 242 | }); 243 | -------------------------------------------------------------------------------- /test/injector.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { Injector } from 'didi'; 4 | 5 | /** 6 | * @typedef {import('didi').ModuleDeclaration} ModuleDeclaration 7 | */ 8 | 9 | 10 | describe('injector', function() { 11 | 12 | it('should consume an object as a module', function() { 13 | 14 | class BubType { 15 | constructor() { 16 | this.name = 'bub'; 17 | } 18 | } 19 | 20 | function BazType() { 21 | this.name = 'baz'; 22 | } 23 | 24 | const injector = new Injector([ 25 | { 26 | foo: [ 27 | 'factory', 28 | function() { 29 | return 'foo-value'; 30 | } 31 | ], 32 | bar: [ 'value', 'bar-value' ], 33 | baz: [ 'type', BazType ], 34 | bub: [ 'type', BubType ] 35 | } 36 | ]); 37 | 38 | expect(injector.get('foo')).to.equal('foo-value'); 39 | expect(injector.get('bar')).to.equal('bar-value'); 40 | 41 | const bub = injector.get('bub'); 42 | expect(bub).to.be.an.instanceof(BubType); 43 | expect(bub.name).to.eql('bub'); 44 | 45 | const baz = injector.get('baz'); 46 | expect(baz).to.be.an.instanceof(BazType); 47 | expect(baz.name).to.eql('baz'); 48 | }); 49 | 50 | 51 | describe('get', function() { 52 | 53 | it('should return an instance', function() { 54 | class BazType { 55 | constructor() { 56 | this.name = 'baz'; 57 | } 58 | } 59 | 60 | const injector = new Injector([ { 61 | foo: [ 'factory', function() { 62 | return { 63 | name: 'foo' 64 | }; 65 | } ], 66 | bar: [ 'value', 'bar value' ], 67 | baz: [ 'type', BazType ], 68 | } ]); 69 | 70 | expect(injector.get('foo')).to.deep.equal({ 71 | name: 'foo' 72 | }); 73 | expect(injector.get('bar')).to.equal('bar value'); 74 | expect(injector.get('baz')).to.deep.equal({ 75 | name: 'baz' 76 | }); 77 | expect(injector.get('baz')).to.be.an.instanceof(BazType); 78 | 79 | // default to strict=true 80 | expect(injector.get('bar', true)).to.equal('bar value'); 81 | }); 82 | 83 | 84 | it('should always return the same instance', function() { 85 | class BazType { 86 | constructor() { 87 | this.name = 'baz'; 88 | } 89 | } 90 | 91 | const injector = new Injector([ { 92 | foo: [ 'factory', function() { 93 | return { 94 | name: 'foo' 95 | }; 96 | } ], 97 | bar: [ 'value', 'bar value' ], 98 | baz: [ 'type', BazType ], 99 | } ]); 100 | 101 | expect(injector.get('foo')).to.equal(injector.get('foo')); 102 | expect(injector.get('bar')).to.equal(injector.get('bar')); 103 | expect(injector.get('baz')).to.equal(injector.get('baz')); 104 | }); 105 | 106 | 107 | it('should reuse module', function() { 108 | class FooType { 109 | constructor() { 110 | this.name = 'foo'; 111 | } 112 | } 113 | 114 | function barFactory(foo) { 115 | return foo; 116 | } 117 | 118 | const module = /** @type ModuleDeclaration */ ({ 119 | foo: [ 'type', [ FooType ] ], 120 | bar: [ 'factory', [ 'foo', barFactory ] ], 121 | }); 122 | 123 | const injector1 = new Injector([ module ]); 124 | expect(injector1.get('foo')).to.equal(injector1.get('bar')); 125 | 126 | const injector2 = new Injector([ module ]); 127 | expect(injector2.get('foo')).to.equal(injector2.get('bar')); 128 | }); 129 | 130 | 131 | it('should reuse inject fn', function() { 132 | class FooType { 133 | constructor() { 134 | this.name = 'foo'; 135 | } 136 | 137 | } 138 | 139 | function barFactory(foo) { 140 | return foo; 141 | } 142 | 143 | const module = /** @type ModuleDeclaration */ ({ 144 | 'foo': [ 'type', [ FooType ] ], 145 | 'bar': [ 'factory', [ 'foo', barFactory ] ] 146 | }); 147 | 148 | const injector = new Injector([ module ]); 149 | function fn(foo, bar) { 150 | expect(foo).to.equal(injector.get('foo')); 151 | expect(bar).to.equal(injector.get('bar')); 152 | } 153 | 154 | injector.invoke([ 'foo', 'bar', fn ]); 155 | 156 | injector.invoke([ 'foo', 'bar', fn ]); 157 | }); 158 | 159 | 160 | it('should resolve dependencies', function() { 161 | class Foo { 162 | constructor(bar1, baz1) { 163 | this.bar = bar1; 164 | this.baz = baz1; 165 | } 166 | } 167 | Foo.$inject = [ 'bar', 'baz' ]; 168 | 169 | function bar(baz, abc) { 170 | return { 171 | baz: baz, 172 | abc: abc 173 | }; 174 | } 175 | bar.$inject = [ 'baz', 'abc' ]; 176 | 177 | const module = /** @type ModuleDeclaration */ ({ 178 | foo: [ 'type', Foo ], 179 | bar: [ 'factory', bar ], 180 | baz: [ 'value', 'baz-value' ], 181 | abc: [ 'value', 'abc-value' ] 182 | }); 183 | 184 | const injector = new Injector([ module ]); 185 | const fooInstance = injector.get('foo'); 186 | 187 | expect(fooInstance.bar).to.deep.equal({ 188 | baz: 'baz-value', 189 | abc: 'abc-value' 190 | }); 191 | 192 | expect(fooInstance.baz).to.equal('baz-value'); 193 | }); 194 | 195 | 196 | it('should resolve dependencies (array notation)', function() { 197 | class Foo { 198 | constructor(bar1, baz1) { 199 | this.bar = bar1; 200 | this.baz = baz1; 201 | } 202 | } 203 | 204 | const bar = function(baz, abc) { 205 | return { 206 | baz: baz, 207 | abc: abc 208 | }; 209 | }; 210 | 211 | const module = /** @type ModuleDeclaration */ ({ 212 | foo: [ 'type', [ 'bar', 'baz', Foo ] ], 213 | bar: [ 'factory', [ 'baz', 'abc', bar ] ], 214 | baz: [ 'value', 'baz-value' ], 215 | abc: [ 'value', 'abc-value' ] 216 | }); 217 | 218 | const injector = new Injector([ module ]); 219 | const fooInstance = injector.get('foo'); 220 | 221 | expect(fooInstance.bar).to.deep.equal({ 222 | baz: 'baz-value', 223 | abc: 'abc-value' 224 | }); 225 | expect(fooInstance.baz).to.equal('baz-value'); 226 | }); 227 | 228 | 229 | it('should inject properties', function() { 230 | const module = /** @type ModuleDeclaration */ ({ 231 | config: [ 'value', { 232 | a: 1, 233 | b: { 234 | c: 2 235 | } 236 | } ] 237 | }); 238 | 239 | const injector = new Injector([ module ]); 240 | 241 | expect(injector.get('config.a')).to.equal(1); 242 | expect(injector.get('config.b.c')).to.equal(2); 243 | }); 244 | 245 | 246 | it('should inject dotted service if present', function() { 247 | const module = /** @type ModuleDeclaration */ ({ 248 | 'a.b': [ 'value', 'a.b value' ] 249 | }); 250 | 251 | const injector = new Injector([ module ]); 252 | expect(injector.get('a.b')).to.equal('a.b value'); 253 | }); 254 | 255 | 256 | it('should provide "injector"', function() { 257 | const injector = new Injector([]); 258 | 259 | expect(injector.get('injector')).to.equal(injector); 260 | }); 261 | 262 | 263 | it('should throw error with full path if no provider', function() { 264 | 265 | // a requires b requires c (not provided) 266 | function aFn(b) { 267 | return 'a-value'; 268 | } 269 | aFn.$inject = [ 'b' ]; 270 | 271 | function bFn(c) { 272 | return 'b-value'; 273 | } 274 | bFn.$inject = [ 'c' ]; 275 | 276 | const module = /** @type ModuleDeclaration */ ({ 277 | a: [ 'factory', aFn ], 278 | b: [ 'factory', bFn ] 279 | }); 280 | 281 | const injector = new Injector([ module ]); 282 | 283 | expect(function() { 284 | return injector.get('a'); 285 | }).to.throw('No provider for "c"! (Resolving: a -> b -> c)'); 286 | }); 287 | 288 | 289 | it('should return null if non-strict and no provider', function() { 290 | const injector = new Injector([]); 291 | const notDefined = injector.get('not-defined', false); 292 | 293 | expect(notDefined).to.be.null; 294 | }); 295 | 296 | 297 | it('should throw error if circular dependency', function() { 298 | function aFn(b) { 299 | return 'a-value'; 300 | } 301 | 302 | function bFn(a) { 303 | return 'b-value'; 304 | } 305 | 306 | const module = /** @type ModuleDeclaration */ ({ 307 | a: [ 'factory', [ 'b', aFn ] ], 308 | b: [ 'factory', [ 'a', bFn ] ], 309 | }); 310 | 311 | const injector = new Injector([ module ]); 312 | 313 | expect(function() { 314 | return injector.get('a'); 315 | }).to.throw('Cannot resolve circular dependency! ' + '(Resolving: a -> b -> a)'); 316 | }); 317 | 318 | }); 319 | 320 | 321 | describe('invoke', function() { 322 | 323 | it('should resolve dependencies', function() { 324 | function bar(baz, abc) { 325 | return { 326 | baz: baz, 327 | abc: abc 328 | }; 329 | } 330 | bar.$inject = [ 'baz', 'abc' ]; 331 | 332 | const module = /** @type ModuleDeclaration */ ({ 333 | baz: [ 'value', 'baz-value' ], 334 | abc: [ 'value', 'abc-value' ] 335 | }); 336 | 337 | const injector = new Injector([ module ]); 338 | 339 | expect(injector.invoke(bar)).to.deep.equal({ 340 | baz: 'baz-value', 341 | abc: 'abc-value' 342 | }); 343 | }); 344 | 345 | 346 | it('should resolve dependencies (array notation)', function() { 347 | function bar(a, b) { 348 | return { 349 | baz: a, 350 | abc: b 351 | }; 352 | } 353 | 354 | const module = /** @type ModuleDeclaration */ ({ 355 | baz: [ 'value', 'baz-value' ], 356 | abc: [ 'value', 'abc-value' ] 357 | }); 358 | 359 | const injector = new Injector([ module ]); 360 | 361 | const result = injector.invoke([ 'baz', 'abc', bar ]); 362 | 363 | expect(result).to.deep.equal({ 364 | baz: 'baz-value', 365 | abc: 'abc-value' 366 | }); 367 | }); 368 | 369 | 370 | it('should invoke function on given context', function() { 371 | const context = {}; 372 | const injector = new Injector([]); 373 | 374 | injector.invoke((function() { 375 | 376 | // @ts-expect-error 377 | expect(this).to.equal(context); 378 | }), context); 379 | }); 380 | 381 | 382 | it('should throw error if a non function given', function() { 383 | const injector = new Injector([]); 384 | 385 | expect(function() { 386 | 387 | // @ts-expect-error 388 | return injector.invoke(123); 389 | }).to.throw('Cannot invoke "123". Expected a function!'); 390 | 391 | expect(function() { 392 | 393 | // @ts-expect-error 394 | return injector.invoke('abc'); 395 | }).to.throw('Cannot invoke "abc". Expected a function!'); 396 | 397 | expect(function() { 398 | 399 | // @ts-expect-error 400 | return injector.invoke(null); 401 | }).to.throw('Cannot invoke "null". Expected a function!'); 402 | 403 | expect(function() { 404 | 405 | // @ts-expect-error 406 | return injector.invoke(void 0); 407 | }).to.throw('Cannot invoke "undefined". ' + 'Expected a function!'); 408 | 409 | expect(function() { 410 | 411 | // @ts-expect-error 412 | return injector.invoke({}); 413 | }).to.throw('Cannot invoke "[object Object]". ' + 'Expected a function!'); 414 | }); 415 | 416 | 417 | it('should auto parse arguments/comments if no $inject defined', function() { 418 | function bar(/* baz */ a, abc) { 419 | return { baz: a, abc: abc }; 420 | } 421 | 422 | const module = /** @type ModuleDeclaration */ ({ 423 | baz: [ 'value', 'baz-value' ], 424 | abc: [ 'value', 'abc-value' ] 425 | }); 426 | 427 | const injector = new Injector([ module ]); 428 | 429 | expect(injector.invoke(bar)).to.deep.equal({ 430 | baz: 'baz-value', 431 | abc: 'abc-value' 432 | }); 433 | }); 434 | 435 | 436 | it('should resolve with local overrides', function() { 437 | class FooType { 438 | constructor() { 439 | throw new Error('foo broken'); 440 | } 441 | } 442 | 443 | const injector = new Injector([ 444 | { 445 | foo: [ 'type', FooType ] 446 | } 447 | ]); 448 | 449 | injector.invoke([ 'foo', 'bar', function(foo, bar) { 450 | expect(foo).to.eql('FOO'); 451 | expect(bar).to.equal(undefined); 452 | } ], null, { foo: 'FOO', bar: undefined }); 453 | }); 454 | 455 | }); 456 | 457 | 458 | describe('instantiate', function() { 459 | 460 | it('should resolve dependencies', function() { 461 | class Foo { 462 | constructor(abc1, baz1) { 463 | this.abc = abc1; 464 | this.baz = baz1; 465 | } 466 | } 467 | Foo.$inject = [ 'abc', 'baz' ]; 468 | 469 | const module = /** @type ModuleDeclaration */ ({ 470 | baz: [ 'value', 'baz-value' ], 471 | abc: [ 'value', 'abc-value' ] 472 | }); 473 | 474 | const injector = new Injector([ module ]); 475 | 476 | expect(injector.instantiate(Foo)).to.deep.equal({ 477 | abc: 'abc-value', 478 | baz: 'baz-value' 479 | }); 480 | }); 481 | 482 | 483 | it('should return returned value from constructor if an object returned', function() { 484 | 485 | const injector = new Injector([]); 486 | const returnedObj = {}; 487 | 488 | function ObjCls() { 489 | return returnedObj; 490 | } 491 | function StringCls() { 492 | return 'some string'; 493 | } 494 | function NumberCls() { 495 | return 123; 496 | } 497 | 498 | expect(injector.instantiate(ObjCls)).to.equal(returnedObj); 499 | 500 | expect(injector.instantiate(StringCls)).to.be.an.instanceof(StringCls); 501 | 502 | expect(injector.instantiate(NumberCls)).to.be.an.instanceof(NumberCls); 503 | 504 | expect(injector.instantiate([ 'injector', NumberCls ])).to.be.an.instanceof(NumberCls); 505 | }); 506 | 507 | }); 508 | 509 | 510 | describe('child', function() { 511 | 512 | it('should inject from child', function() { 513 | const moduleParent = /** @type ModuleDeclaration */ ({ 514 | a: [ 'value', 'a-parent' ] 515 | }); 516 | 517 | const moduleChild = /** @type ModuleDeclaration */ ({ 518 | a: [ 'value', 'a-child' ], 519 | d: [ 'value', 'd-child' ] 520 | }); 521 | 522 | const injector = new Injector([ moduleParent ]); 523 | const child = injector.createChild([ moduleChild ]); 524 | 525 | expect(child.get('d')).to.equal('d-child'); 526 | expect(child.get('a')).to.equal('a-child'); 527 | }); 528 | 529 | 530 | it('should provide the child injector as "injector"', function() { 531 | const injector = new Injector([]); 532 | const childInjector = injector.createChild([]); 533 | 534 | expect(childInjector.get('injector')).to.equal(childInjector); 535 | }); 536 | 537 | 538 | it('should inject from parent if not provided in child', function() { 539 | const moduleParent = /** @type ModuleDeclaration */ ({ 540 | a: [ 'value', 'a-parent' ] 541 | }); 542 | 543 | const moduleChild = /** @type ModuleDeclaration */ ({ 544 | b: [ 'factory', function(a) { 545 | return { 546 | a: a 547 | }; 548 | } ] 549 | }); 550 | 551 | const injector = new Injector([ moduleParent ]); 552 | const child = injector.createChild([ moduleChild ]); 553 | 554 | expect(child.get('b')).to.deep.equal({ 555 | a: 'a-parent' 556 | }); 557 | }); 558 | 559 | 560 | it('should inject from parent but never use dependency from child', function() { 561 | const moduleParent = /** @type ModuleDeclaration */ ({ 562 | b: [ 'factory', function(c) { 563 | return 'b-parent'; 564 | } ] 565 | }); 566 | 567 | const moduleChild = /** @type ModuleDeclaration */ ({ 568 | c: [ 'value', 'c-child' ] 569 | }); 570 | 571 | const injector = new Injector([ moduleParent ]); 572 | const child = injector.createChild([ moduleChild ]); 573 | 574 | expect(function() { 575 | return child.get('b'); 576 | }).to.throw('No provider for "c"! (Resolving: b -> c)'); 577 | }); 578 | 579 | 580 | it('should force new instance in child', function() { 581 | const moduleParent = /** @type ModuleDeclaration */ ({ 582 | b: [ 'factory', function(c) { 583 | return { 584 | c: c 585 | }; 586 | } ], 587 | c: [ 'value', 'c-parent' ] 588 | }); 589 | const injector = new Injector([ moduleParent ]); 590 | 591 | expect(injector.get('b')).to.deep.equal({ 592 | c: 'c-parent' 593 | }); 594 | 595 | const moduleChild = /** @type ModuleDeclaration */ ({ 596 | c: [ 'value', 'c-child' ] 597 | }); 598 | 599 | const child = injector.createChild([ moduleChild ], [ 'b' ]); 600 | expect(child.get('b')).to.deep.equal({ 601 | c: 'c-child' 602 | }); 603 | }); 604 | 605 | 606 | it('should force new instance using provider from grand parent', function() { 607 | 608 | const x = {}; 609 | 610 | // regression 611 | const moduleGrandParent = /** @type ModuleDeclaration */ ({ 612 | x: [ 'value', x ] 613 | }); 614 | 615 | const injector = new Injector([ moduleGrandParent ]); 616 | 617 | const grandChildInjector = injector.createChild([]).createChild([], [ 'x' ]); 618 | 619 | expect(grandChildInjector).to.exist; 620 | expect(grandChildInjector.get('x')).to.equal(x); 621 | }); 622 | 623 | 624 | it('should throw error if forced provider does not exist', function() { 625 | const injector = new Injector([]); 626 | 627 | expect(function() { 628 | return injector.createChild([], [ 'b' ]); 629 | }).to.throw('No provider for "b". Cannot use provider from the parent!'); 630 | }); 631 | 632 | }); 633 | 634 | 635 | describe('private modules', function() { 636 | 637 | it('should only expose public bindings', function() { 638 | const injector = new Injector([ 639 | { 640 | __exports__: [ 'publicFoo' ], 641 | 'publicFoo': [ 642 | 'factory', 643 | function(privateBar) { 644 | return { 645 | dependency: privateBar 646 | }; 647 | } 648 | ], 649 | 'privateBar': [ 'value', 'private-value' ] 650 | }, 651 | { 652 | 'bar': [ 653 | 'factory', 654 | function(privateBar) { 655 | return null; 656 | } 657 | ], 658 | 'baz': [ 659 | 'factory', 660 | function(publicFoo) { 661 | return { 662 | dependency: publicFoo 663 | }; 664 | } 665 | ] 666 | } 667 | ]); 668 | 669 | const publicFoo = injector.get('publicFoo'); 670 | 671 | expect(publicFoo).to.exist; 672 | expect(publicFoo.dependency).to.equal('private-value'); 673 | 674 | expect(function() { 675 | return injector.get('privateBar'); 676 | }).to.throw('No provider for "privateBar"! (Resolving: privateBar)'); 677 | 678 | expect(function() { 679 | return injector.get('bar'); 680 | }).to.throw('No provider for "privateBar"! (Resolving: bar -> privateBar)'); 681 | 682 | expect(injector.get('baz').dependency).to.equal(publicFoo); 683 | }); 684 | 685 | 686 | it('should allow name collisions in private bindings', function() { 687 | 688 | const injector = new Injector([ 689 | { 690 | __exports__: [ 'foo' ], 691 | 'foo': [ 692 | 'factory', 693 | function(conflict) { 694 | return conflict; 695 | } 696 | ], 697 | 'conflict': [ 'value', 'private-from-a' ] 698 | }, 699 | { 700 | __exports__: [ 'bar' ], 701 | 'bar': [ 702 | 'factory', 703 | function(conflict) { 704 | return conflict; 705 | } 706 | ], 707 | 'conflict': [ 'value', 'private-from-b' ] 708 | } 709 | ]); 710 | 711 | expect(injector.get('foo')).to.equal('private-from-a'); 712 | expect(injector.get('bar')).to.equal('private-from-b'); 713 | }); 714 | 715 | 716 | it('should not override global names', function() { 717 | 718 | const injector = new Injector([ 719 | { 720 | 'foo': [ 'value', 'GLOBAL_FOO' ] 721 | }, 722 | { 723 | __exports__: [ 'bar' ], 724 | 'foo': [ 'value', 'LOCAL_FOO' ], 725 | 'bar': [ 'factory', (foo) => foo ] 726 | } 727 | ]); 728 | 729 | expect(injector.get('foo')).to.equal('GLOBAL_FOO'); 730 | expect(injector.get('bar')).to.equal('LOCAL_FOO'); 731 | }); 732 | 733 | 734 | it('should allow forcing new instance', function() { 735 | 736 | const injector = new Injector([ 737 | { 738 | __exports__: [ 'foo' ], 739 | 'foo': [ 740 | 'factory', 741 | function(bar) { 742 | return { 743 | bar: bar 744 | }; 745 | } 746 | ], 747 | 'bar': [ 'value', 'private-bar' ] 748 | } 749 | ]); 750 | 751 | const firstChild = injector.createChild([], [ 'foo' ]); 752 | const secondChild = injector.createChild([], [ 'foo' ]); 753 | const fooFromFirstChild = firstChild.get('foo'); 754 | const fooFromSecondChild = secondChild.get('foo'); 755 | 756 | expect(fooFromFirstChild).not.to.equal(fooFromSecondChild); 757 | expect(fooFromFirstChild.bar).to.equal(fooFromSecondChild.bar); 758 | }); 759 | 760 | 761 | describe('additional __modules__', function() { 762 | 763 | it('should load', function() { 764 | 765 | const otherModule = /** @type ModuleDeclaration */ ({ 766 | 'bar': [ 'value', 'bar-from-other-module' ] 767 | }); 768 | 769 | const injector = new Injector([ 770 | { 771 | __exports__: [ 'foo' ], 772 | __modules__: [ otherModule ], 773 | 'foo': [ 774 | 'factory', 775 | function(bar) { 776 | return { 777 | bar: bar 778 | }; 779 | } 780 | ] 781 | } 782 | ]); 783 | const foo = injector.get('foo'); 784 | 785 | expect(foo).to.exist; 786 | expect(foo.bar).to.equal('bar-from-other-module'); 787 | 788 | expect(function() { 789 | injector.get('bar'); 790 | }).to.throw('No provider for "bar"! (Resolving: bar)'); 791 | }); 792 | 793 | 794 | it('should re-use', function() { 795 | 796 | const otherModule = /** @type ModuleDeclaration */ ({ 797 | 'bar': [ 'value', {} ] 798 | }); 799 | 800 | const injector = new Injector([ 801 | otherModule, 802 | { 803 | __exports__: [ 'foo' ], 804 | __modules__: [ otherModule ], 805 | 'foo': [ 806 | 'factory', 807 | function(bar) { 808 | return { 809 | bar: bar 810 | }; 811 | } 812 | ] 813 | } 814 | ]); 815 | const foo = injector.get('foo'); 816 | 817 | expect(foo).to.exist; 818 | expect(foo.bar).to.equal(injector.get('bar')); 819 | }); 820 | 821 | }); 822 | 823 | 824 | it('should initialize', function() { 825 | 826 | // given 827 | const loaded = []; 828 | 829 | const injector = new Injector([ 830 | { 831 | __exports__: [ 'foo' ], 832 | __modules__: [ 833 | { 834 | __init__: [ () => loaded.push('nested') ], 835 | bar: [ 'value', 10 ] 836 | } 837 | ], 838 | __init__: [ (bar) => loaded.push('module' + bar) ], 839 | foo: [ 840 | 'factory', 841 | function(bar) { 842 | return bar; 843 | } 844 | ] 845 | } 846 | ]); 847 | 848 | // when 849 | injector.init(); 850 | 851 | // then 852 | expect(loaded).to.eql([ 853 | 'nested', 854 | 'module10' 855 | ]); 856 | 857 | expect(function() { 858 | injector.get('bar'); 859 | }).to.throw(/No provider for "bar"/); 860 | }); 861 | 862 | 863 | it('should only create one private child injector', function() { 864 | 865 | const injector = new Injector([ 866 | { 867 | __exports__: [ 'foo', 'bar' ], 868 | 'foo': [ 869 | 'factory', 870 | function(bar) { 871 | return { 872 | bar: bar 873 | }; 874 | } 875 | ], 876 | 'bar': [ 877 | 'factory', 878 | function(internal) { 879 | return { 880 | internal: internal 881 | }; 882 | } 883 | ], 884 | 'internal': [ 885 | 'factory', 886 | function() { 887 | return {}; 888 | } 889 | ] 890 | } 891 | ]); 892 | const foo = injector.get('foo'); 893 | const bar = injector.get('bar'); 894 | const childInjector = injector.createChild([], [ 'foo', 'bar' ]); 895 | const fooFromChild = childInjector.get('foo'); 896 | const barFromChild = childInjector.get('bar'); 897 | 898 | expect(fooFromChild).to.not.equal(foo); 899 | expect(barFromChild).to.not.equal(bar); 900 | expect(fooFromChild.bar).to.equal(barFromChild); 901 | }); 902 | 903 | }); 904 | 905 | 906 | describe('scopes', function() { 907 | 908 | return it('should force new instances per scope', function() { 909 | function Foo() {} 910 | Foo.$scope = [ 'request' ]; 911 | 912 | function createBar() { 913 | return {}; 914 | } 915 | createBar.$scope = [ 'session' ]; 916 | 917 | const injector = new Injector([ 918 | { 919 | 'foo': [ 'type', Foo ], 920 | 'bar': [ 'factory', createBar ] 921 | } 922 | ]); 923 | const foo = injector.get('foo'); 924 | const bar = injector.get('bar'); 925 | 926 | const sessionInjector = injector.createChild([], [ 'session' ]); 927 | expect(sessionInjector.get('foo')).to.equal(foo); 928 | expect(sessionInjector.get('bar')).to.not.equal(bar); 929 | 930 | const requestInjector = injector.createChild([], [ 'request' ]); 931 | 932 | expect(requestInjector.get('foo')).to.not.equal(foo); 933 | expect(requestInjector.get('bar')).to.equal(bar); 934 | }); 935 | }); 936 | 937 | 938 | describe('override', function() { 939 | 940 | it('should replace definition via override module', function() { 941 | class Foo { 942 | constructor(bar1, baz1) { 943 | this.bar = bar1; 944 | this.baz = baz1; 945 | } 946 | } 947 | 948 | function createBlub(foo1) { 949 | return foo1; 950 | } 951 | 952 | const base = /** @type ModuleDeclaration */ ({ 953 | foo: [ 'type', [ 'bar', 'baz', Foo ] ], 954 | blub: [ 'factory', [ 'foo', createBlub ] ], 955 | baz: [ 'value', 'baz-value' ], 956 | abc: [ 'value', 'abc-value' ] 957 | }); 958 | 959 | const extension = /** @type ModuleDeclaration */ ({ 960 | foo: [ 'type', [ 'baz', 'abc', Foo ] ] 961 | }); 962 | 963 | const injector = new Injector([ base, extension ]); 964 | const expectedFoo = { 965 | bar: 'baz-value', 966 | baz: 'abc-value' 967 | }; 968 | 969 | expect(injector.get('foo')).to.deep.equal(expectedFoo); 970 | expect(injector.get('blub')).to.deep.equal(expectedFoo); 971 | }); 972 | 973 | 974 | it('should mock element via value', function() { 975 | function createBar() { 976 | return { 977 | a: 'realA' 978 | }; 979 | } 980 | 981 | const base = /** @type ModuleDeclaration */ ({ 982 | bar: [ 'factory', createBar ] 983 | }); 984 | 985 | const mocked = { 986 | a: 'A' 987 | }; 988 | 989 | const mock = /** @type ModuleDeclaration */ ({ 990 | bar: [ 'value', mocked ] 991 | }); 992 | 993 | const injector = new Injector([ base, mock ]); 994 | 995 | expect(injector.get('bar')).to.equal(mocked); 996 | }); 997 | 998 | }); 999 | 1000 | 1001 | describe('initialize (__init__)', function() { 1002 | 1003 | it('should init component', function() { 1004 | 1005 | // given 1006 | const injector = new Injector([ 1007 | { 1008 | __init__: [ 'foo' ], 1009 | 'foo': [ 'factory', function(bar) { 1010 | bar.initialized = true; 1011 | 1012 | return bar; 1013 | } ], 1014 | 'bar': [ 'value', {} ] 1015 | } 1016 | ]); 1017 | 1018 | // when 1019 | injector.init(); 1020 | 1021 | const bar = injector.get('bar'); 1022 | 1023 | // then 1024 | expect(bar.initialized).to.be.true; 1025 | }); 1026 | 1027 | 1028 | it('should call initializer', function() { 1029 | 1030 | // given 1031 | const injector = new Injector([ 1032 | { 1033 | __init__: [ function(bar) { 1034 | bar.initialized = true; 1035 | } ], 1036 | 'bar': [ 'value', {} ] 1037 | } 1038 | ]); 1039 | 1040 | const bar = injector.get('bar'); 1041 | 1042 | // assume 1043 | expect(bar.initialized).not.to.exist; 1044 | 1045 | // when 1046 | injector.init(); 1047 | 1048 | // then 1049 | expect(bar.initialized).to.be.true; 1050 | }); 1051 | 1052 | 1053 | it('should execute only once', function() { 1054 | 1055 | // given 1056 | const injector = new Injector([ 1057 | { 1058 | __init__: [ function(bar) { 1059 | bar.initCalled++; 1060 | 1061 | return bar; 1062 | } ], 1063 | 'bar': [ 'value', { initCalled: 0 } ] 1064 | } 1065 | ]); 1066 | 1067 | injector.init(); 1068 | 1069 | // when 1070 | injector.init(); 1071 | 1072 | const bar = injector.get('bar'); 1073 | 1074 | // then 1075 | expect(bar.initCalled).to.eql(1); 1076 | }); 1077 | 1078 | 1079 | describe('private modules', function() { 1080 | 1081 | it('should init with child injector', function() { 1082 | 1083 | // given 1084 | const privateBar = {}; 1085 | 1086 | const injector = new Injector([ 1087 | { 1088 | __exports__: [ 'publicFoo' ], 1089 | __init__: [ function(privateBar) { 1090 | privateBar.initialized = true; 1091 | } ], 1092 | 'publicFoo': [ 1093 | 'factory', 1094 | function(privateBar) { 1095 | return { 1096 | privateBar 1097 | }; 1098 | } 1099 | ], 1100 | 'privateBar': [ 'value', {} ] 1101 | } 1102 | ]); 1103 | 1104 | // when 1105 | injector.init(); 1106 | 1107 | const publicFoo = injector.get('publicFoo'); 1108 | 1109 | expect(publicFoo.privateBar.initialized).to.be.true; 1110 | }); 1111 | 1112 | }); 1113 | 1114 | 1115 | describe('error handling', function() { 1116 | 1117 | it('should indicate missing dependency', function() { 1118 | 1119 | // given 1120 | const injector = new Injector([ 1121 | { 1122 | __init__: [ 'foo' ] 1123 | } 1124 | ]); 1125 | 1126 | // then 1127 | expect(function() { 1128 | injector.init(); 1129 | }).to.throw(/No provider for "foo"!/); 1130 | }); 1131 | 1132 | 1133 | it('should indicate initialization error', function() { 1134 | 1135 | // given 1136 | const injector = new Injector([ 1137 | { 1138 | __init__: [ function() { 1139 | throw new Error('INIT ERROR'); 1140 | } ] 1141 | } 1142 | ]); 1143 | 1144 | // then 1145 | expect(function() { 1146 | injector.init(); 1147 | }).to.throw(/INIT ERROR/); 1148 | }); 1149 | 1150 | }); 1151 | 1152 | }); 1153 | 1154 | 1155 | describe('module dependencies (__depends__)', function() { 1156 | 1157 | it('should load in reverse order', function() { 1158 | 1159 | const loaded = []; 1160 | 1161 | // given 1162 | const injector = new Injector([ 1163 | { 1164 | __depends__: [ 1165 | { 1166 | __depends__: [ 1167 | { 1168 | __init__: [ function() { loaded.push('L2_A'); } ] 1169 | }, 1170 | { 1171 | __init__: [ function() { loaded.push('L2_B'); } ] 1172 | } 1173 | ], 1174 | __init__: [ function() { loaded.push('L1'); } ] 1175 | } 1176 | ], 1177 | __init__: [ function() { loaded.push('ROOT'); } ] 1178 | } 1179 | ]); 1180 | 1181 | // when 1182 | injector.init(); 1183 | 1184 | // then 1185 | expect(loaded).to.eql([ 1186 | 'L2_A', 1187 | 'L2_B', 1188 | 'L1', 1189 | 'ROOT' 1190 | ]); 1191 | }); 1192 | 1193 | 1194 | it('should de-duplicate', function() { 1195 | 1196 | const loaded = []; 1197 | 1198 | const duplicateModule = /** @type ModuleDeclaration */ ({ 1199 | __init__: [ function() { loaded.push('DUP'); } ] 1200 | }); 1201 | 1202 | // given 1203 | const injector = new Injector([ 1204 | { 1205 | __depends__: [ 1206 | { 1207 | __depends__: [ 1208 | duplicateModule, 1209 | duplicateModule 1210 | ], 1211 | __init__: [ function() { loaded.push('L1'); } ] 1212 | }, 1213 | duplicateModule 1214 | ], 1215 | __init__: [ function() { loaded.push('ROOT'); } ] 1216 | } 1217 | ]); 1218 | 1219 | // when 1220 | injector.init(); 1221 | 1222 | // then 1223 | expect(loaded).to.eql([ 1224 | 'DUP', 1225 | 'L1', 1226 | 'ROOT' 1227 | ]); 1228 | }); 1229 | 1230 | }); 1231 | 1232 | }); 1233 | -------------------------------------------------------------------------------- /test/integration/node.spec.cjs: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | 4 | describe('integration', function() { 5 | 6 | describe('node bundle', function() { 7 | 8 | const { 9 | annotate, 10 | Injector 11 | } = require('didi'); 12 | 13 | 14 | it('should expose API', function() { 15 | expect(annotate).to.exist; 16 | expect(Injector).to.exist; 17 | }); 18 | 19 | 20 | it('should work bundled', function() { 21 | 22 | class BubType { 23 | constructor() { 24 | this.name = 'bub'; 25 | } 26 | } 27 | 28 | function BazType() { 29 | this.name = 'baz'; 30 | } 31 | 32 | const injector = new Injector([ 33 | { 34 | foo: [ 35 | 'factory', 36 | function() { 37 | return 'foo-value'; 38 | } 39 | ], 40 | bar: [ 'value', 'bar-value' ], 41 | baz: [ 'type', BazType ], 42 | bub: [ 'type', BubType ] 43 | } 44 | ]); 45 | 46 | expect(injector.get('foo')).to.equal('foo-value'); 47 | expect(injector.get('bar')).to.equal('bar-value'); 48 | 49 | const bub = injector.get('bub'); 50 | expect(bub).to.be.an.instanceof(BubType); 51 | expect(bub.name).to.eql('bub'); 52 | 53 | const baz = injector.get('baz'); 54 | expect(baz).to.be.an.instanceof(BazType); 55 | expect(baz.name).to.eql('baz'); 56 | }); 57 | 58 | }); 59 | 60 | 61 | describe('esm bundle', function() { 62 | 63 | it('should expose API', async function() { 64 | 65 | // when 66 | const { 67 | annotate, 68 | Injector 69 | } = await import('didi'); 70 | 71 | // then 72 | expect(annotate).to.exist; 73 | expect(Injector).to.exist; 74 | }); 75 | 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /test/integration/ts.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from 'didi'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | 6 | describe('typed', function() { 7 | 8 | class BubType { 9 | bar: string; 10 | 11 | constructor(bar: string) { 12 | this.bar = bar; 13 | } 14 | } 15 | 16 | class BazType { 17 | name: string; 18 | 19 | constructor() { 20 | this.name = 'baz'; 21 | } 22 | } 23 | 24 | 25 | describe('Injector', function() { 26 | 27 | it('should instantiate', function() { 28 | 29 | // when 30 | const injector = new Injector([ 31 | { 32 | foo: [ 33 | 'factory', 34 | function() { 35 | return 'foo-value'; 36 | } 37 | ], 38 | bar: [ 'value', 'bar' ], 39 | baz: [ 'type', BazType ], 40 | bub: [ 'type', BubType ] 41 | } 42 | ]); 43 | 44 | // then 45 | expect(injector).to.exist; 46 | }); 47 | 48 | 49 | it('should ignore extension', function() { 50 | 51 | // given 52 | const injector = new Injector([ 53 | { 54 | __wooop__: [ 'bub' ] 55 | } 56 | ]); 57 | 58 | // then 59 | expect(injector).to.exist; 60 | }); 61 | 62 | 63 | it('should offer typed injections', function() { 64 | 65 | // given 66 | type ServiceMap = { 67 | 'foo': 1, 68 | 'bar': 'BAR' 69 | }; 70 | 71 | // when 72 | const injector = new Injector([ 73 | { 74 | foo: [ 'value', 1 ], 75 | bar: [ 'value', 'BAR' ] 76 | } 77 | ]); 78 | 79 | // then 80 | const foo = injector.get('foo'); 81 | expect(foo).to.eql(1); 82 | 83 | const bar = injector.get('bar'); 84 | expect(bar).to.eql('BAR'); 85 | 86 | const baz = injector.get('baz', false); 87 | expect(baz).not.to.exist; 88 | 89 | // illegal usage, but if you think you know better 90 | // we still accept it 91 | const boolBar = injector.get('bar'); 92 | expect(boolBar).to.exist; 93 | 94 | // @ts-expect-error illegal type conversion 95 | const invalidFoo : string = injector.get('foo'); 96 | }); 97 | 98 | }); 99 | 100 | 101 | describe('#get', function() { 102 | 103 | it('should get', function() { 104 | 105 | // given 106 | const injector = new Injector([ 107 | { 108 | foo: [ 109 | 'factory', 110 | function() { 111 | return 'foo-value'; 112 | } 113 | ], 114 | bar: [ 'value', 'bar-value' ], 115 | foop: [ 116 | 'factory', 117 | function(bar: string) { 118 | return bar; 119 | } 120 | ], 121 | baz: [ 'type', BazType ], 122 | bub: [ 'type', BubType ] 123 | } 124 | ]); 125 | 126 | // when 127 | const foo = injector.get('foo') as string; 128 | const _bar = injector.get('bar') as string; 129 | const foop = injector.get('foop') as string; 130 | const bub = injector.get('bub'); 131 | const baz = injector.get('baz') as BazType; 132 | 133 | const typedFoo : string = injector.get('foo'); 134 | const maybeBar = injector.get('bar', false); 135 | 136 | // then 137 | expect(foo).to.eql('foo-value'); 138 | expect(_bar).to.eql('bar-value'); 139 | expect(foop).to.eql('bar-value'); 140 | 141 | expect(maybeBar!.charAt(0)).to.eql('b'); 142 | expect(typedFoo).to.eql('foo-value'); 143 | 144 | expect(bub).to.be.an.instanceof(BubType); 145 | expect(bub.bar).to.eql('bar-value'); 146 | 147 | expect(baz).to.be.an.instanceof(BazType); 148 | expect(baz.name).to.eql('baz'); 149 | }); 150 | 151 | 152 | it('should get nested', function() { 153 | 154 | // given 155 | const injector = new Injector([ 156 | { 157 | __exports__: [ 'bub' ], 158 | __modules__: [ 159 | { 160 | bar: [ 'value', 'bar-value' ] 161 | } 162 | ], 163 | bub: [ 'type', BubType ] 164 | } 165 | ]); 166 | 167 | // when 168 | const bub = injector.get('bub'); 169 | 170 | expect(() => { 171 | injector.get('bar', true); 172 | }).to.throw(/No provider for "bar"!/); 173 | 174 | const bar = injector.get('bar', false); 175 | 176 | // then 177 | expect(bar).not.to.exist; 178 | 179 | expect(bub.bar).to.eql('bar-value'); 180 | }); 181 | 182 | 183 | it('should get dynamic', function() { 184 | 185 | // given 186 | const injector = new Injector([]); 187 | 188 | // when 189 | const get = (service: string, strict: boolean) => { 190 | return injector.get(service, strict); 191 | }; 192 | 193 | // then 194 | expect(get('bar', false)).not.to.exist; 195 | }); 196 | 197 | }); 198 | 199 | 200 | describe('#init', function() { 201 | 202 | it('should initialize', function() { 203 | 204 | // given 205 | const loaded : string[] = []; 206 | 207 | const injector = new Injector([ 208 | { 209 | __init__: [ () => loaded.push('first') ] 210 | }, 211 | { 212 | __init__: [ () => loaded.push('second') ] 213 | } 214 | ]); 215 | 216 | // when 217 | injector.init(); 218 | 219 | // then 220 | expect(loaded).to.eql([ 221 | 'first', 222 | 'second' 223 | ]); 224 | }); 225 | 226 | 227 | it('should load dependent modules', function() { 228 | 229 | // given 230 | const loaded : string[] = []; 231 | 232 | const injector = new Injector([ 233 | { 234 | __depends__: [ 235 | { 236 | __init__: [ () => loaded.push('dep') ] 237 | } 238 | ], 239 | __init__: [ () => loaded.push('module') ] 240 | } 241 | ]); 242 | 243 | // when 244 | injector.init(); 245 | 246 | // then 247 | expect(loaded).to.eql([ 248 | 'dep', 249 | 'module' 250 | ]); 251 | }); 252 | 253 | }); 254 | 255 | 256 | describe('#invoke', function() { 257 | 258 | it('should invoke', function() { 259 | 260 | // given 261 | const injector = new Injector([ 262 | { 263 | one: [ 'value', 1 ], 264 | two: [ 'value', 2 ] 265 | } 266 | ]); 267 | 268 | type Four = { 269 | four: number; 270 | }; 271 | 272 | type Five = { 273 | five: number; 274 | }; 275 | 276 | // when 277 | // then 278 | expect(injector.invoke((one, two) => { 279 | return one + two; 280 | })).to.eql(3); 281 | 282 | expect(injector.invoke((one, two, three) => { 283 | return one + two + three; 284 | }, null, { three: 3 })).to.eql(6); 285 | 286 | expect(injector.invoke(function(this: Four, one, two, three) { 287 | return one + two + three + this.four; 288 | }, { four: 4 }, { three: 3 })).to.eql(10); 289 | 290 | const result = injector.invoke(function() : Five { 291 | 292 | const five : Five = { 293 | five: 5 294 | }; 295 | 296 | return five; 297 | }); 298 | 299 | expect(result.five).to.eql(5); 300 | 301 | expect(injector.invoke(() => {})).not.to.exist; 302 | 303 | }); 304 | 305 | }); 306 | 307 | 308 | describe('#instantiate', function() { 309 | 310 | it('should instantiate Class', function() { 311 | 312 | // given 313 | const injector = new Injector([ 314 | { 315 | one: [ 'value', 1 ], 316 | two: [ 'value', 2 ] 317 | } 318 | ]); 319 | 320 | class Foo { 321 | one: number; 322 | 323 | constructor(one: number) { 324 | this.one = one; 325 | } 326 | } 327 | 328 | // when 329 | const fooInstance = injector.instantiate(Foo); 330 | 331 | // then 332 | expect(fooInstance.one).to.eql(1); 333 | }); 334 | 335 | }); 336 | 337 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "checkJs": true, 6 | "rootDir": ".", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "resolveJsonModule": true 10 | }, 11 | "exclude": [ 12 | "dist", 13 | "coverage", 14 | ".nyc_output" 15 | ] 16 | } 17 | --------------------------------------------------------------------------------