├── src ├── index.js ├── provider.js ├── index.d.ts ├── provider.test.js └── test.ts ├── assets └── compiler-ts.png ├── rollup.js ├── .github └── workflows │ └── test.yml ├── LICENSE ├── package.json ├── .gitignore └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './provider.js'; 2 | -------------------------------------------------------------------------------- /assets/compiler-ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenzofox3/dismoi/main/assets/compiler-ts.png -------------------------------------------------------------------------------- /rollup.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: './src/index.js', 3 | output: [ 4 | { 5 | file: './dist/index.cjs', 6 | format: 'cjs', 7 | }, 8 | { file: './dist/index.js', format: 'es' }, 9 | ], 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | all: 17 | name: build and test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - name: Install 25 | run: npm ci 26 | - name: Build 27 | run: npm run build 28 | - name: Test 29 | run: npm t 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RENARD Laurent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dismoi", 3 | "version": "0.3.5", 4 | "description": "dependency injection for javascript projects", 5 | "main": "dist/index.cjs", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test:unit": "pta", 9 | "test:types": "tsc src/test.ts --noEmit", 10 | "test": "npm run test:unit && npm run test:types", 11 | "build": "rollup -c rollup.js && cp src/index.d.ts dist/ && cp src/index.d.ts dist/index.d.cts" 12 | }, 13 | "type": "module", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/lorenzofox3/dismoi.git" 17 | }, 18 | "keywords": [ 19 | "di", 20 | "dependency", 21 | "injection", 22 | "provide", 23 | "dependency injection", 24 | "ioc" 25 | ], 26 | "files": [ 27 | "dist" 28 | ], 29 | "exports": { 30 | "./package.json": "./package.json", 31 | ".": { 32 | "import": { 33 | "default": "./dist/index.js", 34 | "types": "./dist/index.d.ts" 35 | }, 36 | "require": { 37 | "default": "./dist/index.cjs", 38 | "types": "./dist/index.d.cts" 39 | } 40 | } 41 | }, 42 | "prettier": { 43 | "singleQuote": true 44 | }, 45 | "author": "Laurent RENARD", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/lorenzofox3/dismoi/issues" 49 | }, 50 | "homepage": "https://github.com/lorenzofox3/dismoi#readme", 51 | "devDependencies": { 52 | "prettier": "^3.2.5", 53 | "pta": "^1.2.0", 54 | "rollup": "^4.17.2", 55 | "typescript": "^5.4.5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/provider.js: -------------------------------------------------------------------------------- 1 | const mapValues = (mapFn) => (source) => 2 | Object.fromEntries( 3 | [ 4 | ...Object.getOwnPropertyNames(source), 5 | ...Object.getOwnPropertySymbols(source), 6 | ].map((key) => [key, mapFn(source[key], key)]) 7 | ); 8 | 9 | export const valueFn = (val) => () => val; 10 | 11 | export const fromClass = (Klass) => (deps) => new Klass(deps); 12 | 13 | export const provideSymbol = Symbol('provide'); 14 | 15 | export const singleton = (factory) => { 16 | let instance; 17 | return (...args) => { 18 | if (instance) { 19 | return instance; 20 | } 21 | return (instance = factory(...args)); 22 | }; 23 | }; 24 | 25 | export const createProvider = ({ injectables, api = [] }) => { 26 | return function provide(externalDeps = {}) { 27 | const _injectables = new Proxy( 28 | { 29 | ...injectables, 30 | [provideSymbol]: valueFn((subArgs = {}) => 31 | provide({ 32 | ...externalDeps, 33 | ...subArgs, 34 | }) 35 | ), 36 | ...externalDeps, 37 | }, 38 | { 39 | get(target, prop, receiver) { 40 | if (!(prop in target)) { 41 | throw new Error( 42 | `could not resolve injectable with injection token "${String( 43 | prop 44 | )}"` 45 | ); 46 | } 47 | return Reflect.get(target, prop, receiver); 48 | }, 49 | } 50 | ); 51 | 52 | const mapWithPropertyDescriptor = mapValues((factory, key) => { 53 | const _factory = 54 | typeof factory === 'function' ? factory : valueFn(factory); 55 | return { 56 | get() { 57 | return _factory(_injectables); 58 | }, 59 | enumerable: api.includes(key), 60 | }; 61 | }); 62 | 63 | const properties = mapWithPropertyDescriptor(_injectables); 64 | return Object.defineProperties(_injectables, properties); 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .idea 133 | .DS_Store 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dismoi 2 | 3 | [Lightweight (less than 1kb of code)](https://bundlephobia.com/package/dismoi) declarative Dependency Injection library for Javascript on any platform. 4 | 5 | The dependency registry is centralized which leads to a good type inference and the ability to type check the dependency graph before runtime. 6 | 7 | ## Installation 8 | 9 | Nodejs: 10 | 11 | ``npm install --save dismoi`` 12 | 13 | Browser (CDN) 14 | 15 | ```HTML 16 | 19 | ``` 20 | 21 | (replace 0.0.1 by the appropriate version) 22 | 23 | ## Usage 24 | 25 | ### Define your module 26 | 27 | You define a registry of *injectable* items within a flat object whose keys (strings or Symbols) are the lookup tokens and values are *factories* to instantiate those items. 28 | 29 | A factory must have the following signature 30 | 31 | ``(deps?: T) => any // returns an injectable`` 32 | 33 | ``deps`` is an object providing the named dependency map of the injectable. 34 | 35 | Alternatively it can be any value which gets automatically wrapped into a factory function. 36 | 37 | ```Javascript 38 | const token = Symbol('something'); 39 | const injectables = { 40 | [token]: ({foo}) => { return 'whathever'}, 41 | foo: ({externalThing, someValue}) => externalThing, 42 | someValue: 'something' // a value 43 | } 44 | ``` 45 | the dependency graph of your module is the following: 46 | 47 | 1. The injectable designed by the symbol token depends on ``foo`` 48 | 2. ``foo`` depends on ``externalThing`` (not provided by the module) and ``someValue`` 49 | 3. ``someValue`` always returns the string ``something`` 50 | 51 | Factories can be decorated to adapt to any instantiation pattern: 52 | 53 | ```Javascript 54 | import {fromClass, singleton} from 'dismoi'; 55 | 56 | const injectables = { 57 | foo: fromClass(class blah { 58 | constructor({depA}){}; 59 | }), 60 | depA: singleton(someFactory) // make sure someFactory only instantiate once and then returns the same instance 61 | } 62 | ``` 63 | 64 | How factories get registered in the module is left out: simple imports, to sophisticated class annotation system. 65 | 66 | ### Create a provider 67 | 68 | You pass the injectable registry to the ``createProvider`` function alongside with the injectable list you want to expose. 69 | It gives you a function to instantiate the module: 70 | 71 | Example using the injectables aforementioned 72 | ```Javascript 73 | import {createProvider} from 'dismoi'; 74 | 75 | const provide = createProvider({ 76 | injectables, 77 | api:['foo'] 78 | }); 79 | ``` 80 | 81 | You call the ``provide`` function to instantiate the module passing the missing dependencies in the graph, eventually overwriting some you have defined in the registry. 82 | 83 | ```javascript 84 | const moduleA = provide({ 85 | someValue: 'otherValue', // overwrite 86 | externalThing: 42 // required 87 | }) 88 | ``` 89 | 90 | Then injectables get instantiated lazily when required through their getter. 91 | 92 | A different instance is created each time, unless you have a "singleton" factory 93 | 94 | ```Javascript 95 | const { foo } = services; 96 | const otherFoo = services.foo; 97 | ``` 98 | 99 | An exception is thrown if some dependencies are not met. 100 | 101 | See the [extensive test suite](src/provider.test.js) for advanced usages. 102 | 103 | ## Typescript support. 104 | 105 | Typescript is well-supported and the compiler will throw if there are incompatible dependencies or if some are missing. 106 | 107 | ![typescript compiler error](assets/compiler-ts.png) 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory as defined in the injectable property: either a function with eventually a single named dependencies argument object or a value 3 | */ 4 | export type FactoryFn = FactoryLike extends (args: any) => any 5 | ? FactoryLike 6 | : () => FactoryLike; 7 | 8 | type NamedArguments = Parameters>[0]; 9 | 10 | type Defined = T extends undefined ? never : T; 11 | 12 | /** 13 | * The dependencies map of a given factory: if there is no argument, the type is an empty map 14 | */ 15 | type Dependencies = Defined>; 16 | 17 | /** 18 | * The actual injectable: ie what a factory instantiates 19 | */ 20 | export type Injectable = ReturnType>; 21 | 22 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( 23 | k: infer I 24 | ) => void 25 | ? I 26 | : never; 27 | 28 | type ObjectLike = Record; 29 | 30 | /** 31 | * All the dependencies of the declared injectables on a registry 32 | */ 33 | export type FlatDependencyTree = 34 | UnionToIntersection>; 35 | 36 | /** 37 | * All the Injectables defined by a registry 38 | */ 39 | export type InjectableMap = { 40 | [key in keyof Registry]: Injectable; 41 | }; 42 | 43 | type MaybeMet = 44 | keyof FlatDependencyTree & keyof InjectableMap; 45 | 46 | /** 47 | * Dependencies already met by the injectables themselves (union of keys) 48 | */ 49 | export type FulfilledDependencies = { 50 | [Dep in MaybeMet]: InjectableMap[Dep] extends FlatDependencyTree[Dep] 51 | ? Dep 52 | : never; 53 | }[MaybeMet]; 54 | 55 | export type ExternalDeps = Omit< 56 | FlatDependencyTree, 57 | FulfilledDependencies 58 | > & 59 | Partial>; 60 | 61 | type ModuleAPI = []> = { 62 | [injectable in PublicAPI[number]]: Injectable; 63 | }; 64 | 65 | type ProviderFnArgs = { 66 | [key in keyof ExternalDeps]: 67 | | ExternalDeps[key] 68 | | ((arg: InjectableMap) => ExternalDeps[key]); // technically there is no constraint on arg, it is quite the opposite: it may bring new constraints but this gets contrived. 69 | }; 70 | 71 | export type ProviderFn< 72 | Registry extends ObjectLike, 73 | PublicAPI extends Array = [] 74 | > = Partial> extends ExternalDeps 75 | ? (externalDeps?: ProviderFnArgs) => ModuleAPI 76 | : (externalDeps: ProviderFnArgs) => ModuleAPI; 77 | 78 | /** 79 | * If the injectable is a function, we have to wrap it in a function to avoid treating it as a factory 80 | */ 81 | type WrapFunctionInjectable = [T] extends [(...args: any[]) => any] 82 | ? (deps?: any) => T 83 | : ((deps?: any) => T) | T; 84 | 85 | /** 86 | * Checks if each injectable match the required dependencies of the entire registry 87 | */ 88 | type ValidateRegistry> = { 89 | [key in keyof Registry]: key extends keyof Deps ? WrapFunctionInjectable : Registry[key]; 90 | }; 91 | 92 | declare function valueFn(value: T): () => T; 93 | 94 | declare const provideSymbol: unique symbol; 95 | 96 | declare function singleton any>( 97 | factory: Factory 98 | ): (...args: Parameters) => ReturnType; 99 | 100 | declare function createProvider< 101 | Registry extends ObjectLike, 102 | PublicAPI extends Array = [] 103 | >(args: { 104 | injectables: ValidateRegistry; 105 | api?: PublicAPI; 106 | }): ProviderFn; 107 | 108 | declare function fromClass any>( 109 | Klass: T 110 | ): (deps: Defined[0]>) => InstanceType; 111 | -------------------------------------------------------------------------------- /src/provider.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { 3 | createProvider, 4 | fromClass, 5 | provideSymbol, 6 | singleton, 7 | } from './provider.js'; 8 | 9 | test('instantiates an injectable, calling the factory', ({ eq }) => { 10 | const provide = createProvider({ 11 | injectables: { 12 | a: () => 'a', 13 | }, 14 | }); 15 | 16 | const services = provide(); 17 | 18 | eq(services.a, 'a'); 19 | }); 20 | 21 | test('instantiates an injectable, when it is a value', ({ eq }) => { 22 | const provide = createProvider({ 23 | injectables: { 24 | a: 'a', 25 | }, 26 | }); 27 | 28 | const services = provide(); 29 | 30 | eq(services.a, 'a'); 31 | }); 32 | 33 | test('everytime the getter is called a new instance is created', ({ 34 | eq, 35 | isNot, 36 | }) => { 37 | const provide = createProvider({ 38 | injectables: { 39 | a: () => ({ prop: 'a' }), 40 | }, 41 | }); 42 | 43 | const services = provide(); 44 | 45 | const instance1 = services.a; 46 | const { a: instance2 } = services; 47 | eq(instance1, { prop: 'a' }); 48 | eq(instance2, { prop: 'a' }); 49 | isNot(instance2, instance1); 50 | }); 51 | 52 | test('singleton decorator makes sure an injectable is only instantiated once', ({ 53 | eq, 54 | is, 55 | }) => { 56 | const provider = createProvider({ 57 | injectables: { 58 | a: ({ b }) => b, 59 | b: singleton(({ c }) => ({ c })), 60 | c: 'c', 61 | }, 62 | }); 63 | 64 | const services = provider(); 65 | const instance1 = services.a; 66 | const instance2 = services.a; 67 | eq(instance1, { c: 'c' }); 68 | eq(instance2, { c: 'c' }); 69 | is(instance1, instance2); 70 | }); 71 | 72 | test('resolves dependency graph, instantiating the transitive dependencies ', ({ 73 | eq, 74 | }) => { 75 | const provide = createProvider({ 76 | injectables: { 77 | a: ({ b, c }) => b + '+' + c, 78 | b: () => 'b', 79 | c: ({ d }) => d, 80 | d: 'd', 81 | }, 82 | }); 83 | 84 | const services = provide(); 85 | eq(services.a, 'b+d'); 86 | }); 87 | 88 | test('injection tokens can be symbols', ({ eq }) => { 89 | const aSymbol = Symbol('a'); 90 | const bSymbol = Symbol('b'); 91 | const cSymbol = Symbol('c'); 92 | const dSymbol = Symbol('d'); 93 | 94 | const provide = createProvider({ 95 | injectables: { 96 | [aSymbol]: ({ [bSymbol]: b, [cSymbol]: c }) => b + '+' + c, 97 | [bSymbol]: () => 'b', 98 | [cSymbol]: ({ [dSymbol]: d }) => d, 99 | [dSymbol]: 'd', 100 | }, 101 | }); 102 | 103 | const services = provide(); 104 | eq(services[aSymbol], 'b+d'); 105 | }); 106 | 107 | test(`only instantiates an injectable when required`, ({ eq, notOk, ok }) => { 108 | let aInstantiated = false; 109 | let bInstantiated = false; 110 | let cInstantiated = false; 111 | 112 | const provide = createProvider({ 113 | injectables: { 114 | a: ({ b }) => { 115 | aInstantiated = true; 116 | return b; 117 | }, 118 | b: () => { 119 | bInstantiated = true; 120 | return 'b'; 121 | }, 122 | c: () => { 123 | cInstantiated = true; 124 | return 'c'; 125 | }, 126 | }, 127 | }); 128 | 129 | const services = provide(); 130 | const { a } = services; 131 | 132 | eq(a, 'b'); 133 | ok(aInstantiated); 134 | ok(bInstantiated); 135 | notOk(cInstantiated); 136 | 137 | const { c } = services; 138 | eq(c, 'c'); 139 | ok(cInstantiated); 140 | }); 141 | 142 | test('provide function allows late binding', ({ eq }) => { 143 | const provide = createProvider({ 144 | injectables: { 145 | a: ({ b }) => b, 146 | }, 147 | }); 148 | 149 | const { a } = provide({ b: () => 'b' }); 150 | 151 | eq(a, 'b'); 152 | }); 153 | 154 | test('provide function allows to overwrite defined injectable', ({ eq }) => { 155 | const provide = createProvider({ 156 | injectables: { 157 | a: ({ b }) => b, 158 | b: 'b', 159 | }, 160 | }); 161 | 162 | const { a } = provide({ b: `b'` }); 163 | 164 | eq(a, `b'`); 165 | }); 166 | 167 | test('gives a friendly message when it can not resolve a dependency', ({ 168 | eq, 169 | fail, 170 | }) => { 171 | const provide = createProvider({ 172 | injectables: { 173 | a: ({ b }) => b, 174 | b: ({ c }) => c, 175 | }, 176 | }); 177 | 178 | try { 179 | const { a } = provide(); 180 | fail('should not reach that statement'); 181 | } catch (err) { 182 | eq(err.message, 'could not resolve injectable with injection token "c"'); 183 | } 184 | }); 185 | 186 | test('injectable is explicitly "undefined" then it is an actual injectable value', ({ 187 | eq, 188 | }) => { 189 | const provide = createProvider({ 190 | injectables: { 191 | a: ({ b }) => b, 192 | b: ({ c }) => c, 193 | c: undefined, 194 | }, 195 | }); 196 | 197 | const { a } = provide(); 198 | eq(a, undefined); 199 | const { a: aBis } = provide({ 200 | c: ({ d }) => d, 201 | d: undefined, 202 | }); 203 | eq(aBis, undefined); 204 | }); 205 | 206 | test('provide is itself injected', ({ eq }) => { 207 | const withSession = (factory) => { 208 | return ({ [provideSymbol]: provide }) => { 209 | return factory( 210 | provide({ 211 | session: true, 212 | }) 213 | ); 214 | }; 215 | }; 216 | 217 | const provide = createProvider({ 218 | injectables: { 219 | usecaseA: withSession( 220 | ({ repository, service }) => repository + '&' + service 221 | ), 222 | usecaseB: ({ repository, service }) => repository + '&' + service, 223 | repository: ({ session }) => 224 | session ? 'repositoryWithSession' : 'repository', 225 | service: 'some_service', 226 | session: undefined, 227 | }, 228 | }); 229 | 230 | const { usecaseA, usecaseB } = provide(); 231 | 232 | eq(usecaseA, 'repositoryWithSession&some_service'); 233 | eq(usecaseB, 'repository&some_service'); 234 | }); 235 | 236 | test('provide is itself injected, so are late bindings', ({ eq }) => { 237 | const withSession = (factory) => { 238 | return ({ [provideSymbol]: provide }) => { 239 | return factory( 240 | provide({ 241 | session: true, 242 | }) 243 | ); 244 | }; 245 | }; 246 | 247 | const provide = createProvider({ 248 | injectables: { 249 | usecaseA: withSession( 250 | ({ repository, service }) => repository + '&' + service 251 | ), 252 | usecaseB: ({ repository, service }) => repository + '&' + service, 253 | repository: ({ session }) => 254 | session ? 'repositoryWithSession' : 'repository', 255 | session: undefined, 256 | // service is missing and will be late bound 257 | }, 258 | }); 259 | 260 | const { usecaseA, usecaseB } = provide({ service: 'some_service' }); 261 | 262 | eq(usecaseA, 'repositoryWithSession&some_service'); 263 | eq(usecaseB, 'repository&some_service'); 264 | }); 265 | 266 | test(`"api" defines the public API`, ({ eq }) => { 267 | const provide = createProvider({ 268 | injectables: { 269 | a: ({ b }) => b, 270 | b: 'b', 271 | c: ({ b, d }) => `${b}+${d}`, 272 | d: 'd', 273 | }, 274 | api: ['a', 'c'], 275 | }); 276 | 277 | const moduleAPI = { 278 | ...provide(), 279 | }; 280 | 281 | eq(Object.keys(moduleAPI), ['a', 'c']); 282 | }); 283 | 284 | test('"fromClass" wraps the class within a factory', ({ eq }) => { 285 | class A { 286 | constructor({ b }) { 287 | this.b = b; 288 | } 289 | 290 | test() { 291 | return this.b.value; 292 | } 293 | } 294 | 295 | class B { 296 | constructor({ c }) { 297 | this.value = c; 298 | } 299 | } 300 | 301 | const provide = createProvider({ 302 | injectables: { 303 | a: fromClass(A), 304 | b: fromClass(B), 305 | c: 'test', 306 | }, 307 | }); 308 | 309 | const { a } = provide(); 310 | 311 | eq(a.test(), 'test'); 312 | }); 313 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { 3 | ExternalDeps, 4 | FlatDependencyTree, 5 | Injectable, 6 | InjectableMap, 7 | ProviderFn, 8 | WrapFunctionInjectable, 9 | } from './index'; 10 | import { 11 | createProvider, 12 | fromClass, 13 | FulfilledDependencies, 14 | provideSymbol, 15 | } from './index'; 16 | 17 | injectables: { 18 | // result from a factory (the return value type) 19 | let injectable: Injectable<(arg: string) => number> = 42; 20 | // a value 21 | const injectableValue: Injectable = 42; 22 | // @ts-expect-error 23 | // should be a number 24 | injectable = 'foo'; 25 | 26 | let registry: InjectableMap<{ 27 | foo: (arg: any) => number; 28 | bar: (arg: any) => (x: number) => string; 29 | blah: string; 30 | }> = { 31 | foo: 42, 32 | bar: (x) => String(x), 33 | blah: 'hello', 34 | }; 35 | 36 | // @ts-expect-error 37 | // missing injectable "bar" 38 | registry = { 39 | foo: 42, 40 | blah: 'hello', 41 | }; 42 | 43 | registry = { 44 | foo: 42, 45 | // @ts-expect-error 46 | // wrong injectable type inference: blah should be string 47 | blah: 42, 48 | bar: (x: number) => String(x), 49 | }; 50 | 51 | let injectablesMap: InjectableMap<{ 52 | foo: () => number; 53 | bar: string; 54 | rich: { foo: { bar: number } }; 55 | }> = { 56 | foo: 42, 57 | bar: 'woot', 58 | rich: { 59 | foo: { bar: 34 }, 60 | }, 61 | }; 62 | 63 | injectablesMap = { 64 | foo: 42, 65 | bar: 'woot', 66 | rich: { 67 | // @ts-expect-error 68 | // wrong nested type 69 | foo: {}, 70 | }, 71 | }; 72 | 73 | // @ts-expect-error 74 | // missing dep bar 75 | injectablesMap = { 76 | foo: 42, 77 | rich: { 78 | foo: { 79 | bar: 42, 80 | }, 81 | }, 82 | }; 83 | } 84 | 85 | dependenciesTree: { 86 | const dependenciesTree: FlatDependencyTree<{ 87 | foo: (arg: { x: number; blah: string }) => any; 88 | bar: (arg: { x: number }) => any; 89 | bim: (arg: { y: string }) => any; 90 | blah: () => any; 91 | }> = { 92 | x: 42, 93 | blah: 'hello', 94 | y: 'woot', 95 | }; 96 | 97 | const dependenciesTreeImpossible: FlatDependencyTree<{ 98 | foo: (arg: { x: number; blah: string }) => any; 99 | bar: (arg: { x: string }) => any; // x is "never" 100 | bim: (arg: { y: string }) => any; 101 | }> = { 102 | // @ts-expect-error 103 | // x can't be in the same time number and string 104 | x: 42, 105 | blah: 'hello', 106 | y: 'woot', 107 | }; 108 | } 109 | 110 | fulfilledDependencies: { 111 | let fulfilled: FulfilledDependencies<{ 112 | foo: (arg: { x: number; blah: string; woot: { prop: number } }) => any; 113 | x: ({ otherThing }: { otherThing: string; y: string }) => number; 114 | woot: () => { prop: number }; 115 | }> = 'x'; 116 | fulfilled = 'woot'; 117 | 118 | // @ts-expect-error 119 | // blah is not met 120 | fulfilled = 'blah'; 121 | 122 | // @ts-expect-error 123 | // otherThing is not met 124 | fulfilled = 'otherThing'; 125 | 126 | let incompatibleInterfaces: FulfilledDependencies<{ 127 | x: (deps: { y: number; woot: { prop: { nested: number } } }) => any; 128 | y: () => string; 129 | woot: (deps: { met: string }) => { prop: { nested: string } }; 130 | met: () => string; 131 | }> = 'met'; 132 | 133 | // @ts-expect-error 134 | // y should return a number 135 | incompatibleInterfaces = 'y'; 136 | 137 | // @ts-expect-error 138 | // nested type should be number 139 | incompatibleInterfaces = 'woot'; 140 | } 141 | 142 | lateBoundDependencies: { 143 | type SampleRegistry = { 144 | foo: (deps: { x: number; dep: number }) => any; 145 | bar: (deps: { y: string; dep: number; dep2: number }) => number; 146 | dep2: number; 147 | x: () => number; 148 | }; 149 | 150 | let externalDeps: ExternalDeps; 151 | externalDeps = { 152 | y: 'hello', 153 | dep: 42, 154 | }; 155 | 156 | // overwrite already provided deps 157 | externalDeps = { 158 | y: '2354', 159 | dep: 42, 160 | x: 42, // x is optional as provided within the registry 161 | dep2: 42, // dep2 is optional as provided within the registry 162 | bar: 42, // overwrite factory 163 | }; 164 | 165 | // @ts-expect-error 166 | // missing a dependency "y" 167 | externalDeps = { 168 | dep: 42, 169 | }; 170 | 171 | // wrong type 172 | externalDeps = { 173 | y: '2354', 174 | // @ts-expect-error 175 | dep: '42', 176 | // overwrite type 177 | // @ts-expect-error 178 | x: 'hello', 179 | }; 180 | } 181 | 182 | wrapFunctionInjectable: { 183 | // test that () => SomeUnionType is assignable to WrapFunctionInjectable 184 | let unionInjectable: WrapFunctionInjectable<'a' | 'b'> = (): 'a' | 'b' => 'a'; 185 | } 186 | 187 | publicAPI: { 188 | let publicAPI: ProviderFn< 189 | { 190 | foo: (x: any) => number; 191 | bar: (x: any) => string; 192 | woot: (x: any) => number; 193 | }, 194 | ['foo', 'woot'] 195 | >; 196 | 197 | // only foo and woot are required 198 | publicAPI = () => ({ 199 | foo: 42, 200 | woot: 42, 201 | }); 202 | } 203 | 204 | createProvider: { 205 | // provideSymbol is not a mandatory dep 206 | createProvider({ 207 | injectables: { 208 | foo: ({ [provideSymbol]: provide }) => 42, 209 | }, 210 | })({}); 211 | 212 | createProvider({ 213 | injectables: { 214 | foo: ({ a }: { a: number }) => a, 215 | bar: ({ b }: { b: string }) => b, 216 | baz: ({ c }: { c: () => boolean }) => c(), 217 | // @ts-expect-error a is not a number 218 | a: "42", 219 | // @ts-expect-error b is not a string 220 | b: () => 42 as number, 221 | // @ts-expect-error c has to be wrapped in a function 222 | c: () => true, 223 | }, 224 | api: ['foo', 'bar', 'baz'], 225 | }); 226 | 227 | createProvider({ 228 | injectables: { 229 | a: ({ c }: { c: number }) => c, 230 | b: ({ c }: { c: string }) => c, 231 | // @ts-expect-error c does not satisfy a & b 232 | c: 42, 233 | }, 234 | api: ['a', 'c'], 235 | }); 236 | 237 | // when all dependencies are provide, external Deps is optional 238 | const provideFulfilled = createProvider({ 239 | injectables: { 240 | foo: ({ val }: { val: number }) => val, 241 | val: 42, 242 | }, 243 | api: ['foo'], 244 | }); 245 | provideFulfilled(); 246 | provideFulfilled({}); 247 | provideFulfilled({ val: 72 }); 248 | 249 | const provideMissing = createProvider({ 250 | injectables: { 251 | foo: ({ val }: { val: number }) => val, 252 | }, 253 | api: ['foo'], 254 | }); 255 | provideMissing({ val: 72 }); 256 | provideMissing({ val: () => 42 }); 257 | // @ts-expect-error 258 | // missing required deps "val" 259 | provideMissing(); 260 | // @ts-expect-error 261 | provideMissing({}); 262 | 263 | const provideDeepMissing = createProvider({ 264 | injectables: { 265 | foo: ({ service }: { service: number }) => service, 266 | service: ({ nonTypedDep }) => nonTypedDep, 267 | bar: ({ typedDep }: { typedDep: string }) => typedDep, 268 | }, 269 | api: ['foo', 'bar'], 270 | }); 271 | provideDeepMissing({ typedDep: 'toto', nonTypedDep: 42 }); 272 | // @ts-expect-error typedDep & nonTypedDep is missing here 273 | provideDeepMissing(); 274 | // @ts-expect-error typedDep & nonTypedDep is missing here 275 | provideDeepMissing({}); 276 | 277 | const provideWrongType = createProvider({ 278 | injectables: { 279 | foo: ({ val }: { val: number }) => val, 280 | }, 281 | api: ['foo'], 282 | }); 283 | // @ts-expect-error wrong dependency type 284 | provideWrongType({ val: "42" }) 285 | } 286 | 287 | fromClass: { 288 | class Foo { 289 | constructor({ b }: { b: string }) { } 290 | } 291 | let factory = fromClass(Foo); 292 | factory({ b: 'woot' }); 293 | // @ts-expect-error 294 | // wrong dependency type 295 | factory({ c: 42 }); 296 | 297 | // @ts-expect-error 298 | // not a class 299 | fromClass(42); 300 | // @ts-expect-error 301 | // not a class 302 | fromClass(() => 42); 303 | } 304 | 305 | issue4: { 306 | let injectables = { 307 | a: ({ value }: { value: number }) => value + 10, 308 | intermediate: () => '120', 309 | }; 310 | const provideMissingWithIntermediate = createProvider({ 311 | injectables: injectables, 312 | api: ['a'] 313 | }); 314 | 315 | 316 | provideMissingWithIntermediate({ 317 | value: ({ intermediate }: { intermediate: string }) => Number(intermediate) 318 | }) 319 | } 320 | --------------------------------------------------------------------------------