├── .editorconfig ├── .github └── workflows │ ├── checks.yml │ ├── labels.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── benchmarks.png ├── benchmarks ├── module_expression.ts ├── module_importer.ts └── services │ ├── comments.ts │ ├── posts.ts │ ├── thread.ts │ └── users.ts ├── bin └── test.ts ├── eslint.config.js ├── index.ts ├── package.json ├── src ├── container.ts ├── contextual_bindings_builder.ts ├── debug.ts ├── decorators │ └── inject.ts ├── deferred_promise.ts ├── helpers.ts ├── module_caller.ts ├── module_expression.ts ├── module_importer.ts ├── provider.ts ├── resolver.ts └── types.ts ├── tests ├── container │ ├── bindings.spec.ts │ ├── call_method.spec.ts │ ├── events.spec.ts │ ├── hooks.spec.ts │ ├── known_bindings.spec.ts │ ├── known_make_class.spec.ts │ ├── make_class.spec.ts │ ├── make_class_via_inject.spec.ts │ └── swap.spec.ts ├── enqueue.spec.ts ├── module_caller │ ├── to_callable.spec.ts │ └── to_handle_method.spec.ts ├── module_expression │ ├── parse.spec.ts │ ├── to_callable.spec.ts │ └── to_handle_method.spec.ts ├── module_importer │ ├── to_callable.spec.ts │ └── to_handle_method.spec.ts ├── provider.spec.ts └── resolver.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | on: 3 | - push 4 | - pull_request 5 | - workflow_call 6 | 7 | jobs: 8 | test: 9 | uses: adonisjs/.github/.github/workflows/test.yml@next 10 | 11 | lint: 12 | uses: adonisjs/.github/.github/workflows/lint.yml@next 13 | 14 | typecheck: 15 | uses: adonisjs/.github/.github/workflows/typecheck.yml@next 16 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | issues: write 6 | jobs: 7 | labels: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: EndBug/label-sync@v2 12 | with: 13 | config-file: 'https://raw.githubusercontent.com/thetutlage/static/main/labels.yml' 14 | delete-other-labels: true 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: workflow_dispatch 3 | permissions: 4 | contents: write 5 | id-token: write 6 | jobs: 7 | checks: 8 | uses: ./.github/workflows/checks.yml 9 | release: 10 | needs: checks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - name: git config 20 | run: | 21 | git config user.name "${GITHUB_ACTOR}" 22 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 23 | - name: Init npm config 24 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 25 | env: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | - run: npm install 28 | - run: npm run release -- --ci 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue' 13 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request' 14 | close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue' 15 | close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request' 16 | days-before-stale: 21 17 | days-before-close: 5 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | yarn.lock 13 | shrinkwrap.yaml 14 | package-lock.json 15 | test/__app 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.html 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2023 Harminder Virk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adonisjs/fold/88f0f6ba2e0c16e7290270308795358bc98d3553/benchmarks.png -------------------------------------------------------------------------------- /benchmarks/module_expression.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | // @ts-expect-error 11 | import benchmark from 'benchmark' 12 | import Thread from '#services/thread' 13 | import { Container } from '../index.js' 14 | import { resolveDefault } from '../src/helpers.js' 15 | import { moduleExpression } from '../src/module_expression.js' 16 | 17 | const suite = new benchmark.Suite() 18 | 19 | /** 20 | * Our implementation that returns a callable function 21 | */ 22 | const fn = moduleExpression('#services/users.find', import.meta.url).toCallable() 23 | 24 | /** 25 | * Our implementation that returns an object with handle method. 26 | */ 27 | const handler = moduleExpression('#services/posts.find', import.meta.url).toHandleMethod() 28 | 29 | /** 30 | * If we decided not to have cached version and rely on dynamic imports 31 | * all the time 32 | */ 33 | const native = async (resolver: Container) => { 34 | const defaultExport = await resolveDefault('#services/comments', import.meta.url) 35 | return resolver.call(await resolver.make(defaultExport), 'find') 36 | } 37 | 38 | /** 39 | * What if there was were dynamic imports in first place and we were 40 | * just importing everything inline in one place. 41 | */ 42 | const inline = async (resolver: Container) => { 43 | return resolver.call(await resolver.make(Thread), 'find') 44 | } 45 | 46 | const container = new Container() 47 | 48 | suite 49 | .add('handler', { 50 | defer: true, 51 | fn: (deferred: any) => { 52 | handler.handle(container, []).then(() => deferred.resolve()) 53 | }, 54 | }) 55 | .add('callable', { 56 | defer: true, 57 | fn: (deferred: any) => { 58 | fn(container, []).then(() => deferred.resolve()) 59 | }, 60 | }) 61 | .add('native dynamic', { 62 | defer: true, 63 | fn: (deferred: any) => { 64 | native(container).then(() => deferred.resolve()) 65 | }, 66 | }) 67 | .add('inline', { 68 | defer: true, 69 | fn: (deferred: any) => { 70 | inline(container).then(() => deferred.resolve()) 71 | }, 72 | }) 73 | .on('cycle', function (event: any) { 74 | console.log(String(event.target)) 75 | }) 76 | .on('complete', function (this: any) { 77 | console.log('Fastest is ' + this.filter('fastest').map('name')) 78 | }) 79 | .run({ async: true }) 80 | -------------------------------------------------------------------------------- /benchmarks/module_importer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | // @ts-expect-error 11 | import benchmark from 'benchmark' 12 | import { importDefault } from '@poppinss/utils' 13 | 14 | import Thread from '#services/thread' 15 | import { Container } from '../index.js' 16 | import { moduleImporter } from '../src/module_importer.js' 17 | 18 | const suite = new benchmark.Suite() 19 | 20 | /** 21 | * Our implementation that returns a callable function 22 | */ 23 | const fn = moduleImporter(() => import('#services/users'), 'find').toCallable() 24 | 25 | /** 26 | * Our implementation that returns an object with handle method. 27 | */ 28 | const handler = moduleImporter(() => import('#services/posts'), 'find').toHandleMethod() 29 | 30 | /** 31 | * If we decided not to have cached version and rely on dynamic imports 32 | * all the time 33 | */ 34 | const native = async (resolver: Container) => { 35 | const defaultExport = await importDefault(() => import('#services/comments')) 36 | return resolver.call(await resolver.make(defaultExport), 'find') 37 | } 38 | 39 | /** 40 | * What if there was were dynamic imports in first place and we were 41 | * just importing everything inline in one place. 42 | */ 43 | const inline = async (resolver: Container) => { 44 | return resolver.call(await resolver.make(Thread), 'find') 45 | } 46 | 47 | const container = new Container() 48 | 49 | suite 50 | .add('handler', { 51 | defer: true, 52 | fn: (deferred: any) => { 53 | handler.handle(container, []).then(() => deferred.resolve()) 54 | }, 55 | }) 56 | .add('callable', { 57 | defer: true, 58 | fn: (deferred: any) => { 59 | fn(container, []).then(() => deferred.resolve()) 60 | }, 61 | }) 62 | .add('native dynamic', { 63 | defer: true, 64 | fn: (deferred: any) => { 65 | native(container).then(() => deferred.resolve()) 66 | }, 67 | }) 68 | .add('inline', { 69 | defer: true, 70 | fn: (deferred: any) => { 71 | inline(container).then(() => deferred.resolve()) 72 | }, 73 | }) 74 | .on('cycle', function (event: any) { 75 | console.log(String(event.target)) 76 | }) 77 | .on('complete', function (this: any) { 78 | console.log('Fastest is ' + this.filter('fastest').map('name')) 79 | }) 80 | .run({ async: true }) 81 | -------------------------------------------------------------------------------- /benchmarks/services/comments.ts: -------------------------------------------------------------------------------- 1 | export default class CommentsService { 2 | async find() {} 3 | } 4 | -------------------------------------------------------------------------------- /benchmarks/services/posts.ts: -------------------------------------------------------------------------------- 1 | export default class PostsService { 2 | async find() {} 3 | } 4 | -------------------------------------------------------------------------------- /benchmarks/services/thread.ts: -------------------------------------------------------------------------------- 1 | export default class ThreadService { 2 | async find() {} 3 | } 4 | -------------------------------------------------------------------------------- /benchmarks/services/users.ts: -------------------------------------------------------------------------------- 1 | export default class UsersService { 2 | async find() {} 3 | } 4 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '@japa/assert' 2 | import { fileSystem } from '@japa/file-system' 3 | import { processCLIArgs, configure, run } from '@japa/runner' 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Configure tests 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The configure method accepts the configuration to configure the Japa 11 | | tests runner. 12 | | 13 | | The first method call "processCliArgs" process the command line arguments 14 | | and turns them into a config object. Using this method is not mandatory. 15 | | 16 | | Please consult japa.dev/runner-config for the config docs. 17 | */ 18 | processCLIArgs(process.argv.slice(2)) 19 | configure({ 20 | files: ['tests/**/*.spec.ts'], 21 | plugins: [assert(), fileSystem()], 22 | }) 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Run tests 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The following "run" method is required to execute all the tests. 30 | | 31 | */ 32 | run() 33 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { configPkg } from '@adonisjs/eslint-config' 2 | 3 | export default configPkg({ 4 | ignores: ['coverage'], 5 | }) 6 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | export { Container } from './src/container.js' 11 | export { inject } from './src/decorators/inject.js' 12 | export { moduleCaller } from './src/module_caller.js' 13 | export { ContainerResolver } from './src/resolver.js' 14 | export { moduleImporter } from './src/module_importer.js' 15 | export { moduleExpression } from './src/module_expression.js' 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/fold", 3 | "version": "10.1.3", 4 | "description": "Simplest and straightforward implementation of IoC container in JavaScript", 5 | "type": "module", 6 | "main": "build/index.js", 7 | "files": [ 8 | "build", 9 | "!build/benchmarks", 10 | "!build/bin", 11 | "!build/tests" 12 | ], 13 | "engines": { 14 | "node": ">=18.16.0" 15 | }, 16 | "imports": { 17 | "#controllers/*": "./tests/app/controllers/*.js", 18 | "#middleware/*": "./tests/app/middleware/*.js", 19 | "#services/*": "./build/benchmarks/services/*.js" 20 | }, 21 | "exports": { 22 | ".": "./build/index.js", 23 | "./types": "./build/src/types.js" 24 | }, 25 | "scripts": { 26 | "pretest": "npm run lint", 27 | "test": "cross-env NODE_DEBUG=adonisjs:fold c8 npm run quick:test", 28 | "quick:test": "node --import=@poppinss/ts-exec bin/test.ts", 29 | "citgm": "cross-env FORCE_COLOR=0 npm run quick:test", 30 | "clean": "del-cli build", 31 | "benchmark": "npm run build && node build/benchmarks/module_expression.js && node build/benchmarks/module_importer.js", 32 | "typecheck": "tsc --noEmit", 33 | "precompile": "npm run lint && npm run clean", 34 | "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", 35 | "build": "npm run compile", 36 | "release": "release-it", 37 | "version": "npm run build", 38 | "prepublishOnly": "npm run build", 39 | "lint": "eslint .", 40 | "format": "prettier --write ." 41 | }, 42 | "dependencies": { 43 | "@poppinss/utils": "^7.0.0-next.1" 44 | }, 45 | "devDependencies": { 46 | "@adonisjs/eslint-config": "^2.0.0", 47 | "@adonisjs/prettier-config": "^1.4.4", 48 | "@adonisjs/tsconfig": "^1.4.0", 49 | "@japa/assert": "^4.0.1", 50 | "@japa/file-system": "^2.3.2", 51 | "@japa/runner": "^4.2.0", 52 | "@poppinss/ts-exec": "^1.2.0", 53 | "@release-it/conventional-changelog": "^10.0.1", 54 | "@types/node": "^22.15.21", 55 | "benchmark": "^2.1.4", 56 | "c8": "^10.1.3", 57 | "cross-env": "^7.0.3", 58 | "del-cli": "^6.0.0", 59 | "eslint": "^9.27.0", 60 | "expect-type": "^1.2.1", 61 | "p-event": "^6.0.1", 62 | "prettier": "^3.5.3", 63 | "reflect-metadata": "^0.2.2", 64 | "release-it": "^19.0.2", 65 | "tsup": "^8.5.0", 66 | "typescript": "~5.8.3" 67 | }, 68 | "license": "MIT", 69 | "keywords": [ 70 | "ioc", 71 | "container", 72 | "dependency-injection", 73 | "di" 74 | ], 75 | "author": "Harminder Virk ", 76 | "publishConfig": { 77 | "access": "public", 78 | "tag": "latest" 79 | }, 80 | "directories": { 81 | "test": "tests" 82 | }, 83 | "release-it": { 84 | "git": { 85 | "requireCleanWorkingDir": true, 86 | "requireUpstream": true, 87 | "commitMessage": "chore(release): ${version}", 88 | "tagAnnotation": "v${version}", 89 | "push": true, 90 | "tagName": "v${version}" 91 | }, 92 | "github": { 93 | "release": true 94 | }, 95 | "npm": { 96 | "publish": true, 97 | "skipChecks": true 98 | }, 99 | "plugins": { 100 | "@release-it/conventional-changelog": { 101 | "preset": { 102 | "name": "angular" 103 | } 104 | } 105 | } 106 | }, 107 | "repository": { 108 | "type": "git", 109 | "url": "git+https://github.com/adonisjs/fold.git" 110 | }, 111 | "bugs": { 112 | "url": "https://github.com/adonisjs/fold/issues" 113 | }, 114 | "homepage": "https://github.com/adonisjs/fold#readme", 115 | "commitlint": { 116 | "extends": [ 117 | "@commitlint/config-conventional" 118 | ] 119 | }, 120 | "c8": { 121 | "reporter": [ 122 | "text", 123 | "html" 124 | ], 125 | "exclude": [ 126 | "tests/**" 127 | ] 128 | }, 129 | "prettier": "@adonisjs/prettier-config", 130 | "tsup": { 131 | "entry": [ 132 | "./index.ts", 133 | "./benchmarks/module_expression.ts", 134 | "./benchmarks/module_importer.ts", 135 | "./benchmarks/services/comments.ts", 136 | "./benchmarks/services/posts.ts", 137 | "./benchmarks/services/thread.ts", 138 | "./benchmarks/services/users.ts", 139 | "./src/types.ts" 140 | ], 141 | "outDir": "./build", 142 | "clean": true, 143 | "format": "esm", 144 | "dts": false, 145 | "sourcemap": false, 146 | "target": "esnext" 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/container.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { inspect } from 'node:util' 11 | import { InvalidArgumentsException } from '@poppinss/utils/exception' 12 | import type { AbstractConstructor, Constructor, ExtractFunctions } from '@poppinss/utils/types' 13 | 14 | import type { 15 | Make, 16 | Hooks, 17 | Swaps, 18 | Bindings, 19 | BindingKey, 20 | ErrorCreator, 21 | HookCallback, 22 | BindingValues, 23 | BindingResolver, 24 | ContainerOptions, 25 | ContextualBindings, 26 | } from './types.js' 27 | 28 | import debug from './debug.js' 29 | import { enqueue, isClass } from './helpers.js' 30 | import { ContainerResolver } from './resolver.js' 31 | import { ContextBindingsBuilder } from './contextual_bindings_builder.js' 32 | 33 | /** 34 | * The container class exposes the API to register bindings, values 35 | * and resolve them. 36 | * 37 | * Known bindings types can be defined at the time of the constructing 38 | * the container. 39 | * 40 | * ```ts 41 | * new Container<{ 'route': Route, encryption: Encryption }>() 42 | * ``` 43 | * 44 | * You can resolve bindings and construct classes as follows 45 | * 46 | * ```ts 47 | * await container.make(BINDING_NAME) 48 | * await container.make(CLASS_CONSTRUCTOR) 49 | * ``` 50 | */ 51 | export class Container> { 52 | /** 53 | * A set of defined aliases for the bindings 54 | */ 55 | #aliases: Map, keyof KnownBindings | AbstractConstructor> = 56 | new Map() 57 | 58 | /** 59 | * Contextual bindings are same as binding, but instead defined 60 | * for a parent class constructor. 61 | * 62 | * The contextual bindings can only be registered for class constructors, because 63 | * that is what gets injected to the class. 64 | */ 65 | #contextualBindings: Map, ContextualBindings> = new Map() 66 | 67 | /** 68 | * A collection of bindings with registered swapped implementations. Swaps can only 69 | * be define for a class, because the goal is swap the dependency tree defined 70 | * using the Inject decorator and inject decorator does not take anything 71 | * other than a class. 72 | */ 73 | #swaps: Swaps = new Map() 74 | 75 | /** 76 | * Registered bindings. Singleton and normal bindings, both are 77 | * registered inside the bindings map 78 | */ 79 | #bindings: Bindings = new Map() 80 | 81 | /** 82 | * Registered bindings as values. The values are preferred over the bindings. 83 | */ 84 | #bindingValues: BindingValues = new Map() 85 | 86 | /** 87 | * Registered hooks. 88 | */ 89 | #hooks: Hooks = new Map() 90 | 91 | /** 92 | * Container options 93 | */ 94 | #options: ContainerOptions 95 | 96 | constructor(options?: ContainerOptions) { 97 | this.#options = options || {} 98 | } 99 | 100 | /** 101 | * Define an emitter instance to use 102 | */ 103 | useEmitter(emitter: Exclude) { 104 | this.#options.emitter = emitter 105 | return this 106 | } 107 | 108 | /** 109 | * Create a container resolver to resolve bindings, or make classes. 110 | * 111 | * ```ts 112 | * const resolver = container.createResolver() 113 | * await resolver.make(CLASS_CONSTRUCTOR) 114 | * ``` 115 | * 116 | * Bind values with the resolver. Resolver values are isolated from the 117 | * container. 118 | * 119 | * ```ts 120 | * resolver.bindValue(HttpContext, new HttpContext()) 121 | * await resolver.make(UsersController) 122 | * ``` 123 | */ 124 | createResolver() { 125 | return new ContainerResolver( 126 | { 127 | bindings: this.#bindings, 128 | bindingValues: this.#bindingValues, 129 | swaps: this.#swaps, 130 | hooks: this.#hooks, 131 | aliases: this.#aliases, 132 | contextualBindings: this.#contextualBindings, 133 | }, 134 | this.#options 135 | ) 136 | } 137 | 138 | /** 139 | * Find if the container has a binding registered using the 140 | * "bind", the "singleton", or the "bindValue" methods. 141 | */ 142 | hasBinding(binding: Binding): boolean 143 | hasBinding(binding: BindingKey): boolean 144 | hasBinding(binding: BindingKey): boolean { 145 | return ( 146 | this.#aliases.has(binding) || this.#bindingValues.has(binding) || this.#bindings.has(binding) 147 | ) 148 | } 149 | 150 | /** 151 | * Find if the container has all the bindings registered using the 152 | * "bind", the "singleton", or the "bindValue" methods. 153 | */ 154 | hasAllBindings(bindings: Binding[]): boolean 155 | hasAllBindings(binding: BindingKey[]): boolean 156 | hasAllBindings(bindings: BindingKey[]): boolean { 157 | return bindings.every((binding) => this.hasBinding(binding)) 158 | } 159 | 160 | /** 161 | * Resolves the binding or constructor a class instance as follows. 162 | * 163 | * - Resolve the binding from the values (if registered) 164 | * - Resolve the binding from the bindings (if registered) 165 | * - If binding is a class, then create a instance of it. The constructor 166 | * dependencies are further resolved as well. 167 | * - All other values are returned as it is. 168 | * 169 | * ```ts 170 | * await container.make('route') 171 | * await container.make(Database) 172 | * ``` 173 | */ 174 | make( 175 | binding: Binding, 176 | runtimeValues?: any[], 177 | createError?: ErrorCreator 178 | ): Promise> 179 | make( 180 | binding: Binding, 181 | runtimeValues?: any[], 182 | createError?: ErrorCreator 183 | ): Promise> 184 | make( 185 | binding: Binding, 186 | runtimeValues?: any[], 187 | createError?: ErrorCreator 188 | ): Promise> { 189 | return this.createResolver().make(binding, runtimeValues, createError) 190 | } 191 | 192 | /** 193 | * Call a method on an object by injecting its dependencies. The method 194 | * dependencies are resolved in the same manner as a class constructor 195 | * dependencies. 196 | * 197 | * ```ts 198 | * await container.call(await container.make(UsersController), 'index') 199 | * ``` 200 | */ 201 | call, Method extends ExtractFunctions>( 202 | value: Value, 203 | method: Method, 204 | runtimeValues?: any[], 205 | createError?: ErrorCreator 206 | ): Promise> { 207 | return this.createResolver().call(value, method, runtimeValues, createError) 208 | } 209 | 210 | /** 211 | * Register an alias for a binding. The value can be a reference 212 | * to an existing binding or to a class constructor that will 213 | * instantiate to the same value as the alias. 214 | */ 215 | alias( 216 | /** 217 | * An alias must always be defined as a string or a symbol. Classes cannot be 218 | * aliases 219 | */ 220 | alias: Alias extends string | symbol ? Alias : never, 221 | 222 | /** 223 | * The value should either be the constructor point to the alias value 224 | * or reference to binding that has the same value as the alias 225 | */ 226 | value: 227 | | AbstractConstructor 228 | | Exclude< 229 | { 230 | [K in keyof KnownBindings]: KnownBindings[K] extends KnownBindings[Alias] ? K : never 231 | }[keyof KnownBindings], 232 | Alias 233 | > 234 | ): void { 235 | if (typeof alias !== 'string' && typeof alias !== 'symbol') { 236 | throw new InvalidArgumentsException( 237 | 'The container alias key must be of type "string" or "symbol"' 238 | ) 239 | } 240 | 241 | this.#aliases.set(alias, value) 242 | } 243 | 244 | /** 245 | * Register a binding inside the container. The method receives a 246 | * key-value pair. 247 | * 248 | * - Key can be a string, symbol or a constructor. 249 | * - The value is always a factory function to construct the dependency. 250 | * 251 | * ```ts 252 | * container.bind('route', () => new Route()) 253 | * await container.make('route') 254 | * 255 | * container.bind(Route, () => new Route()) 256 | * await container.make(Route) 257 | * 258 | * const routeSymbol = Symbol('route') 259 | * container.bind(routeSymbol, () => new Route()) 260 | * await container.make(routeSymbol) 261 | * ``` 262 | */ 263 | bind( 264 | /** 265 | * Need to narrow down the "Binding" for the case where "KnownBindings" are 266 | */ 267 | binding: Binding extends string | symbol ? Binding : never, 268 | resolver: BindingResolver 269 | ): void 270 | bind>( 271 | binding: Binding, 272 | resolver: BindingResolver> 273 | ): void 274 | bind( 275 | binding: Binding, 276 | resolver: BindingResolver< 277 | KnownBindings, 278 | Binding extends AbstractConstructor 279 | ? A 280 | : Binding extends keyof KnownBindings 281 | ? KnownBindings[Binding] 282 | : never 283 | > 284 | ): void { 285 | if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isClass(binding)) { 286 | throw new InvalidArgumentsException( 287 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 288 | ) 289 | } 290 | 291 | debug('adding binding to container "%O"', binding) 292 | this.#bindings.set(binding, { resolver, isSingleton: false }) 293 | } 294 | 295 | /** 296 | * Register a binding as a value 297 | * 298 | * ```ts 299 | * container.bindValue(Route, new Route()) 300 | * ``` 301 | */ 302 | bindValue( 303 | /** 304 | * Need to narrow down the "Binding" for the case where "KnownBindings" are 305 | */ 306 | binding: Binding extends string | symbol ? Binding : never, 307 | value: KnownBindings[Binding] 308 | ): void 309 | bindValue>( 310 | binding: Binding, 311 | value: InstanceType 312 | ): void 313 | bindValue( 314 | binding: Binding, 315 | value: Binding extends AbstractConstructor 316 | ? A 317 | : Binding extends keyof KnownBindings 318 | ? KnownBindings[Binding] 319 | : never 320 | ): void { 321 | if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isClass(binding)) { 322 | throw new InvalidArgumentsException( 323 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 324 | ) 325 | } 326 | 327 | debug('adding value to container %O', binding) 328 | this.#bindingValues.set(binding, value) 329 | } 330 | 331 | /** 332 | * Register a binding as a single. The singleton method is same 333 | * as the bind method, but the factory function is invoked 334 | * only once. 335 | * 336 | * ```ts 337 | * container.singleton('route', () => new Route()) 338 | * await container.make('route') 339 | * 340 | * container.singleton(Route, () => new Route()) 341 | * await container.make(Route) 342 | * 343 | * const routeSymbol = Symbol('route') 344 | * container.singleton(routeSymbol, () => new Route()) 345 | * await container.make(routeSymbol) 346 | * ``` 347 | */ 348 | singleton( 349 | /** 350 | * Need to narrow down the "Binding" for the case where "KnownBindings" are 351 | */ 352 | binding: Binding extends string | symbol ? Binding : never, 353 | resolver: BindingResolver 354 | ): void 355 | singleton>( 356 | binding: Binding, 357 | resolver: BindingResolver> 358 | ): void 359 | singleton( 360 | binding: Binding, 361 | resolver: BindingResolver< 362 | KnownBindings, 363 | Binding extends AbstractConstructor 364 | ? A 365 | : Binding extends keyof KnownBindings 366 | ? KnownBindings[Binding] 367 | : never 368 | > 369 | ): void { 370 | if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isClass(binding)) { 371 | throw new InvalidArgumentsException( 372 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 373 | ) 374 | } 375 | 376 | debug('adding singleton to container %O', binding) 377 | this.#bindings.set(binding, { resolver: enqueue(resolver), isSingleton: true }) 378 | } 379 | 380 | /** 381 | * Define a fake implementation for a binding or a class constructor. 382 | * Fakes have the highest priority when resolving dependencies 383 | * from the container. 384 | */ 385 | swap>( 386 | binding: Binding, 387 | resolver: BindingResolver> 388 | ): void { 389 | if (!isClass(binding)) { 390 | throw new InvalidArgumentsException( 391 | `Cannot call swap on value "${inspect(binding)}". Only classes can be swapped` 392 | ) 393 | } 394 | 395 | debug('defining swap for %O', binding) 396 | this.#swaps.set(binding, resolver) 397 | } 398 | 399 | /** 400 | * Restore binding by removing its swap 401 | */ 402 | restore(binding: AbstractConstructor) { 403 | debug('removing swap for %s', binding) 404 | this.#swaps.delete(binding) 405 | } 406 | 407 | /** 408 | * Restore mentioned or all bindings by removing 409 | * their swaps 410 | */ 411 | restoreAll(bindings?: AbstractConstructor[]) { 412 | if (!bindings) { 413 | debug('removing all swaps') 414 | this.#swaps.clear() 415 | return 416 | } 417 | 418 | for (let binding of bindings) { 419 | this.restore(binding) 420 | } 421 | } 422 | 423 | /** 424 | * Define hooks to be executed after a binding has been resolved 425 | * from the container. 426 | * 427 | * The hooks are executed for 428 | * 429 | * - Bindings 430 | * - Only once for singletons 431 | * - And class constructor 432 | * 433 | * In other words, the hooks are not executed for direct values registered 434 | * with the container 435 | */ 436 | resolving( 437 | binding: Binding extends string | symbol ? Binding : never, 438 | callback: HookCallback 439 | ): void 440 | resolving>( 441 | binding: Binding, 442 | callback: HookCallback> 443 | ): void 444 | resolving( 445 | binding: Binding, 446 | callback: Binding extends AbstractConstructor 447 | ? HookCallback 448 | : Binding extends keyof KnownBindings 449 | ? HookCallback 450 | : never 451 | ): void { 452 | binding = (this.#aliases.get(binding) as Binding) || binding 453 | 454 | if (!this.#hooks.has(binding)) { 455 | this.#hooks.set(binding, new Set()) 456 | } 457 | 458 | const callbacks = this.#hooks.get(binding)! 459 | callbacks.add(callback) 460 | } 461 | 462 | /** 463 | * Create a contextual builder to define contextual bindings 464 | */ 465 | when(parent: Constructor): ContextBindingsBuilder> { 466 | return new ContextBindingsBuilder(parent, this) 467 | } 468 | 469 | /** 470 | * Add a contextual binding for a given class constructor. A 471 | * contextual takes a parent, parent's dependency and a callback 472 | * to self resolve the dependency. 473 | * 474 | * For example: 475 | * - When "UsersController" 476 | * - Asks for "Hash class" 477 | * - Provide "Argon2" implementation 478 | */ 479 | contextualBinding>( 480 | parent: Constructor, 481 | binding: Binding, 482 | resolver: BindingResolver> 483 | ): void { 484 | if (!isClass(binding)) { 485 | throw new InvalidArgumentsException( 486 | `The binding value for contextual binding should be class` 487 | ) 488 | } 489 | if (!isClass(parent)) { 490 | throw new InvalidArgumentsException(`The parent value for contextual binding should be class`) 491 | } 492 | 493 | debug('adding contextual binding %O to %O', binding, parent) 494 | 495 | /** 496 | * Create map for the parent if doesn't already exists 497 | */ 498 | if (!this.#contextualBindings.has(parent)) { 499 | this.#contextualBindings.set(parent, new Map()) 500 | } 501 | 502 | const parentBindings = this.#contextualBindings.get(parent)! 503 | parentBindings.set(binding, { resolver }) 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /src/contextual_bindings_builder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { RuntimeException } from '@poppinss/utils/exception' 11 | import type { AbstractConstructor, Constructor } from '@poppinss/utils/types' 12 | 13 | import type { Container } from './container.js' 14 | import type { BindingResolver, Make } from './types.js' 15 | 16 | /** 17 | * A fluent builder to register contextual bindings with the 18 | * container. 19 | */ 20 | export class ContextBindingsBuilder< 21 | KnownBindings extends Record, 22 | PinnedBinding extends AbstractConstructor, 23 | > { 24 | /** 25 | * The parent for whom to define the contextual 26 | * binding 27 | */ 28 | #parent: Constructor 29 | 30 | /** 31 | * The binding the parent asks for 32 | */ 33 | #binding?: PinnedBinding 34 | 35 | /** 36 | * Container instance for registering the contextual 37 | * bindings 38 | */ 39 | #container: Container 40 | 41 | constructor(parent: Constructor, container: Container) { 42 | this.#parent = parent 43 | this.#container = container 44 | } 45 | 46 | /** 47 | * Specify the binding for which to register a custom 48 | * resolver. 49 | */ 50 | asksFor( 51 | binding: Binding 52 | ): ContextBindingsBuilder { 53 | this.#binding = binding 54 | return this as unknown as ContextBindingsBuilder 55 | } 56 | 57 | /** 58 | * Provide a resolver to resolve the parent dependency 59 | */ 60 | provide(resolver: BindingResolver>): void { 61 | if (!this.#binding) { 62 | throw new RuntimeException( 63 | 'Missing value for contextual binding. Call "asksFor" method before calling the "provide" method' 64 | ) 65 | } 66 | 67 | this.#container.contextualBinding(this.#parent, this.#binding, resolver) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { debuglog } from 'node:util' 11 | export default debuglog('adonisjs:fold') 12 | -------------------------------------------------------------------------------- /src/decorators/inject.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { defineStaticProperty } from '@poppinss/utils' 11 | import { RuntimeException } from '@poppinss/utils/exception' 12 | 13 | import debug from '../debug.js' 14 | import type { ErrorCreator, InspectableConstructor } from '../types.js' 15 | 16 | /** 17 | * Creating a debugging error that points to the source 18 | * using the @inject decorator 19 | */ 20 | function createDebuggingError(original: Error) { 21 | return function createError(message: string) { 22 | const error = new RuntimeException(message) 23 | error.stack = original.stack 24 | return error 25 | } 26 | } 27 | 28 | /** 29 | * Initiating the "containerInjections" property on the target, which is assumed 30 | * to be the class constructor. 31 | */ 32 | function initiateContainerInjections( 33 | target: any, 34 | method: string | symbol, 35 | createError: ErrorCreator 36 | ) { 37 | defineStaticProperty(target, 'containerInjections', { initialValue: {}, strategy: 'inherit' }) 38 | target.containerInjections[method] = { 39 | createError, 40 | dependencies: [], 41 | } 42 | } 43 | 44 | /** 45 | * Defining the injections for the constructor of the class using 46 | * reflection 47 | */ 48 | function defineConstructorInjections(target: InspectableConstructor, createError: ErrorCreator) { 49 | const params = Reflect.getMetadata('design:paramtypes', target) 50 | /* c8 ignore next 3 */ 51 | if (!params) { 52 | return 53 | } 54 | 55 | initiateContainerInjections(target, '_constructor', createError) 56 | if (debug.enabled) { 57 | debug('defining constructor injections for %O, params %O', `[class: ${target.name}]`, params) 58 | } 59 | 60 | for (const param of params) { 61 | target.containerInjections!._constructor.dependencies.push(param) 62 | } 63 | } 64 | 65 | /** 66 | * Defining the injections for the class instance method 67 | */ 68 | function defineMethodInjections(target: any, method: string | symbol, createError: ErrorCreator) { 69 | const constructor = target.constructor as InspectableConstructor 70 | const params = Reflect.getMetadata('design:paramtypes', target, method) 71 | /* c8 ignore next 3 */ 72 | if (!params) { 73 | return 74 | } 75 | 76 | initiateContainerInjections(constructor, method, createError) 77 | if (debug.enabled) { 78 | debug( 79 | 'defining method injections for %O, method %O, params %O', 80 | `[class ${constructor.name}]`, 81 | method, 82 | params 83 | ) 84 | } 85 | 86 | for (const param of params) { 87 | constructor.containerInjections![method].dependencies.push(param) 88 | } 89 | } 90 | 91 | /** 92 | * The "@inject" decorator uses Reflection to inspect the dependencies of a class 93 | * or a method and defines them as metaData on the class for the container to 94 | * discover them. 95 | */ 96 | export function inject() { 97 | /** 98 | * Creating an error builder for the inject decorator, so that 99 | * the stack trace can point back to the code that used 100 | * the decorator 101 | */ 102 | const createError = createDebuggingError(new Error()) 103 | 104 | function injectDecorator(target: C): void 105 | function injectDecorator(target: any, propertyKey: string | symbol): void 106 | function injectDecorator(target: any, propertyKey?: string | symbol): void { 107 | if (!propertyKey) { 108 | defineConstructorInjections(target, createError) 109 | return 110 | } 111 | 112 | defineMethodInjections(target, propertyKey, createError) 113 | } 114 | 115 | return injectDecorator 116 | } 117 | -------------------------------------------------------------------------------- /src/deferred_promise.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * Exports the `resolve` and the reject methods as part of the 12 | * class public API. 13 | * 14 | * It allows resolving and rejecting promises outside of the 15 | * class constructor. 16 | */ 17 | export class Deferred { 18 | resolve!: (value: T | PromiseLike) => void 19 | reject!: (reason?: any) => void 20 | promise: Promise = new Promise((resolve, reject) => { 21 | this.reject = reject 22 | this.resolve = resolve 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Constructor } from '@poppinss/utils/types' 11 | import { RuntimeException } from '@poppinss/utils/exception' 12 | 13 | import { Deferred } from './deferred_promise.js' 14 | 15 | /** 16 | * Type guard and check if value is a class constructor. Plain old 17 | * functions are not considered as class constructor. 18 | */ 19 | export function isClass(value: unknown): value is Constructor { 20 | return typeof value === 'function' && value.toString().startsWith('class ') 21 | } 22 | 23 | /** 24 | * Runs a function inside an async function. This ensure that syncrohonous 25 | * errors are handled in the same way rejected promise is handled 26 | */ 27 | async function runAsAsync(callback: Function, args: any[]) { 28 | return callback(...args) 29 | } 30 | 31 | /** 32 | * Converts a function to a self contained queue, where each call to 33 | * the function is queued until the first call resolves or rejects. 34 | * 35 | * After the first call, the value is cached and used forever. 36 | */ 37 | export function enqueue(callback: Function) { 38 | /** 39 | * A flag to know if we are in the middleware of computing the 40 | * value. 41 | */ 42 | let isComputingValue = false 43 | 44 | /** 45 | * The computed after the callback resolves 46 | */ 47 | let computedValue: { value?: any; completed: boolean } = { completed: false } 48 | 49 | /** 50 | * The computed error the callback resolves 51 | */ 52 | let computedError: { error?: any; completed: boolean } = { completed: false } 53 | 54 | /** 55 | * The internal queue of deferred promises. 56 | */ 57 | let queue: Deferred[] = [] 58 | 59 | /** 60 | * Resolve pending queue promises 61 | */ 62 | function resolvePromises(value: any) { 63 | isComputingValue = false 64 | computedValue.completed = true 65 | computedValue.value = value 66 | queue.forEach((promise) => promise.resolve(value)) 67 | queue = [] 68 | } 69 | 70 | /** 71 | * Reject pending queue promises 72 | */ 73 | function rejectPromises(error: any) { 74 | isComputingValue = false 75 | computedError.completed = true 76 | computedError.error = error 77 | queue.forEach((promise) => promise.reject(error)) 78 | queue = [] 79 | } 80 | 81 | return function (...args: any): Promise<{ value: any; cached: boolean }> { 82 | /** 83 | * Already has value 84 | */ 85 | if (computedValue.completed) { 86 | return computedValue.value 87 | } 88 | 89 | /** 90 | * Already ended with error 91 | */ 92 | if (computedError.completed) { 93 | throw computedError.error 94 | } 95 | 96 | /** 97 | * In process, returning a deferred promise 98 | */ 99 | if (isComputingValue) { 100 | const promise = new Deferred<{ value: any; cached: true }>() 101 | queue.push(promise) 102 | return promise.promise 103 | } 104 | 105 | isComputingValue = true 106 | 107 | /** 108 | * We could have removed this promise in favor of async/await. But then 109 | * we will have to call "resolvePromises" before returning the value. 110 | * However, we want the following promise to resolve first and 111 | * then resolve all other deferred promises. 112 | */ 113 | return new Promise((resolve, reject) => { 114 | runAsAsync(callback, args) 115 | .then((value) => { 116 | resolve({ value, cached: false }) 117 | resolvePromises({ value, cached: true }) 118 | }) 119 | .catch((error) => { 120 | reject(error) 121 | rejectPromises(error) 122 | }) 123 | }) 124 | } 125 | } 126 | 127 | /** 128 | * Dynamically import a module and ensure it has a default export 129 | */ 130 | export async function resolveDefault(importPath: string, parentURL: URL | string) { 131 | const resolvedPath = await import.meta.resolve!(importPath, parentURL) 132 | const moduleExports = await import(resolvedPath) 133 | 134 | /** 135 | * Make sure a default export exists 136 | */ 137 | if (!moduleExports.default) { 138 | throw new RuntimeException(`Missing export default from "${importPath}" module`, { 139 | cause: { 140 | source: resolvedPath, 141 | }, 142 | }) 143 | } 144 | 145 | return moduleExports.default 146 | } 147 | -------------------------------------------------------------------------------- /src/module_caller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Constructor } from '@poppinss/utils/types' 11 | 12 | import { Container } from './container.js' 13 | import { ContainerResolver } from './resolver.js' 14 | import type { ModuleHandler, ModuleCallable } from './types.js' 15 | 16 | /** 17 | * The moduleCaller works around a very specific pattern we use with 18 | * AdonisJS, ie to construct classes and call methods using the 19 | * container. 20 | * 21 | * For example: Controllers of AdonisJS allows defining a controller 22 | * as follows 23 | * 24 | * ```ts 25 | * route.get('/', [HomeController, 'index']) 26 | * ``` 27 | * 28 | * Behind the scenes, we have to run following operations in order to call the 29 | * handle method on the defined middleware. 30 | * 31 | * - Create an instance of the controller class using the container. 32 | * - Call the method using the container. Hence having the ability to use 33 | * DI 34 | */ 35 | export function moduleCaller(target: Constructor, method: string) { 36 | return { 37 | /** 38 | * Converts the class reference to a callable function. Invoking this method 39 | * internally creates a new instance of the class using the container and 40 | * invokes the method using the container. 41 | * 42 | * You can create a callable function using the container instance as shown below 43 | * 44 | * ```ts 45 | * const fn = moduleCaller(HomeController, 'handle') 46 | * .toCallable(container) 47 | * 48 | * // Call the function and pass context to it 49 | * await fn(ctx) 50 | * ``` 51 | * 52 | * Another option is to not pass the container at the time of creating 53 | * the callable function, but instead pass a resolver instance at 54 | * the time of calling the function 55 | * 56 | * ```ts 57 | * const fn = moduleCaller(HomeController, 'handle') 58 | * .toCallable() 59 | * 60 | * // Call the function and pass context to it 61 | * const resolver = container.createResolver() 62 | * await fn(resolver, ctx) 63 | * ``` 64 | */ 65 | toCallable< 66 | T extends Container | ContainerResolver | undefined = undefined, 67 | Args extends any[] = any[], 68 | >(container?: T): ModuleCallable { 69 | /** 70 | * When container defined at the time of the calling this function, 71 | * we will use it to inside the return function 72 | */ 73 | if (container) { 74 | return async function (...args: Args) { 75 | return container.call(await container.make(target), method, args) 76 | } as ModuleCallable 77 | } 78 | 79 | /** 80 | * Otherwise the return function asks for the resolver or container 81 | */ 82 | return async function (resolver: ContainerResolver | Container, ...args: Args) { 83 | return resolver.call(await resolver.make(target), method, args) 84 | } as ModuleCallable 85 | }, 86 | 87 | /** 88 | * Converts the class reference to an object with handle method. Invoking this 89 | * method internally creates a new instance of the class using the container 90 | * and invokes the method using the container. 91 | * 92 | * You can create a handle method object using the container instance as shown below 93 | * 94 | * ```ts 95 | * const handler = moduleCaller(HomeController, 'handle') 96 | * .toHandleMethod(container) 97 | * 98 | * // Call the function and pass context to it 99 | * await handler.handle(ctx) 100 | * ``` 101 | * 102 | * Another option is to not pass the container at the time of creating 103 | * the handle method object, but instead pass a resolver instance at 104 | * the time of calling the function 105 | * 106 | * ```ts 107 | * const handler = moduleCaller(HomeController, 'handle') 108 | * .toHandleMethod() 109 | * 110 | * // Call the function and pass context to it 111 | * const resolver = container.createResolver() 112 | * await handler.handle(resolver, ctx) 113 | * ``` 114 | */ 115 | toHandleMethod< 116 | T extends Container | ContainerResolver | undefined = undefined, 117 | Args extends any[] = any[], 118 | >(container?: T): ModuleHandler { 119 | if (container) { 120 | return { 121 | name: `${target.name}.${method}`, 122 | async handle(...args: Args) { 123 | return container.call(await container.make(target), method, args) 124 | }, 125 | } as ModuleHandler 126 | } 127 | 128 | return { 129 | name: `${target.name}.${method}`, 130 | async handle(resolver: ContainerResolver | Container, ...args: Args) { 131 | return resolver.call(await resolver.make(target), method, args) 132 | }, 133 | } as ModuleHandler 134 | }, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/module_expression.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { Container } from './container.js' 11 | import { resolveDefault } from './helpers.js' 12 | import { ContainerResolver } from './resolver.js' 13 | import type { ModuleHandler, ModuleCallable } from './types.js' 14 | 15 | /** 16 | * The moduleExpression module works around a very specific pattern we use 17 | * with AdonisJS, ie to bind modules as string. 18 | * 19 | * For example: With the router of AdonisJS, we can bind a controller to a route 20 | * as follows. 21 | * 22 | * ```ts 23 | * Route.get('users', '#controllers/users_controller.index') 24 | * ``` 25 | * 26 | * Behind the scenes, we have to run following operations in order to call a 27 | * method on the users_controller class. 28 | * 29 | * - Dynamic import `#controllers/users_controller` module 30 | * - Check if the module has a default export. 31 | * - Create an instance of the default export class using the container. 32 | * - Call the `index` method on the controller class using the container. 33 | * 34 | * Router is just one example, we do this with event listeners, redis pub/sub 35 | * and so on. 36 | * 37 | * So, instead of writing all this parsing logic, we encapsulate it inside the 38 | * "moduleExpression" module. 39 | */ 40 | export function moduleExpression(expression: string, parentURL: URL | string) { 41 | return { 42 | /** 43 | * Parses a module expression to extract the module import path 44 | * and the method to call on the default exported class. 45 | * 46 | * ```ts 47 | * moduleExpression('#controllers/users_controller').parse() 48 | * // ['#controllers/users_controller', 'handle'] 49 | * ``` 50 | * 51 | * With method 52 | * ```ts 53 | * moduleExpression('#controllers/users_controller.index').parse() 54 | * // ['#controllers/users_controller', 'index'] 55 | * ``` 56 | */ 57 | parse(): [string, string] { 58 | const parts = expression.split('.') 59 | if (parts.length === 1) { 60 | return [expression, 'handle'] 61 | } 62 | 63 | const method = parts.pop()! 64 | return [parts.join('.'), method] 65 | }, 66 | 67 | /** 68 | * Converts the module expression to a callable function. Invoking this 69 | * method run internally import the module, create a new instance of the 70 | * default export class using the container and invokes the method using 71 | * the container. 72 | * 73 | * You can create a callable function using the container instance as shown below 74 | * 75 | * ```ts 76 | * const fn = moduleExpression('#controllers/users_controller.index') 77 | * .toCallable(container) 78 | * 79 | * // Call the function and pass context to it 80 | * await fn(ctx) 81 | * ``` 82 | * 83 | * Another option is to not pass the container at the time of creating 84 | * the callable function, but instead pass a resolver instance at 85 | * the time of calling the function 86 | * 87 | * ```ts 88 | * const fn = moduleExpression('#controllers/users_controller.index') 89 | * .toCallable() 90 | * 91 | * // Call the function and pass context to it 92 | * const resolver = container.createResolver() 93 | * await fn(resolver, ctx) 94 | * ``` 95 | */ 96 | toCallable< 97 | T extends Container | ContainerResolver | undefined = undefined, 98 | Args extends any[] = any[], 99 | >(container?: T): ModuleCallable { 100 | let defaultExport: any = null 101 | const [importPath, method] = this.parse() 102 | 103 | /** 104 | * When container defined at the time of the calling this function, 105 | * we will use it to inside the return function 106 | */ 107 | if (container) { 108 | return async function (...args: Args) { 109 | if (!defaultExport || 'hot' in import.meta) { 110 | defaultExport = await resolveDefault(importPath, parentURL) 111 | } 112 | return container.call(await container.make(defaultExport), method, args) 113 | } as ModuleCallable 114 | } 115 | 116 | /** 117 | * Otherwise the return function asks for the resolver or container 118 | */ 119 | return async function (resolver: ContainerResolver | Container, ...args: Args) { 120 | if (!defaultExport || 'hot' in import.meta) { 121 | defaultExport = await resolveDefault(importPath, parentURL) 122 | } 123 | return resolver.call(await resolver.make(defaultExport), method, args) 124 | } as ModuleCallable 125 | }, 126 | 127 | /** 128 | * Converts the module expression to an object with handle method. Invoking the 129 | * handle method run internally imports the module, create a new instance of 130 | * the default export class using the container and invokes the method using 131 | * the container. 132 | * 133 | * You can create a handle method object using the container instance as shown below 134 | * 135 | * ```ts 136 | * const handler = moduleExpression('#controllers/users_controller.index') 137 | * .toHandleMethod(container) 138 | * 139 | * // Call the function and pass context to it 140 | * await handler.handle(ctx) 141 | * ``` 142 | * 143 | * Another option is to not pass the container at the time of creating 144 | * the handle method object, but instead pass a resolver instance at 145 | * the time of calling the function 146 | * 147 | * ```ts 148 | * const handler = moduleExpression('#controllers/users_controller.index') 149 | * .toHandleMethod() 150 | * 151 | * // Call the function and pass context to it 152 | * const resolver = container.createResolver() 153 | * await handler.handle(resolver, ctx) 154 | * ``` 155 | */ 156 | toHandleMethod< 157 | T extends Container | ContainerResolver | undefined = undefined, 158 | Args extends any[] = any[], 159 | >(container?: T): ModuleHandler { 160 | let defaultExport: any = null 161 | const [importPath, method] = this.parse() 162 | 163 | if (container) { 164 | return { 165 | async handle(...args: Args) { 166 | if (!defaultExport || 'hot' in import.meta) { 167 | defaultExport = await resolveDefault(importPath, parentURL) 168 | } 169 | return container.call(await container.make(defaultExport), method, args) 170 | }, 171 | } as ModuleHandler 172 | } 173 | 174 | return { 175 | async handle(resolver: ContainerResolver | Container, ...args: Args) { 176 | if (!defaultExport || 'hot' in import.meta) { 177 | defaultExport = await resolveDefault(importPath, parentURL) 178 | } 179 | return resolver.call(await resolver.make(defaultExport), method, args) 180 | }, 181 | } as ModuleHandler 182 | }, 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/module_importer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { importDefault } from '@poppinss/utils' 11 | import type { Constructor } from '@poppinss/utils/types' 12 | 13 | import { Container } from './container.js' 14 | import { ContainerResolver } from './resolver.js' 15 | import type { ModuleHandler, ModuleCallable } from './types.js' 16 | 17 | /** 18 | * The moduleImporter module works around a very specific pattern we use 19 | * with AdonisJS, ie to lazy load modules by wrapping import calls inside 20 | * a callback. 21 | * 22 | * For example: Middleware of AdonisJS allows registering middleware as an 23 | * array of import calls. 24 | * 25 | * ```ts 26 | * defineMiddleware([ 27 | * () => import('#middleware/silent_auth') 28 | * ]) 29 | * 30 | * defineMiddleware({ 31 | * auth: () => import('#middleware/auth') 32 | * }) 33 | * ``` 34 | * 35 | * Behind the scenes, we have to run following operations in order to call the 36 | * handle method on the defined middleware. 37 | * 38 | * - Lazily call the registered callbacks to import the middleware. 39 | * - Check if the module has a default export. 40 | * - Create an instance of the default export class using the container. 41 | * - Call the `handle` method on the middleware class using the container. 42 | */ 43 | export function moduleImporter( 44 | importFn: () => Promise<{ default: Constructor }>, 45 | method: string 46 | ) { 47 | return { 48 | /** 49 | * Converts the module import function to a callable function. Invoking this 50 | * method run internally import the module, create a new instance of the 51 | * default export class using the container and invokes the method using 52 | * the container. 53 | * 54 | * You can create a callable function using the container instance as shown below 55 | * 56 | * ```ts 57 | * const fn = moduleImporter(() => import('#middleware/auth_middleware'), 'handle') 58 | * .toCallable(container) 59 | * 60 | * // Call the function and pass context to it 61 | * await fn(ctx) 62 | * ``` 63 | * 64 | * Another option is to not pass the container at the time of creating 65 | * the callable function, but instead pass a resolver instance at 66 | * the time of calling the function 67 | * 68 | * ```ts 69 | * const fn = moduleImporter(() => import('#middleware/auth_middleware'), 'handle') 70 | * .toCallable() 71 | * 72 | * // Call the function and pass context to it 73 | * const resolver = container.createResolver() 74 | * await fn(resolver, ctx) 75 | * ``` 76 | */ 77 | toCallable< 78 | T extends Container | ContainerResolver | undefined = undefined, 79 | Args extends any[] = any[], 80 | >(container?: T): ModuleCallable { 81 | let defaultExport: any = null 82 | 83 | /** 84 | * When container defined at the time of the calling this function, 85 | * we will use it to inside the return function 86 | */ 87 | if (container) { 88 | return async function (...args: Args) { 89 | if (!defaultExport || 'hot' in import.meta) { 90 | defaultExport = await importDefault(importFn) 91 | } 92 | return container.call(await container.make(defaultExport), method, args) 93 | } as ModuleCallable 94 | } 95 | 96 | /** 97 | * Otherwise the return function asks for the resolver or container 98 | */ 99 | return async function (resolver: ContainerResolver | Container, ...args: Args) { 100 | if (!defaultExport || 'hot' in import.meta) { 101 | defaultExport = await importDefault(importFn) 102 | } 103 | return resolver.call(await resolver.make(defaultExport), method, args) 104 | } as ModuleCallable 105 | }, 106 | 107 | /** 108 | * Converts the module import function to an object with handle method. Invoking the 109 | * handle method run internally imports the module, create a new instance of 110 | * the default export class using the container and invokes the method using 111 | * the container. 112 | * 113 | * You can create a handle method object using the container instance as shown below 114 | * 115 | * ```ts 116 | * const handler = moduleImporter(() => import('#middleware/auth_middleware'), 'handle') 117 | * .toHandleMethod(container) 118 | * 119 | * // Call the function and pass context to it 120 | * await handler.handle(ctx) 121 | * ``` 122 | * 123 | * Another option is to not pass the container at the time of creating 124 | * the handle method object, but instead pass a resolver instance at 125 | * the time of calling the function 126 | * 127 | * ```ts 128 | * const handler = moduleImporter(() => import('#middleware/auth_middleware'), 'handle') 129 | * .toHandleMethod() 130 | * 131 | * // Call the function and pass context to it 132 | * const resolver = container.createResolver() 133 | * await handler.handle(resolver, ctx) 134 | * ``` 135 | */ 136 | toHandleMethod< 137 | T extends Container | ContainerResolver | undefined = undefined, 138 | Args extends any[] = any[], 139 | >(container?: T): ModuleHandler { 140 | let defaultExport: any = null 141 | 142 | if (container) { 143 | return { 144 | name: importFn.name, 145 | async handle(...args: Args) { 146 | if (!defaultExport || 'hot' in import.meta) { 147 | defaultExport = await importDefault(importFn) 148 | } 149 | return container.call(await container.make(defaultExport), method, args) 150 | }, 151 | } as ModuleHandler 152 | } 153 | 154 | return { 155 | name: importFn.name, 156 | async handle(resolver: ContainerResolver | Container, ...args: Args) { 157 | if (!defaultExport || 'hot' in import.meta) { 158 | defaultExport = await importDefault(importFn) 159 | } 160 | return resolver.call(await resolver.make(defaultExport), method, args) 161 | }, 162 | } as ModuleHandler 163 | }, 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import debug from './debug.js' 11 | import type { ContainerResolver } from './resolver.js' 12 | import type { InspectableConstructor } from './types.js' 13 | 14 | /** 15 | * The default provider for resolving dependencies. It uses the resolver 16 | * to resolve all the values. 17 | */ 18 | export async function containerProvider( 19 | binding: InspectableConstructor, 20 | property: string | symbol | number, 21 | resolver: ContainerResolver, 22 | runtimeValues?: any[] 23 | ) { 24 | const values = runtimeValues || [] 25 | 26 | /** 27 | * Return early when the class does not have static "containerInjections" 28 | * property or if there are no injections for the given property 29 | */ 30 | if (!binding.containerInjections || !binding.containerInjections[property]) { 31 | return values 32 | } 33 | 34 | const injections = binding.containerInjections[property].dependencies 35 | const createError = binding.containerInjections[property].createError 36 | 37 | /** 38 | * If the length of runtime values is more than the injections 39 | * length, then we make sure to return all the runtime 40 | * values and fill undefined slots with container lookup 41 | */ 42 | if (values.length > injections.length) { 43 | if (debug.enabled) { 44 | debug( 45 | 'created resolver plan. target: "[class %s]", property: "%s", injections: %O', 46 | binding.name, 47 | property, 48 | values.map((value, index) => { 49 | if (value !== undefined) { 50 | return value 51 | } 52 | 53 | return injections[index] 54 | }) 55 | ) 56 | } 57 | 58 | return Promise.all( 59 | values.map((value, index) => { 60 | if (value !== undefined) { 61 | return value 62 | } 63 | 64 | const injection = injections[index] 65 | return resolver.resolveFor(binding, injection, undefined, createError) 66 | }) 67 | ) 68 | } 69 | 70 | /** 71 | * Otherwise, we go through the injections, giving 72 | * priority to the runtime values for a given index. 73 | */ 74 | if (debug.enabled) { 75 | debug( 76 | 'created resolver plan. target: "[class %s]", property: "%s", injections: %O', 77 | binding.name, 78 | property, 79 | injections.map((injection, index) => { 80 | if (values[index] !== undefined) { 81 | return values[index] 82 | } 83 | 84 | return injection 85 | }) 86 | ) 87 | } 88 | 89 | return Promise.all( 90 | injections.map((injection, index) => { 91 | if (values[index] !== undefined) { 92 | return values[index] 93 | } 94 | 95 | return resolver.resolveFor(binding, injection, undefined, createError) 96 | }) 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { inspect } from 'node:util' 11 | import { InvalidArgumentsException, RuntimeException } from '@poppinss/utils/exception' 12 | import type { AbstractConstructor, Constructor, ExtractFunctions } from '@poppinss/utils/types' 13 | 14 | import type { 15 | Make, 16 | Hooks, 17 | Swaps, 18 | Bindings, 19 | BindingKey, 20 | ErrorCreator, 21 | BindingValues, 22 | BindingResolver, 23 | ContainerOptions, 24 | ContextualBindings, 25 | InspectableConstructor, 26 | } from './types.js' 27 | import debug from './debug.js' 28 | import { isClass } from './helpers.js' 29 | import { containerProvider } from './provider.js' 30 | 31 | /** 32 | * Container resolver exposes the APIs to resolve bindings. You can think 33 | * of resolver as an isolated container instance, with only the APIs 34 | * to resolve bindings. 35 | * 36 | * ```ts 37 | * const container = new Container() 38 | * const resolver = container.createResolver() 39 | * 40 | * await resolver.make(BINDING_NAME) 41 | * await resolver.make(CLASS_CONSTRUCTOR) 42 | * ``` 43 | */ 44 | export class ContainerResolver> { 45 | /** 46 | * Reference to the container aliases. They are shared between the container 47 | * and resolver. 48 | * 49 | * We do not mutate this property within the resolver 50 | */ 51 | #containerAliases: Map< 52 | Partial, 53 | keyof KnownBindings | AbstractConstructor 54 | > 55 | 56 | /** 57 | * Pre-registered contextual bindings. They are shared between the container 58 | * and resolver. 59 | * 60 | * We do not mutate this property within the resolver 61 | */ 62 | #containerContextualBindings: Map, ContextualBindings> 63 | 64 | /** 65 | * Pre-registered bindings. They are shared between the container 66 | * and resolver. 67 | * 68 | * We do not mutate this property within the resolver 69 | */ 70 | #containerBindings: Bindings 71 | 72 | /** 73 | * Pre-registered bindings. They are shared between the container 74 | * and resolver. 75 | * 76 | * We mutate this property within the resolver to set singleton 77 | * cached values 78 | */ 79 | #containerBindingValues: BindingValues 80 | 81 | /** 82 | * Pre-registered swaps for bindings. They are shared between 83 | * the container and resolver. 84 | * 85 | * We do not mutate this property within the resolver 86 | */ 87 | #containerSwaps: Swaps 88 | 89 | /** 90 | * Reference to the container hooks 91 | */ 92 | #containerHooks: Hooks 93 | 94 | /** 95 | * Binding values local to the resolver 96 | */ 97 | #bindingValues: BindingValues = new Map() 98 | 99 | /** 100 | * Container options 101 | */ 102 | #options: ContainerOptions 103 | 104 | constructor( 105 | container: { 106 | bindings: Bindings 107 | bindingValues: BindingValues 108 | swaps: Swaps 109 | hooks: Hooks 110 | aliases: Map, keyof KnownBindings | AbstractConstructor> 111 | contextualBindings: Map, ContextualBindings> 112 | }, 113 | options: ContainerOptions 114 | ) { 115 | this.#containerBindings = container.bindings 116 | this.#containerBindingValues = container.bindingValues 117 | this.#containerSwaps = container.swaps 118 | this.#containerHooks = container.hooks 119 | this.#containerAliases = container.aliases 120 | this.#containerContextualBindings = container.contextualBindings 121 | this.#options = options 122 | } 123 | 124 | /** 125 | * Constructs exception for invalid binding value 126 | */ 127 | #invalidBindingException( 128 | parent: any, 129 | binding: any, 130 | createError: ErrorCreator 131 | ): InvalidArgumentsException { 132 | if (parent) { 133 | const error = createError(`Cannot inject "${inspect(binding)}" in "[class ${parent.name}]"`) 134 | error.help = 'The value is not a valid class' 135 | return error 136 | } 137 | 138 | return createError(`Cannot construct value "${inspect(binding)}" using container`) 139 | } 140 | 141 | /** 142 | * Constructs exception for binding with missing dependencies 143 | */ 144 | #missingDependenciesException(parent: any, binding: any, createError: ErrorCreator) { 145 | if (parent) { 146 | const error = createError( 147 | `Cannot inject "[class ${binding.name}]" in "[class ${parent.name}]"` 148 | ) 149 | error.help = `Container is not able to resolve "${parent.name}" class dependencies. Did you forget to use @inject() decorator?` 150 | return error 151 | } 152 | 153 | return createError( 154 | `Cannot construct "[class ${binding.name}]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?` 155 | ) 156 | } 157 | 158 | /** 159 | * Returns the provider for the class constructor 160 | */ 161 | #getBindingProvider(binding: InspectableConstructor) { 162 | return binding.containerProvider 163 | } 164 | 165 | /** 166 | * Returns the binding resolver for a parent and a binding. Returns 167 | * undefined when no contextual binding exists 168 | */ 169 | #getBindingResolver( 170 | parent: any, 171 | binding: AbstractConstructor 172 | ): BindingResolver | undefined { 173 | const parentBindings = this.#containerContextualBindings.get(parent) 174 | if (!parentBindings) { 175 | return 176 | } 177 | 178 | const bindingResolver = parentBindings.get(binding) 179 | if (!bindingResolver) { 180 | return 181 | } 182 | 183 | return bindingResolver.resolver 184 | } 185 | 186 | /** 187 | * Notify emitter 188 | */ 189 | #emit(binding: BindingKey, value: any) { 190 | if (!this.#options.emitter) { 191 | return 192 | } 193 | this.#options.emitter.emit('container_binding:resolved', { binding, value }) 194 | } 195 | 196 | /** 197 | * Execute hooks for a given binding 198 | */ 199 | async #execHooks(binding: BindingKey, value: any) { 200 | const callbacks = this.#containerHooks.get(binding) 201 | if (!callbacks || callbacks.size === 0) { 202 | return 203 | } 204 | 205 | for (let callback of callbacks) { 206 | await callback(value, this) 207 | } 208 | } 209 | 210 | /** 211 | * Find if the resolver has a binding registered using the 212 | * "bind", the "singleton", or the "bindValue" methods. 213 | */ 214 | hasBinding(binding: Binding): boolean 215 | hasBinding(binding: BindingKey): boolean 216 | hasBinding(binding: BindingKey): boolean { 217 | return ( 218 | this.#containerAliases.has(binding) || 219 | this.#bindingValues.has(binding) || 220 | this.#containerBindingValues.has(binding) || 221 | this.#containerBindings.has(binding) 222 | ) 223 | } 224 | 225 | /** 226 | * Find if the resolver has all the bindings registered using the 227 | * "bind", the "singleton", or the "bindValue" methods. 228 | */ 229 | hasAllBindings(bindings: Binding[]): boolean 230 | hasAllBindings(bindings: BindingKey[]): boolean 231 | hasAllBindings(bindings: BindingKey[]): boolean { 232 | return bindings.every((binding) => this.hasBinding(binding)) 233 | } 234 | 235 | /** 236 | * Resolves binding in context of a parent. The method is same as 237 | * the "make" method, but instead takes a parent class 238 | * constructor. 239 | */ 240 | async resolveFor( 241 | parent: unknown, 242 | binding: Binding, 243 | runtimeValues?: any[], 244 | createError: ErrorCreator = (message) => new RuntimeException(message) 245 | ): Promise> { 246 | const isAClass = isClass(binding) 247 | 248 | /** 249 | * Raise exception when the binding is not a string, a class constructor 250 | * or a symbol. 251 | */ 252 | if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isAClass) { 253 | throw this.#invalidBindingException(parent, binding, createError) 254 | } 255 | 256 | /** 257 | * Entertain swaps with highest priority. The swaps can only exists for 258 | * class constructors. 259 | */ 260 | if (isAClass && this.#containerSwaps.has(binding)) { 261 | const resolver = this.#containerSwaps.get(binding)! 262 | const value = await resolver(this, runtimeValues) 263 | 264 | if (debug.enabled) { 265 | debug('resolved swap for binding %O, resolved value :%O', binding, value) 266 | } 267 | 268 | /** 269 | * Executing hooks and emitting events for the swaps is 270 | * debatable for now 271 | */ 272 | await this.#execHooks(binding, value) 273 | this.#emit(binding, value) 274 | return value 275 | } 276 | 277 | /** 278 | * Resolving contextual binding. Contextual bindings can only exists for 279 | * class constructors 280 | */ 281 | const contextualResolver = isAClass && this.#getBindingResolver(parent, binding) 282 | if (contextualResolver) { 283 | const value = await contextualResolver(this, runtimeValues) 284 | 285 | if (debug.enabled) { 286 | debug('resolved using contextual resolver binding %O, resolved value :%O', binding, value) 287 | } 288 | 289 | await this.#execHooks(binding, value) 290 | this.#emit(binding, value) 291 | 292 | return value 293 | } 294 | 295 | /** 296 | * First priority is given to the RESOLVER binding values 297 | */ 298 | if (this.#bindingValues.has(binding)) { 299 | const value = this.#bindingValues.get(binding) 300 | 301 | if (debug.enabled) { 302 | debug('resolved from resolver values %O, resolved value :%O', binding, value) 303 | } 304 | 305 | this.#emit(binding, value) 306 | return value 307 | } 308 | 309 | /** 310 | * Next priority is given to the CONTAINER binding values 311 | */ 312 | if (this.#containerBindingValues.has(binding)) { 313 | const value = this.#containerBindingValues.get(binding) 314 | 315 | if (debug.enabled) { 316 | debug('resolved from container values %O, resolved value :%O', binding, value) 317 | } 318 | 319 | this.#emit(binding, value) 320 | return value 321 | } 322 | 323 | /** 324 | * Followed by the CONTAINER bindings 325 | */ 326 | if (this.#containerBindings.has(binding)) { 327 | const { resolver, isSingleton } = this.#containerBindings.get(binding)! 328 | let value 329 | let executeHooks = isSingleton ? false : true 330 | 331 | /** 332 | * Invoke binding resolver to get the value. In case of singleton, 333 | * the "enqueue" method returns an object with the value and a 334 | * boolean telling if a cached value is resolved. 335 | */ 336 | if (isSingleton) { 337 | const result = await resolver(this, runtimeValues) 338 | value = result.value 339 | executeHooks = !result.cached 340 | } else { 341 | value = await resolver(this, runtimeValues) 342 | } 343 | 344 | if (debug.enabled) { 345 | debug('resolved binding %O, resolved value :%O', binding, value) 346 | } 347 | 348 | if (executeHooks) { 349 | await this.#execHooks(binding, value) 350 | } 351 | this.#emit(binding, value) 352 | 353 | return value 354 | } 355 | 356 | /** 357 | * Create an instance of the class with its constructor 358 | * dependencies. 359 | */ 360 | if (isAClass) { 361 | let dependencies: any[] = [] 362 | const classConstructor: InspectableConstructor = binding 363 | 364 | const bindingProvider = this.#getBindingProvider(classConstructor) 365 | if (bindingProvider) { 366 | dependencies = await bindingProvider( 367 | classConstructor, 368 | '_constructor', 369 | this, 370 | containerProvider, 371 | runtimeValues 372 | ) 373 | } else { 374 | dependencies = await containerProvider( 375 | classConstructor, 376 | '_constructor', 377 | this, 378 | runtimeValues 379 | ) 380 | } 381 | 382 | /** 383 | * Class has dependencies for which we do not have runtime values and neither 384 | * we have typehints. Therefore we throw an exception 385 | */ 386 | if (dependencies.length < classConstructor.length) { 387 | throw this.#missingDependenciesException(parent, binding, createError) 388 | } 389 | 390 | const value = new binding(...dependencies) as Promise> 391 | 392 | if (debug.enabled) { 393 | debug('constructed class %O, resolved value :%O', binding, value) 394 | } 395 | 396 | await this.#execHooks(binding, value) 397 | this.#emit(binding, value) 398 | 399 | return value 400 | } 401 | 402 | throw createError(`Cannot resolve binding "${String(binding)}" from the container`) 403 | } 404 | 405 | /** 406 | * Resolves the binding or constructor a class instance as follows. 407 | * 408 | * - Resolve the binding from the values (if registered) 409 | * - Resolve the binding from the bindings (if registered) 410 | * - If binding is a class, then create a instance of it. The constructor 411 | * dependencies are further resolved as well. 412 | * - All other values are returned as it is. 413 | * 414 | * ```ts 415 | * await resolver.make('route') 416 | * await resolver.make(Database) 417 | * ``` 418 | */ 419 | make( 420 | binding: Binding, 421 | runtimeValues?: any[], 422 | createError?: ErrorCreator 423 | ): Promise> 424 | make( 425 | binding: Binding, 426 | runtimeValues?: any[], 427 | createError?: ErrorCreator 428 | ): Promise> 429 | async make( 430 | binding: Binding, 431 | runtimeValues?: any[], 432 | createError?: ErrorCreator 433 | ): Promise> { 434 | /** 435 | * Make alias 436 | */ 437 | if (this.#containerAliases.has(binding)) { 438 | return this.resolveFor(null, this.#containerAliases.get(binding), runtimeValues, createError) 439 | } 440 | 441 | return this.resolveFor(null, binding, runtimeValues, createError) 442 | } 443 | 444 | /** 445 | * Call a method on an object by injecting its dependencies. The method 446 | * dependencies are resolved in the same manner as a class constructor 447 | * dependencies. 448 | * 449 | * ```ts 450 | * await resolver.call(await resolver.make(UsersController), 'index') 451 | * ``` 452 | */ 453 | async call, Method extends ExtractFunctions>( 454 | value: Value, 455 | method: Method, 456 | runtimeValues?: any[], 457 | createError: ErrorCreator = (message) => new RuntimeException(message) 458 | ): Promise> { 459 | if (typeof value[method] !== 'function') { 460 | throw createError(`Missing method "${String(method)}" on "${inspect(value)}"`) 461 | } 462 | 463 | if (debug.enabled) { 464 | debug('calling method %s, on value :%O', method, value) 465 | } 466 | 467 | let dependencies: any[] = [] 468 | const binding = value.constructor 469 | 470 | const bindingProvider = this.#getBindingProvider(binding) 471 | if (bindingProvider) { 472 | dependencies = await bindingProvider(binding, method, this, containerProvider, runtimeValues) 473 | } else { 474 | dependencies = await containerProvider(binding, method, this, runtimeValues) 475 | } 476 | 477 | /** 478 | * Method has dependencies for which we do not have runtime values and neither 479 | * we have typehints. Therefore we throw an exception 480 | */ 481 | if (dependencies.length < value[method].length) { 482 | throw createError( 483 | `Cannot call "${binding.name}.${String( 484 | method 485 | )}" method. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?` 486 | ) 487 | } 488 | 489 | return value[method](...dependencies) 490 | } 491 | 492 | /** 493 | * Register a binding as a value 494 | * 495 | * ```ts 496 | * container.bindValue(Route, new Route()) 497 | * ``` 498 | */ 499 | bindValue( 500 | /** 501 | * Need to narrow down the "Binding" for the case where "KnownBindings" are 502 | */ 503 | binding: Binding extends string | symbol ? Binding : never, 504 | value: KnownBindings[Binding] 505 | ): void 506 | bindValue>( 507 | binding: Binding, 508 | value: InstanceType 509 | ): void 510 | bindValue( 511 | binding: Binding, 512 | value: Binding extends AbstractConstructor 513 | ? A 514 | : Binding extends keyof KnownBindings 515 | ? KnownBindings[Binding] 516 | : never 517 | ): void { 518 | if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isClass(binding)) { 519 | throw new InvalidArgumentsException( 520 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 521 | ) 522 | } 523 | 524 | debug('adding value to resolver "%O"', binding) 525 | this.#bindingValues.set(binding, value) 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { Exception } from '@poppinss/utils/exception' 11 | import type { AbstractConstructor } from '@poppinss/utils/types' 12 | 13 | import type { Container } from './container.js' 14 | import type { ContainerResolver } from './resolver.js' 15 | 16 | /** 17 | * A function to create custom errors when container fails. It can be 18 | * used to point errors to the original source 19 | */ 20 | export type ErrorCreator = (message: string) => Exception 21 | 22 | /** 23 | * Shape of a class constructor with injections 24 | */ 25 | export type InspectableConstructor = Function & { 26 | containerInjections?: Record< 27 | string | number | symbol, 28 | { 29 | dependencies: any[] 30 | createError?: ErrorCreator 31 | } 32 | > 33 | containerProvider?: ContainerProvider 34 | } 35 | 36 | /** 37 | * Returns the inferred value for the make method 38 | */ 39 | export type Make = T extends AbstractConstructor ? A : never 40 | 41 | /** 42 | * Accepted values for the binding key 43 | */ 44 | export type BindingKey = string | symbol | AbstractConstructor 45 | 46 | /** 47 | * Shape of the binding resolver 48 | */ 49 | export type BindingResolver, Value> = ( 50 | resolver: ContainerResolver, 51 | runtimeValues?: any[] 52 | ) => Value | Promise 53 | 54 | /** 55 | * Shape of the registered bindings 56 | */ 57 | export type Bindings = Map< 58 | BindingKey, 59 | | { resolver: BindingResolver, any>; isSingleton: false } 60 | | { 61 | resolver: ( 62 | containerResolver: ContainerResolver>, 63 | runtimeValues?: any[] 64 | ) => Promise<{ value: any; cached: boolean }> 65 | isSingleton: true 66 | } 67 | > 68 | 69 | /** 70 | * Shape of the registered contextual bindings 71 | */ 72 | export type ContextualBindings = Map< 73 | AbstractConstructor, 74 | { resolver: BindingResolver, any> } 75 | > 76 | 77 | /** 78 | * Shape of the registered swaps 79 | */ 80 | export type Swaps = Map, BindingResolver, any>> 81 | 82 | /** 83 | * Shape of the registered binding values 84 | */ 85 | export type BindingValues = Map 86 | 87 | /** 88 | * The data emitted by the `container_binding:resolved` event. If known bindings 89 | * are defined, then the bindings and values will be correctly 90 | * inferred. 91 | */ 92 | export type ContainerResolveEventData = 93 | | { 94 | binding: AbstractConstructor 95 | value: unknown 96 | } 97 | | { 98 | [K in keyof KnownBindings]: { 99 | binding: K 100 | value: KnownBindings[K] 101 | } 102 | }[keyof KnownBindings] 103 | 104 | /** 105 | * Shape of the hooks callback 106 | */ 107 | export type HookCallback, Value> = ( 108 | value: Value, 109 | resolver: ContainerResolver 110 | ) => void | Promise 111 | 112 | /** 113 | * Hooks can be registered for all the supported binding datatypes. 114 | */ 115 | export type Hooks = Map>> 116 | 117 | /** 118 | * The default implementation of the container 119 | * provider. 120 | */ 121 | export type DefaultContainerProvider = ( 122 | binding: InspectableConstructor, 123 | property: string | symbol | number, 124 | resolver: ContainerResolver, 125 | runtimeValues?: any[] 126 | ) => Promise 127 | 128 | /** 129 | * The container provider to discover and build dependencies 130 | * for the constructor or the class method. 131 | */ 132 | export type ContainerProvider = ( 133 | binding: InspectableConstructor, 134 | property: string | symbol | number, 135 | resolver: ContainerResolver, 136 | defaultProvider: DefaultContainerProvider, 137 | runtimeValues?: any[] 138 | ) => Promise 139 | 140 | /** 141 | * Options accepted by the container class 142 | */ 143 | export type ContainerOptions = { 144 | emitter?: { 145 | emit(event: string | symbol, ...values: any[]): any 146 | } 147 | } 148 | 149 | /** 150 | * The shape of the function that imports a module expression and runs 151 | * it using the container 152 | */ 153 | export type ModuleCallable = T extends undefined 154 | ? (resolver: ContainerResolver | Container, ...args: Args) => Promise 155 | : (...args: Args) => Promise 156 | 157 | /** 158 | * The shape of the handle method object that imports a module expression 159 | * and runs it using the container 160 | */ 161 | export type ModuleHandler = T extends undefined 162 | ? { 163 | name?: string 164 | handle(resolver: ContainerResolver | Container, ...args: Args): Promise 165 | } 166 | : { 167 | name?: string 168 | handle(...args: Args): Promise 169 | } 170 | -------------------------------------------------------------------------------- /tests/container/bindings.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { Container } from '../../src/container.js' 13 | 14 | test.group('Container | Bindings', () => { 15 | test('register a binding to the container', async ({ assert }) => { 16 | const container = new Container() 17 | class Route {} 18 | 19 | container.bind('route', () => { 20 | return new Route() 21 | }) 22 | 23 | const route = await container.make('route') 24 | expectTypeOf(route).toBeAny() 25 | assert.instanceOf(route, Route) 26 | }) 27 | 28 | test('use symbol for the binding name', async ({ assert }) => { 29 | const container = new Container() 30 | class Route {} 31 | 32 | const routeSymbol = Symbol('route') 33 | container.bind(routeSymbol, () => { 34 | return new Route() 35 | }) 36 | 37 | const route = await container.make(routeSymbol) 38 | expectTypeOf(route).toBeAny() 39 | assert.instanceOf(route, Route) 40 | }) 41 | 42 | test('use class constructor for the binding name', async ({ assert }) => { 43 | const container = new Container() 44 | class Route { 45 | booted = false 46 | boot() { 47 | this.booted = true 48 | } 49 | } 50 | 51 | container.bind(Route, () => { 52 | const route = new Route() 53 | route.boot() 54 | 55 | return route 56 | }) 57 | 58 | const route = await container.make(Route) 59 | expectTypeOf(route).toEqualTypeOf() 60 | assert.instanceOf(route, Route) 61 | assert.isTrue(route.booted) 62 | }) 63 | 64 | test('disallow binding names other than string symbol or class constructor', async ({ 65 | assert, 66 | }) => { 67 | const container = new Container() 68 | 69 | assert.throws( 70 | // @ts-expect-error 71 | () => container.bind(1, () => {}), 72 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 73 | ) 74 | 75 | assert.throws( 76 | // @ts-expect-error 77 | () => container.bind([], () => {}), 78 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 79 | ) 80 | 81 | assert.throws( 82 | // @ts-expect-error 83 | () => container.bind({}, () => {}), 84 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 85 | ) 86 | }) 87 | 88 | test('return fresh value everytime from the factory function', async ({ assert }) => { 89 | const container = new Container() 90 | class Route {} 91 | 92 | container.bind('route', () => { 93 | return new Route() 94 | }) 95 | 96 | const route = await container.make('route') 97 | const route1 = await container.make('route') 98 | 99 | expectTypeOf(route).toBeAny() 100 | expectTypeOf(route1).toBeAny() 101 | assert.instanceOf(route, Route) 102 | assert.instanceOf(route1, Route) 103 | assert.notStrictEqual(route, route1) 104 | }) 105 | 106 | test('find if a binding exists', async ({ assert }) => { 107 | const container = new Container() 108 | class Route {} 109 | 110 | const routeSymbol = Symbol('route') 111 | 112 | container.bind(Route, () => new Route()) 113 | container.bind('route', () => new Route()) 114 | container.bind(routeSymbol, () => new Route()) 115 | 116 | assert.isTrue(container.hasBinding(Route)) 117 | assert.isTrue(container.hasBinding('route')) 118 | assert.isTrue(container.hasBinding(routeSymbol)) 119 | assert.isFalse(container.hasBinding('db')) 120 | }) 121 | 122 | test('find if all the bindings exists', async ({ assert }) => { 123 | const container = new Container() 124 | class Route {} 125 | 126 | const routeSymbol = Symbol('route') 127 | 128 | container.bind(Route, () => new Route()) 129 | container.bind('route', () => new Route()) 130 | container.bind(routeSymbol, () => new Route()) 131 | 132 | assert.isTrue(container.hasAllBindings([Route, 'route', routeSymbol])) 133 | assert.isFalse(container.hasAllBindings([Route, 'db', routeSymbol])) 134 | }) 135 | }) 136 | 137 | test.group('Container | Bindings Singleton', () => { 138 | test('register a singleton to the container', async ({ assert }) => { 139 | const container = new Container() 140 | class Route {} 141 | 142 | container.singleton('route', () => { 143 | return new Route() 144 | }) 145 | 146 | const route = await container.make('route') 147 | expectTypeOf(route).toBeAny() 148 | assert.instanceOf(route, Route) 149 | }) 150 | 151 | test('use symbol for the singleton name', async ({ assert }) => { 152 | const container = new Container() 153 | class Route {} 154 | 155 | const routeSymbol = Symbol('route') 156 | container.singleton(routeSymbol, () => { 157 | return new Route() 158 | }) 159 | 160 | const route = await container.make(routeSymbol) 161 | expectTypeOf(route).toBeAny() 162 | assert.instanceOf(route, Route) 163 | }) 164 | 165 | test('use class constructor for the singleton name', async ({ assert }) => { 166 | const container = new Container() 167 | class Route { 168 | booted = false 169 | boot() { 170 | this.booted = true 171 | } 172 | } 173 | 174 | container.singleton(Route, () => { 175 | const route = new Route() 176 | route.boot() 177 | 178 | return route 179 | }) 180 | 181 | const route = await container.make(Route) 182 | expectTypeOf(route).toEqualTypeOf() 183 | assert.instanceOf(route, Route) 184 | assert.isTrue(route.booted) 185 | }) 186 | 187 | test('disallow binding names other than string symbol or class constructor', async ({ 188 | assert, 189 | }) => { 190 | const container = new Container() 191 | 192 | assert.throws( 193 | // @ts-expect-error 194 | () => container.singleton(1, () => {}), 195 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 196 | ) 197 | 198 | assert.throws( 199 | // @ts-expect-error 200 | () => container.singleton([], () => {}), 201 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 202 | ) 203 | 204 | assert.throws( 205 | // @ts-expect-error 206 | () => container.singleton({}, () => {}), 207 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 208 | ) 209 | }) 210 | 211 | test('return cached value everytime from the factory function', async ({ assert }) => { 212 | const container = new Container() 213 | class Route {} 214 | 215 | container.singleton('route', () => { 216 | return new Route() 217 | }) 218 | 219 | const route = await container.make('route') 220 | const route1 = await container.make('route') 221 | 222 | expectTypeOf(route).toBeAny() 223 | expectTypeOf(route1).toBeAny() 224 | assert.instanceOf(route, Route) 225 | assert.instanceOf(route1, Route) 226 | assert.strictEqual(route, route1) 227 | }) 228 | 229 | test('find if a binding exists', async ({ assert }) => { 230 | const container = new Container() 231 | class Route {} 232 | 233 | const routeSymbol = Symbol('route') 234 | 235 | container.singleton(Route, () => new Route()) 236 | container.singleton('route', () => new Route()) 237 | container.singleton(routeSymbol, () => new Route()) 238 | 239 | assert.isTrue(container.hasBinding(Route)) 240 | assert.isTrue(container.hasBinding('route')) 241 | assert.isTrue(container.hasBinding(routeSymbol)) 242 | assert.isFalse(container.hasBinding('db')) 243 | }) 244 | 245 | test('parallel calls to make should resolve the same singleton', async ({ assert }) => { 246 | const container = new Container() 247 | class Route {} 248 | 249 | container.singleton('route', () => { 250 | return new Route() 251 | }) 252 | 253 | const [route, route1] = await Promise.all([container.make('route'), container.make('route')]) 254 | 255 | assert.instanceOf(route, Route) 256 | assert.instanceOf(route1, Route) 257 | assert.strictEqual(route, route1) 258 | }) 259 | 260 | test('fail when parallel calls to singleton fails', async ({ assert }) => { 261 | const container = new Container() 262 | container.singleton('route', () => { 263 | throw new Error('Rejected') 264 | }) 265 | 266 | const results = await Promise.allSettled([container.make('route'), container.make('route')]) 267 | 268 | assert.deepEqual( 269 | results.map((result) => result.status), 270 | ['rejected', 'rejected'] 271 | ) 272 | }) 273 | 274 | test('fail when parallel calls to async singleton fails', async ({ assert }) => { 275 | const container = new Container() 276 | container.singleton('route', async () => { 277 | throw new Error('Rejected') 278 | }) 279 | 280 | const results = await Promise.allSettled([container.make('route'), container.make('route')]) 281 | 282 | assert.deepEqual( 283 | results.map((result) => result.status), 284 | ['rejected', 'rejected'] 285 | ) 286 | }) 287 | 288 | test('fail when single call to async singleton fails', async ({ assert }) => { 289 | const container = new Container() 290 | container.singleton('route', async () => { 291 | throw new Error('Rejected') 292 | }) 293 | 294 | await assert.rejects(() => container.make('route'), 'Rejected') 295 | }) 296 | }) 297 | 298 | test.group('Container | Binding values', () => { 299 | test('register a value to the container', async ({ assert }) => { 300 | const container = new Container() 301 | class Route {} 302 | 303 | container.bindValue('route', new Route()) 304 | 305 | const route = await container.make('route') 306 | expectTypeOf(route).toBeAny() 307 | assert.instanceOf(route, Route) 308 | }) 309 | 310 | test('use symbol for the value name', async ({ assert }) => { 311 | const container = new Container() 312 | class Route {} 313 | 314 | const routeSymbol = Symbol('route') 315 | container.bindValue(routeSymbol, new Route()) 316 | 317 | const route = await container.make(routeSymbol) 318 | expectTypeOf(route).toBeAny() 319 | assert.instanceOf(route, Route) 320 | }) 321 | 322 | test('use class constructor for the value name', async ({ assert }) => { 323 | const container = new Container() 324 | class Route { 325 | booted = false 326 | boot() { 327 | this.booted = true 328 | } 329 | } 330 | 331 | const routeInstance = new Route() 332 | routeInstance.boot() 333 | 334 | container.bindValue(Route, routeInstance) 335 | 336 | const route = await container.make(Route) 337 | expectTypeOf(route).toEqualTypeOf() 338 | assert.instanceOf(route, Route) 339 | assert.isTrue(route.booted) 340 | }) 341 | 342 | test('return same value every time', async ({ assert }) => { 343 | const container = new Container() 344 | class Route {} 345 | 346 | container.bindValue('route', new Route()) 347 | 348 | const route = await container.make('route') 349 | const route1 = await container.make('route') 350 | 351 | expectTypeOf(route).toBeAny() 352 | expectTypeOf(route1).toBeAny() 353 | assert.instanceOf(route, Route) 354 | assert.instanceOf(route1, Route) 355 | assert.strictEqual(route, route1) 356 | }) 357 | 358 | test('give priority to values over bindings', async ({ assert }) => { 359 | const container = new Container() 360 | class Route {} 361 | 362 | container.bindValue('route', new Route()) 363 | container.bind('route', () => { 364 | return { foo: 'bar' } 365 | }) 366 | 367 | const route = await container.make('route') 368 | const route1 = await container.make('route') 369 | 370 | expectTypeOf(route).toBeAny() 371 | expectTypeOf(route1).toBeAny() 372 | assert.instanceOf(route, Route) 373 | assert.instanceOf(route1, Route) 374 | assert.strictEqual(route, route1) 375 | }) 376 | 377 | test('disallow binding names other than string symbol or class constructor', async ({ 378 | assert, 379 | }) => { 380 | const container = new Container() 381 | 382 | assert.throws( 383 | // @ts-expect-error 384 | () => container.bindValue(1, 1), 385 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 386 | ) 387 | 388 | assert.throws( 389 | // @ts-expect-error 390 | () => container.bindValue([], 1), 391 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 392 | ) 393 | 394 | assert.throws( 395 | // @ts-expect-error 396 | () => container.bindValue({}, 1), 397 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 398 | ) 399 | }) 400 | 401 | test('find if a binding exists', async ({ assert }) => { 402 | const container = new Container() 403 | class Route {} 404 | 405 | const routeSymbol = Symbol('route') 406 | 407 | container.bindValue(Route, new Route()) 408 | container.bindValue('route', new Route()) 409 | container.bindValue(routeSymbol, new Route()) 410 | 411 | assert.isTrue(container.hasBinding(Route)) 412 | assert.isTrue(container.hasBinding('route')) 413 | assert.isTrue(container.hasBinding(routeSymbol)) 414 | assert.isFalse(container.hasBinding('db')) 415 | }) 416 | 417 | test('find if all bindings exists', async ({ assert }) => { 418 | const container = new Container() 419 | class Route {} 420 | 421 | const routeSymbol = Symbol('route') 422 | 423 | container.bindValue(Route, new Route()) 424 | container.bindValue('route', new Route()) 425 | container.bindValue(routeSymbol, new Route()) 426 | 427 | assert.isTrue(container.hasAllBindings([Route, 'route', routeSymbol])) 428 | assert.isFalse(container.hasAllBindings([Route, 'db', routeSymbol])) 429 | }) 430 | }) 431 | 432 | test.group('Container | Contextual Bindings', () => { 433 | test('raise error when "provide" method is called before "asksFor" method', async ({ 434 | assert, 435 | }) => { 436 | const container = new Container() 437 | class Route {} 438 | 439 | assert.throws( 440 | () => container.when(Route).provide(() => {}), 441 | 'Missing value for contextual binding. Call "asksFor" method before calling the "provide" method' 442 | ) 443 | }) 444 | 445 | test('disallow contextual bindings on anything other than classes', async ({ assert }) => { 446 | const container = new Container() 447 | class Hash {} 448 | class Route {} 449 | 450 | assert.throws( 451 | // @ts-expect-error 452 | () => container.contextualBinding('route', 'hash', () => {}), 453 | 'The binding value for contextual binding should be class' 454 | ) 455 | 456 | assert.throws( 457 | // @ts-expect-error 458 | () => container.contextualBinding('route', Hash, () => {}), 459 | 'The parent value for contextual binding should be class' 460 | ) 461 | 462 | assert.throws( 463 | // @ts-expect-error 464 | () => container.contextualBinding(Route, 'hash', () => {}), 465 | 'The binding value for contextual binding should be class' 466 | ) 467 | }) 468 | }) 469 | 470 | test.group('Container | Aliases', () => { 471 | test('register an alias that point to an existing binding', async ({ assert }) => { 472 | const container = new Container() 473 | class Route {} 474 | 475 | container.bind('route', () => { 476 | return new Route() 477 | }) 478 | container.alias('adonisjs.route', 'route') 479 | 480 | const route = await container.make('adonisjs.route') 481 | expectTypeOf(route).toBeAny() 482 | assert.instanceOf(route, Route) 483 | }) 484 | 485 | test('use symbol for the alias name', async ({ assert }) => { 486 | const container = new Container() 487 | class Route {} 488 | 489 | container.bind('route', () => { 490 | return new Route() 491 | }) 492 | container.alias(Symbol.for('adonisjs.route'), 'route') 493 | 494 | const route = await container.make(Symbol.for('adonisjs.route')) 495 | expectTypeOf(route).toBeAny() 496 | assert.instanceOf(route, Route) 497 | }) 498 | 499 | test('disallow values other than string or symbol for the alias name', async ({ assert }) => { 500 | const container = new Container() 501 | 502 | assert.throws( 503 | // @ts-expect-error 504 | () => container.alias(1, 'router'), 505 | 'The container alias key must be of type "string" or "symbol"' 506 | ) 507 | 508 | assert.throws( 509 | // @ts-expect-error 510 | () => container.alias([], 'router'), 511 | 'The container alias key must be of type "string" or "symbol"' 512 | ) 513 | 514 | assert.throws( 515 | // @ts-expect-error 516 | () => container.alias({}, 'router'), 517 | 'The container alias key must be of type "string" or "symbol"' 518 | ) 519 | }) 520 | 521 | test('return true from hasBinding when checking for alias', async ({ assert }) => { 522 | const container = new Container() 523 | class Route {} 524 | 525 | const routeSymbol = Symbol('route') 526 | 527 | container.bind(Route, () => new Route()) 528 | container.bind('route', () => new Route()) 529 | container.bind(routeSymbol, () => new Route()) 530 | container.alias('adonisjs.router', 'route') 531 | 532 | assert.isTrue(container.hasBinding(Route)) 533 | assert.isTrue(container.hasBinding('route')) 534 | assert.isTrue(container.hasBinding(routeSymbol)) 535 | assert.isTrue(container.hasBinding('adonisjs.router')) 536 | assert.isFalse(container.hasBinding('db')) 537 | }) 538 | 539 | test('return true from hasAllBindings when checking for alias', async ({ assert }) => { 540 | const container = new Container() 541 | class Route {} 542 | 543 | const routeSymbol = Symbol('route') 544 | 545 | container.bind(Route, () => new Route()) 546 | container.bind('route', () => new Route()) 547 | container.bind(routeSymbol, () => new Route()) 548 | container.alias('adonisjs.router', 'route') 549 | 550 | assert.isTrue(container.hasAllBindings([Route, 'route', routeSymbol, 'adonisjs.router'])) 551 | assert.isFalse(container.hasAllBindings([Route, 'db', routeSymbol])) 552 | }) 553 | }) 554 | -------------------------------------------------------------------------------- /tests/container/call_method.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { Container } from '../../src/container.js' 13 | 14 | test.group('Container | Call method', () => { 15 | test('dis-allow method call on values other than an object', async ({ assert }) => { 16 | const container = new Container() 17 | 18 | // @ts-expect-error 19 | await assert.rejects(() => container.call(1, 'foo'), 'Missing method "foo" on "1"') 20 | 21 | await assert.rejects( 22 | // @ts-expect-error 23 | () => container.call(false, 'foo'), 24 | 'Missing method "foo" on "false"' 25 | ) 26 | 27 | await assert.rejects( 28 | // @ts-expect-error 29 | () => container.call(undefined, 'foo'), 30 | `Cannot read properties of undefined (reading 'foo')` 31 | ) 32 | 33 | await assert.rejects( 34 | // @ts-expect-error 35 | () => container.call(null, 'foo'), 36 | `Cannot read properties of null (reading 'foo')` 37 | ) 38 | 39 | await assert.rejects( 40 | // @ts-expect-error 41 | () => container.call(new Map([[1, 1]]), 'foo'), 42 | 'Missing method "foo" on "Map(1) { 1 => 1 }"' 43 | ) 44 | 45 | await assert.rejects( 46 | // @ts-expect-error 47 | () => container.call(new Set([1]), 'foo'), 48 | 'Missing method "foo" on "Set(1) { 1 }"' 49 | ) 50 | 51 | await assert.rejects( 52 | // @ts-expect-error 53 | () => container.call(['foo'], 'foo'), 54 | 'Missing method "foo" on "[ \'foo\' ]"' 55 | ) 56 | 57 | await assert.rejects( 58 | // @ts-expect-error 59 | () => container.call(function foo() {}, 'foo'), 60 | 'Missing method "foo" on "[Function: foo]"' 61 | ) 62 | }) 63 | 64 | test('invoke plain object methods without any DI', async ({ assert }) => { 65 | const container = new Container() 66 | const fooResult = await container.call({ foo: () => 'bar' }, 'foo') 67 | 68 | expectTypeOf(fooResult).toEqualTypeOf<'bar'>() 69 | assert.equal(fooResult, 'bar') 70 | }) 71 | 72 | test('invoke method on class instance', async ({ assert }) => { 73 | class UserService { 74 | foo() { 75 | return 'bar' 76 | } 77 | } 78 | 79 | const container = new Container() 80 | const fooResult = await container.call(new UserService(), 'foo') 81 | 82 | expectTypeOf(fooResult).toEqualTypeOf() 83 | assert.equal(fooResult, 'bar') 84 | }) 85 | 86 | test('inject dependencies to class method', async ({ assert }) => { 87 | class Database {} 88 | 89 | class UserService { 90 | static containerInjections = { 91 | foo: { 92 | dependencies: [Database], 93 | }, 94 | } 95 | 96 | foo(db: Database) { 97 | return db 98 | } 99 | } 100 | 101 | const container = new Container() 102 | const fooResult = await container.call(new UserService(), 'foo') 103 | 104 | expectTypeOf(fooResult).toEqualTypeOf() 105 | assert.instanceOf(fooResult, Database) 106 | }) 107 | 108 | test('throw error when injecting non class dependencies to the container', async ({ assert }) => { 109 | class Database {} 110 | 111 | class UserService { 112 | static containerInjections = { 113 | foo: { 114 | dependencies: [Database, { foo: 'bar' }, 1], 115 | createError: (message: string) => new Error(message), 116 | }, 117 | } 118 | 119 | foo(_: any, __: any, id: number) { 120 | return id 121 | } 122 | } 123 | 124 | const container = new Container() 125 | try { 126 | await container.call(new UserService(), 'foo') 127 | } catch (error) { 128 | assert.match(error.stack, /at createError \(.*call_method/) 129 | assert.equal(error.message, `Cannot inject "{ foo: 'bar' }" in "[class UserService]"`) 130 | } 131 | }) 132 | 133 | test('merge runtime values with container dependencies', async ({ assert }) => { 134 | class Database {} 135 | 136 | class UserService { 137 | static containerInjections = { 138 | foo: { 139 | dependencies: [Database, String, Number], 140 | }, 141 | } 142 | 143 | foo(db: Database, name: string, id: number) { 144 | return { db, name, id } 145 | } 146 | } 147 | 148 | const container = new Container() 149 | const fooResult = await container.call(new UserService(), 'foo', [undefined, 'foo', 1]) 150 | 151 | expectTypeOf(fooResult).toEqualTypeOf<{ db: Database; name: string; id: number }>() 152 | assert.deepEqual(fooResult, { db: new Database(), id: 1, name: 'foo' }) 153 | }) 154 | 155 | test('raise exception when method does not exist', async ({ assert }) => { 156 | class UserService {} 157 | 158 | const container = new Container() 159 | await assert.rejects( 160 | // @ts-expect-error 161 | () => container.call(new UserService(), 'foo'), 162 | 'Missing method "foo" on "UserService {}"' 163 | ) 164 | }) 165 | 166 | test('raise error when class method has dependencies but no hints', async ({ assert }) => { 167 | class UserService { 168 | foo(id: number) { 169 | return id 170 | } 171 | } 172 | 173 | const container = new Container() 174 | await assert.rejects( 175 | () => container.call(new UserService(), 'foo'), 176 | 'Cannot call "UserService.foo" method. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?' 177 | ) 178 | }) 179 | 180 | test('work fine when runtime values satisfies dependencies', async ({ assert }) => { 181 | class UserService { 182 | foo(id: number) { 183 | return id 184 | } 185 | } 186 | 187 | const container = new Container() 188 | assert.equal(await container.call(new UserService(), 'foo', [1]), 1) 189 | }) 190 | 191 | test('work fine when method param has a default value', async ({ assert }) => { 192 | class UserService { 193 | foo(id: number = 2) { 194 | return id 195 | } 196 | } 197 | 198 | const container = new Container() 199 | assert.equal(await container.call(new UserService(), 'foo'), 2) 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /tests/container/events.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { EventEmitter } from 'node:events' 12 | import { expectTypeOf } from 'expect-type' 13 | import { pEvent, pEventMultiple } from 'p-event' 14 | 15 | import { Container } from '../../src/container.js' 16 | 17 | test.group('Container | Events', () => { 18 | test('emit event when a binding is resolved', async ({ assert }) => { 19 | const emitter = new EventEmitter() 20 | const container = new Container({ emitter }) 21 | class Route {} 22 | 23 | container.bind('route', () => { 24 | return new Route() 25 | }) 26 | 27 | const [event, route] = await Promise.all([ 28 | pEvent(emitter, 'container_binding:resolved'), 29 | container.make('route'), 30 | ]) 31 | 32 | expectTypeOf(route).toBeAny() 33 | assert.instanceOf(route, Route) 34 | assert.deepEqual(event, { binding: 'route', value: route }) 35 | }) 36 | 37 | test('emit event when a singleton is resolved', async ({ assert }) => { 38 | const emitter = new EventEmitter() 39 | const container = new Container({ emitter }) 40 | class Route {} 41 | 42 | container.singleton('route', () => { 43 | return new Route() 44 | }) 45 | 46 | const [event, route] = await Promise.all([ 47 | pEvent(emitter, 'container_binding:resolved'), 48 | container.make('route'), 49 | ]) 50 | 51 | expectTypeOf(route).toBeAny() 52 | assert.instanceOf(route, Route) 53 | assert.deepEqual(event, { binding: 'route', value: route }) 54 | }) 55 | 56 | test('emit event when a singleton is resolved multiple times', async ({ assert }) => { 57 | const emitter = new EventEmitter() 58 | const container = new Container({ emitter }) 59 | class Route {} 60 | 61 | container.singleton('route', () => { 62 | return new Route() 63 | }) 64 | 65 | const [event, route] = await Promise.all([ 66 | pEvent(emitter, 'container_binding:resolved'), 67 | container.make('route'), 68 | ]) 69 | const [event1, route1] = await Promise.all([ 70 | pEvent(emitter, 'container_binding:resolved'), 71 | container.make('route'), 72 | ]) 73 | 74 | expectTypeOf(route).toBeAny() 75 | assert.instanceOf(route, Route) 76 | assert.deepEqual(event, { binding: 'route', value: route }) 77 | assert.deepEqual(event1, { binding: 'route', value: route1 }) 78 | }) 79 | 80 | test('emit event when a value is resolved', async ({ assert }) => { 81 | const emitter = new EventEmitter() 82 | const container = new Container({ emitter }) 83 | class Route {} 84 | 85 | container.bindValue('route', new Route()) 86 | 87 | const [event, route] = await Promise.all([ 88 | pEvent(emitter, 'container_binding:resolved'), 89 | container.make('route'), 90 | ]) 91 | 92 | expectTypeOf(route).toBeAny() 93 | assert.instanceOf(route, Route) 94 | assert.deepEqual(event, { binding: 'route', value: route }) 95 | }) 96 | 97 | test('emit event when class is constructed', async ({ assert }) => { 98 | const emitter = new EventEmitter() 99 | const container = new Container({ emitter }) 100 | class Route {} 101 | 102 | const [event, route] = await Promise.all([ 103 | pEvent(emitter, 'container_binding:resolved'), 104 | container.make(Route), 105 | ]) 106 | 107 | expectTypeOf(route).toEqualTypeOf() 108 | assert.instanceOf(route, Route) 109 | assert.deepEqual(event, { binding: Route, value: route }) 110 | }) 111 | 112 | test('emit event for nested dependencies', async ({ assert }) => { 113 | const emitter = new EventEmitter() 114 | const container = new Container({ emitter }) 115 | 116 | class Config {} 117 | class Encryption { 118 | static containerInjections = { 119 | _constructor: { 120 | dependencies: [Config], 121 | }, 122 | } 123 | } 124 | class Route { 125 | static containerInjections = { 126 | _constructor: { 127 | dependencies: [Encryption], 128 | }, 129 | } 130 | } 131 | 132 | const [events, route] = await Promise.all([ 133 | pEventMultiple(emitter, 'container_binding:resolved', { count: 3 }), 134 | container.make(Route), 135 | ]) 136 | 137 | expectTypeOf(route).toEqualTypeOf() 138 | assert.instanceOf(route, Route) 139 | assert.lengthOf(events, 3) 140 | 141 | assert.deepEqual(events[0], { binding: Config, value: new Config() }) 142 | assert.deepEqual(events[1], { binding: Encryption, value: new Encryption() }) 143 | assert.deepEqual(events[2], { binding: Route, value: route }) 144 | }) 145 | 146 | test('emit event when swaps are resolved', async ({ assert }) => { 147 | const emitter = new EventEmitter() 148 | const container = new Container({ emitter }) 149 | class Route {} 150 | class FakedRoute extends Route {} 151 | 152 | container.swap(Route, () => new FakedRoute()) 153 | 154 | const [event, route] = await Promise.all([ 155 | pEvent(emitter, 'container_binding:resolved'), 156 | container.make(Route), 157 | ]) 158 | 159 | expectTypeOf(route).toEqualTypeOf() 160 | assert.instanceOf(route, Route) 161 | assert.instanceOf(route, FakedRoute) 162 | assert.deepEqual(event, { binding: Route, value: route }) 163 | }) 164 | 165 | test('register emitter using the useEmitter method', async ({ assert }) => { 166 | const emitter = new EventEmitter() 167 | const container = new Container() 168 | class Route {} 169 | 170 | container.useEmitter(emitter) 171 | container.bind('route', () => { 172 | return new Route() 173 | }) 174 | 175 | const [event, route] = await Promise.all([ 176 | pEvent(emitter, 'container_binding:resolved'), 177 | container.make('route'), 178 | ]) 179 | 180 | expectTypeOf(route).toBeAny() 181 | assert.instanceOf(route, Route) 182 | assert.deepEqual(event, { binding: 'route', value: route }) 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /tests/container/hooks.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { EventEmitter } from 'node:events' 12 | import { expectTypeOf } from 'expect-type' 13 | 14 | import { Container } from '../../src/container.js' 15 | 16 | test.group('Container | Hooks', () => { 17 | test('run hook when a binding is resolved', async ({ assert }) => { 18 | const emitter = new EventEmitter() 19 | const container = new Container<{ route: Route }>({ emitter }) 20 | class Route { 21 | pattern!: string 22 | } 23 | 24 | container.bind('route', () => { 25 | return new Route() 26 | }) 27 | 28 | container.resolving('route', (route) => { 29 | expectTypeOf(route).toEqualTypeOf() 30 | route.pattern = '/' 31 | }) 32 | 33 | const route = await container.make('route') 34 | expectTypeOf(route).toEqualTypeOf() 35 | 36 | assert.instanceOf(route, Route) 37 | assert.equal(route.pattern, '/') 38 | }) 39 | 40 | test('run hook when an alias is resolved', async ({ assert }) => { 41 | const emitter = new EventEmitter() 42 | const container = new Container<{ 'route': Route; 'adonisjs.route': Route }>({ emitter }) 43 | class Route { 44 | pattern!: string 45 | } 46 | 47 | container.bind('route', () => { 48 | return new Route() 49 | }) 50 | container.alias('adonisjs.route', 'route') 51 | 52 | container.resolving('adonisjs.route', (route) => { 53 | expectTypeOf(route).toEqualTypeOf() 54 | route.pattern = '/' 55 | }) 56 | 57 | const route = await container.make('adonisjs.route') 58 | expectTypeOf(route).toEqualTypeOf() 59 | 60 | assert.instanceOf(route, Route) 61 | assert.equal(route.pattern, '/') 62 | }) 63 | 64 | test('run hook listening for binding when an alias is resolved', async ({ assert }) => { 65 | const emitter = new EventEmitter() 66 | const container = new Container<{ 'route': Route; 'adonisjs.route': Route }>({ emitter }) 67 | class Route { 68 | pattern!: string 69 | } 70 | 71 | container.bind('route', () => { 72 | return new Route() 73 | }) 74 | container.alias('adonisjs.route', 'route') 75 | 76 | container.resolving('route', (route) => { 77 | expectTypeOf(route).toEqualTypeOf() 78 | route.pattern = '/' 79 | }) 80 | 81 | const route = await container.make('adonisjs.route') 82 | expectTypeOf(route).toEqualTypeOf() 83 | 84 | assert.instanceOf(route, Route) 85 | assert.equal(route.pattern, '/') 86 | }) 87 | 88 | test('run hook listening for alias when binding is resolved', async ({ assert }) => { 89 | const emitter = new EventEmitter() 90 | const container = new Container<{ 'route': Route; 'adonisjs.route': Route }>({ emitter }) 91 | class Route { 92 | pattern!: string 93 | } 94 | 95 | container.bind('route', () => { 96 | return new Route() 97 | }) 98 | container.alias('adonisjs.route', 'route') 99 | 100 | container.resolving('adonisjs.route', (route) => { 101 | expectTypeOf(route).toEqualTypeOf() 102 | route.pattern = '/' 103 | }) 104 | 105 | const route = await container.make('route') 106 | expectTypeOf(route).toEqualTypeOf() 107 | 108 | assert.instanceOf(route, Route) 109 | assert.equal(route.pattern, '/') 110 | }) 111 | 112 | test('do not run hooks when values are resolved', async ({ assert }) => { 113 | const emitter = new EventEmitter() 114 | const container = new Container<{ route: Route }>({ emitter }) 115 | class Route { 116 | invocations: number = 0 117 | } 118 | 119 | container.bindValue('route', new Route()) 120 | 121 | container.resolving('route', (route) => { 122 | expectTypeOf(route).toEqualTypeOf() 123 | route.invocations++ 124 | }) 125 | 126 | await container.make('route') 127 | await container.make('route') 128 | await container.make('route') 129 | 130 | const route = await container.make('route') 131 | expectTypeOf(route).toEqualTypeOf() 132 | 133 | assert.instanceOf(route, Route) 134 | assert.equal(route.invocations, 0) 135 | }) 136 | 137 | test('run hooks when classes are constructed', async ({ assert }) => { 138 | const emitter = new EventEmitter() 139 | const container = new Container({ emitter }) 140 | class Route { 141 | invocations: number = 0 142 | } 143 | 144 | container.resolving(Route, (route) => { 145 | expectTypeOf(route).toEqualTypeOf() 146 | route.invocations++ 147 | }) 148 | 149 | const route = await container.make(Route) 150 | expectTypeOf(route).toEqualTypeOf() 151 | 152 | assert.instanceOf(route, Route) 153 | assert.equal(route.invocations, 1) 154 | }) 155 | 156 | test('run hooks when a swap is resolved', async ({ assert }) => { 157 | const emitter = new EventEmitter() 158 | const container = new Container({ emitter }) 159 | class Route { 160 | invocations: number = 0 161 | } 162 | class FakedRoute extends Route {} 163 | 164 | container.resolving(Route, (route) => { 165 | expectTypeOf(route).toEqualTypeOf() 166 | route.invocations++ 167 | }) 168 | 169 | container.swap(Route, () => { 170 | return new FakedRoute() 171 | }) 172 | 173 | const route = await container.make(Route) 174 | expectTypeOf(route).toEqualTypeOf() 175 | 176 | assert.instanceOf(route, Route) 177 | assert.instanceOf(route, FakedRoute) 178 | assert.equal(route.invocations, 1) 179 | }) 180 | 181 | test('run hook once when a singleton resolved', async ({ assert }) => { 182 | const emitter = new EventEmitter() 183 | const container = new Container<{ route: Route }>({ emitter }) 184 | class Route { 185 | invocations: number = 0 186 | } 187 | 188 | container.singleton('route', () => { 189 | return new Route() 190 | }) 191 | 192 | container.resolving('route', (route) => { 193 | expectTypeOf(route).toEqualTypeOf() 194 | route.invocations++ 195 | }) 196 | 197 | await container.make('route') 198 | await container.make('route') 199 | await container.make('route') 200 | 201 | const route = await container.make('route') 202 | expectTypeOf(route).toEqualTypeOf() 203 | 204 | assert.instanceOf(route, Route) 205 | assert.equal(route.invocations, 1) 206 | }) 207 | 208 | test('call hook once when a singleton is resolved parallely', async ({ assert }) => { 209 | const emitter = new EventEmitter() 210 | const container = new Container<{ route: Route }>({ emitter }) 211 | class Route { 212 | invocations: number = 0 213 | } 214 | 215 | container.singleton('route', () => { 216 | return new Route() 217 | }) 218 | 219 | container.resolving('route', (route) => { 220 | expectTypeOf(route).toEqualTypeOf() 221 | route.invocations++ 222 | }) 223 | 224 | await Promise.all([container.make('route'), container.make('route'), container.make('route')]) 225 | 226 | const route = await container.make('route') 227 | expectTypeOf(route).toEqualTypeOf() 228 | 229 | assert.instanceOf(route, Route) 230 | assert.equal(route.invocations, 1) 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /tests/container/known_bindings.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { Container } from '../../src/container.js' 13 | 14 | test.group('Container | Bindings', () => { 15 | test('register a binding to the container', async ({ assert }) => { 16 | const container = new Container<{ route: Route }>() 17 | class Route {} 18 | 19 | container.bind('route', () => { 20 | return new Route() 21 | }) 22 | 23 | const route = await container.make('route') 24 | expectTypeOf(route).toEqualTypeOf() 25 | assert.instanceOf(route, Route) 26 | }) 27 | 28 | test('resolve binding using the resolver', async ({ assert }) => { 29 | const container = new Container<{ route: Route }>() 30 | class Route {} 31 | 32 | container.bind('route', () => { 33 | return new Route() 34 | }) 35 | 36 | const resolver = container.createResolver() 37 | const route = await resolver.make('route') 38 | 39 | expectTypeOf(route).toEqualTypeOf() 40 | assert.instanceOf(route, Route) 41 | }) 42 | 43 | test('use symbol for the binding name', async ({ assert }) => { 44 | class Route {} 45 | const routeSymbol = Symbol('route') 46 | 47 | const container = new Container<{ [routeSymbol]: Route }>() 48 | container.bind(routeSymbol, () => { 49 | return new Route() 50 | }) 51 | 52 | const route = await container.make(routeSymbol) 53 | expectTypeOf(route).toEqualTypeOf() 54 | assert.instanceOf(route, Route) 55 | }) 56 | 57 | test('use class constructor for the binding name', async ({ assert }) => { 58 | class Route {} 59 | 60 | const container = new Container() 61 | container.bind(Route, () => { 62 | return new Route() 63 | }) 64 | 65 | const route = await container.make(Route) 66 | expectTypeOf(route).toEqualTypeOf() 67 | assert.instanceOf(route, Route) 68 | }) 69 | 70 | test('return fresh value everytime from the factory function', async ({ assert }) => { 71 | class Route {} 72 | const container = new Container<{ route: Route }>() 73 | 74 | container.bind('route', () => { 75 | return new Route() 76 | }) 77 | 78 | const route = await container.make('route') 79 | const route1 = await container.make('route') 80 | 81 | expectTypeOf(route).toEqualTypeOf() 82 | expectTypeOf(route1).toEqualTypeOf() 83 | assert.instanceOf(route, Route) 84 | assert.instanceOf(route1, Route) 85 | assert.notStrictEqual(route, route1) 86 | }) 87 | }) 88 | 89 | test.group('Container | Bindings Singleton', () => { 90 | test('register a singleton to the container', async ({ assert }) => { 91 | class Route {} 92 | const container = new Container<{ route: Route }>() 93 | 94 | container.singleton('route', () => { 95 | return new Route() 96 | }) 97 | 98 | const route = await container.make('route') 99 | 100 | expectTypeOf(route).toEqualTypeOf() 101 | assert.instanceOf(route, Route) 102 | }) 103 | 104 | test('resolve singleton using the resolver', async ({ assert }) => { 105 | class Route {} 106 | const container = new Container<{ route: Route }>() 107 | 108 | container.singleton('route', () => { 109 | return new Route() 110 | }) 111 | 112 | const resolver = container.createResolver() 113 | const route = await resolver.make('route') 114 | 115 | expectTypeOf(route).toEqualTypeOf() 116 | assert.instanceOf(route, Route) 117 | }) 118 | 119 | test('use symbol for the singleton name', async ({ assert }) => { 120 | class Route {} 121 | const routeSymbol = Symbol('route') 122 | const container = new Container<{ [routeSymbol]: Route }>() 123 | 124 | container.singleton(routeSymbol, () => { 125 | return new Route() 126 | }) 127 | 128 | const route = await container.make(routeSymbol) 129 | 130 | expectTypeOf(route).toEqualTypeOf() 131 | assert.instanceOf(route, Route) 132 | }) 133 | 134 | test('use class constructor for the singleton name', async ({ assert }) => { 135 | const container = new Container() 136 | class Route {} 137 | 138 | container.singleton(Route, () => { 139 | return new Route() 140 | }) 141 | 142 | const route = await container.make(Route) 143 | 144 | expectTypeOf(route).toEqualTypeOf() 145 | assert.instanceOf(route, Route) 146 | }) 147 | 148 | test('return cached value everytime from the factory function', async ({ assert }) => { 149 | class Route {} 150 | const container = new Container<{ route: Route }>() 151 | 152 | container.singleton('route', () => { 153 | return new Route() 154 | }) 155 | 156 | const route = await container.make('route') 157 | const route1 = await container.make('route') 158 | 159 | expectTypeOf(route).toEqualTypeOf() 160 | expectTypeOf(route1).toEqualTypeOf() 161 | assert.instanceOf(route, Route) 162 | assert.instanceOf(route1, Route) 163 | assert.strictEqual(route, route1) 164 | }) 165 | }) 166 | 167 | test.group('Container | Binding values', () => { 168 | test('register a value to the container', async ({ assert }) => { 169 | class Route {} 170 | const container = new Container<{ route: Route }>() 171 | 172 | container.bindValue('route', new Route()) 173 | 174 | const route = await container.make('route') 175 | 176 | expectTypeOf(route).toEqualTypeOf() 177 | assert.instanceOf(route, Route) 178 | }) 179 | 180 | test('resolve value using the container resolver', async ({ assert }) => { 181 | class Route {} 182 | const container = new Container<{ route: Route }>() 183 | 184 | container.bindValue('route', new Route()) 185 | 186 | const resolver = container.createResolver() 187 | const route = await resolver.make('route') 188 | 189 | expectTypeOf(route).toEqualTypeOf() 190 | assert.instanceOf(route, Route) 191 | }) 192 | 193 | test('use symbol for the value name', async ({ assert }) => { 194 | class Route {} 195 | const routeSymbol = Symbol('route') 196 | const container = new Container<{ [routeSymbol]: Route }>() 197 | 198 | container.bindValue(routeSymbol, new Route()) 199 | 200 | const route = await container.make(routeSymbol) 201 | 202 | expectTypeOf(route).toEqualTypeOf() 203 | assert.instanceOf(route, Route) 204 | }) 205 | 206 | test('use class constructor for the value name', async ({ assert }) => { 207 | const container = new Container() 208 | class Route {} 209 | 210 | container.bindValue(Route, new Route()) 211 | 212 | const route = await container.make(Route) 213 | 214 | expectTypeOf(route).toEqualTypeOf() 215 | assert.instanceOf(route, Route) 216 | }) 217 | 218 | test('return same value every time', async ({ assert }) => { 219 | const container = new Container<{ route: Route }>() 220 | class Route {} 221 | 222 | container.bindValue('route', new Route()) 223 | 224 | const route = await container.make('route') 225 | const route1 = await container.make('route') 226 | 227 | expectTypeOf(route).toEqualTypeOf() 228 | expectTypeOf(route1).toEqualTypeOf() 229 | assert.instanceOf(route, Route) 230 | assert.instanceOf(route1, Route) 231 | assert.strictEqual(route, route1) 232 | }) 233 | 234 | test('give priority to values over bindings', async ({ assert }) => { 235 | class Route {} 236 | const container = new Container<{ route: Route }>() 237 | 238 | container.bindValue('route', new Route()) 239 | container.bind('route', () => { 240 | return { foo: 'bar' } 241 | }) 242 | 243 | const route = await container.make('route') 244 | const route1 = await container.make('route') 245 | 246 | expectTypeOf(route).toEqualTypeOf() 247 | expectTypeOf(route1).toEqualTypeOf() 248 | assert.instanceOf(route, Route) 249 | assert.instanceOf(route1, Route) 250 | assert.strictEqual(route, route1) 251 | }) 252 | }) 253 | 254 | test.group('Container | Aliases', () => { 255 | test('register an alias that point to an existing binding', async ({ assert }) => { 256 | const container = new Container<{ 'route': Route; 'adonisjs.route': Route; 'foo': Foo }>() 257 | 258 | class Route { 259 | makeUrl() {} 260 | } 261 | class Foo {} 262 | 263 | container.bind('route', () => { 264 | return new Route() 265 | }) 266 | // @ts-expect-error 267 | container.alias('adonisjs.route', 'foo') 268 | container.alias('adonisjs.route', 'route') 269 | 270 | const route = await container.make('adonisjs.route') 271 | expectTypeOf(route).toEqualTypeOf() 272 | assert.instanceOf(route, Route) 273 | }) 274 | 275 | test('use symbol for the alias name', async ({ assert }) => { 276 | const aliasSymbol = Symbol('adonisjs.route') 277 | const container = new Container<{ route: Route; [aliasSymbol]: Route }>() 278 | class Route { 279 | makeUrl() {} 280 | } 281 | 282 | container.bind('route', () => { 283 | return new Route() 284 | }) 285 | 286 | container.alias(aliasSymbol, 'route') 287 | 288 | const route = await container.make(aliasSymbol) 289 | expectTypeOf(route).toEqualTypeOf() 290 | assert.instanceOf(route, Route) 291 | }) 292 | 293 | test('make alias point to class constructor', async ({ assert }) => { 294 | const container = new Container<{ 'adonisjs.route': Route }>() 295 | class Route { 296 | makeUrl() {} 297 | } 298 | class Foo {} 299 | 300 | container.bind(Route, () => { 301 | return new Route() 302 | }) 303 | 304 | // @ts-expect-error 305 | container.alias('adonisjs.route', Foo) 306 | container.alias('adonisjs.route', Route) 307 | 308 | const route = await container.make('adonisjs.route') 309 | expectTypeOf(route).toEqualTypeOf() 310 | assert.instanceOf(route, Route) 311 | }) 312 | 313 | test('disallow values other than string or symbol for the alias name', async ({ assert }) => { 314 | const container = new Container<{ 'route': Route; 'adonisjs.route': Route }>() 315 | class Route { 316 | makeUrl() {} 317 | } 318 | 319 | assert.throws( 320 | // @ts-expect-error 321 | () => container.alias(1, 'route'), 322 | 'The container alias key must be of type "string" or "symbol"' 323 | ) 324 | 325 | assert.throws( 326 | // @ts-expect-error 327 | () => container.alias([], 'route'), 328 | 'The container alias key must be of type "string" or "symbol"' 329 | ) 330 | 331 | assert.throws( 332 | // @ts-expect-error 333 | () => container.alias({}, 'route'), 334 | 'The container alias key must be of type "string" or "symbol"' 335 | ) 336 | }) 337 | 338 | test('return true from hasBinding when checking for alias', async ({ assert }) => { 339 | const routeSymbol = Symbol('route') 340 | 341 | const container = new Container<{ 342 | [routeSymbol]: Route 343 | 'route': Route 344 | 'adonisjs.router': Route 345 | }>() 346 | class Route {} 347 | 348 | container.bind(Route, () => new Route()) 349 | container.bind('route', () => new Route()) 350 | container.bind(routeSymbol, () => new Route()) 351 | container.alias('adonisjs.router', 'route') 352 | 353 | assert.isTrue(container.hasBinding(Route)) 354 | assert.isTrue(container.hasBinding('route')) 355 | assert.isTrue(container.hasBinding(routeSymbol)) 356 | assert.isTrue(container.hasBinding('adonisjs.router')) 357 | assert.isFalse(container.hasBinding('db')) 358 | }) 359 | 360 | test('return true from hasAllBindings when checking for alias', async ({ assert }) => { 361 | const routeSymbol = Symbol('route') 362 | 363 | const container = new Container<{ 364 | [routeSymbol]: Route 365 | 'route': Route 366 | 'adonisjs.router': Route 367 | }>() 368 | class Route {} 369 | 370 | container.bind(Route, () => new Route()) 371 | container.bind('route', () => new Route()) 372 | container.bind(routeSymbol, () => new Route()) 373 | container.alias('adonisjs.router', 'route') 374 | 375 | assert.isTrue(container.hasAllBindings([Route, 'route', routeSymbol, 'adonisjs.router'])) 376 | assert.isFalse(container.hasAllBindings([Route, 'db', routeSymbol])) 377 | }) 378 | }) 379 | -------------------------------------------------------------------------------- /tests/container/known_make_class.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { Container } from '../../src/container.js' 13 | 14 | test.group('Container | Make class | Known bindings', () => { 15 | test('create a fresh instance of the container', ({ assert }) => { 16 | assert.instanceOf(new Container(), Container) 17 | }) 18 | 19 | test('throw error when unsupported data type is given to the container', async ({ assert }) => { 20 | const container = new Container<{ foo: 'bar' }>() 21 | 22 | try { 23 | const obj = await container.make({ foo: 'bar' }) 24 | expectTypeOf(obj).toEqualTypeOf() 25 | } catch (error) { 26 | assert.equal(error.message, `Cannot construct value "{ foo: 'bar' }" using container`) 27 | } 28 | 29 | try { 30 | const numeric = await container.make(1) 31 | expectTypeOf(numeric).toEqualTypeOf() 32 | } catch (error) { 33 | assert.equal(error.message, `Cannot construct value "1" using container`) 34 | } 35 | 36 | try { 37 | const bool = await container.make(false) 38 | expectTypeOf(bool).toEqualTypeOf() 39 | } catch (error) { 40 | assert.equal(error.message, `Cannot construct value "false" using container`) 41 | } 42 | 43 | try { 44 | const notDefined = await container.make(undefined) 45 | expectTypeOf(notDefined).toEqualTypeOf() 46 | } catch (error) { 47 | assert.equal(error.message, `Cannot construct value "undefined" using container`) 48 | } 49 | 50 | try { 51 | const nullValue = await container.make(null) 52 | expectTypeOf(nullValue).toEqualTypeOf() 53 | } catch (error) { 54 | assert.equal(error.message, `Cannot construct value "null" using container`) 55 | } 56 | 57 | try { 58 | const mapValue = await container.make(new Map([[1, 1]])) 59 | expectTypeOf(mapValue).toEqualTypeOf() 60 | } catch (error) { 61 | assert.equal(error.message, `Cannot construct value "Map(1) { 1 => 1 }" using container`) 62 | } 63 | 64 | try { 65 | const setValue = await container.make(new Set([1])) 66 | expectTypeOf(setValue).toEqualTypeOf() 67 | } catch (error) { 68 | assert.equal(error.message, `Cannot construct value "Set(1) { 1 }" using container`) 69 | } 70 | 71 | try { 72 | const arrayValue = await container.make(['foo']) 73 | expectTypeOf(arrayValue).toEqualTypeOf() 74 | } catch (error) { 75 | assert.equal(error.message, `Cannot construct value "[ 'foo' ]" using container`) 76 | } 77 | 78 | function foo() {} 79 | try { 80 | const func = await container.make(foo) 81 | expectTypeOf(func).toEqualTypeOf() 82 | } catch (error) { 83 | assert.equal(error.message, `Cannot construct value "[Function: foo]" using container`) 84 | } 85 | }) 86 | 87 | test('throw error when unable to resolve a binding by name', async ({ assert }) => { 88 | const container = new Container<{ foo: 'bar' }>() 89 | 90 | try { 91 | const obj = await container.make('bar') 92 | expectTypeOf(obj).toEqualTypeOf() 93 | } catch (error) { 94 | assert.equal(error.message, `Cannot resolve binding "bar" from the container`) 95 | } 96 | 97 | try { 98 | const obj = await container.make(Symbol('bar')) 99 | expectTypeOf(obj).toEqualTypeOf() 100 | } catch (error) { 101 | assert.equal(error.message, `Cannot resolve binding "Symbol(bar)" from the container`) 102 | } 103 | }) 104 | 105 | test('make instance of a class using the container', async ({ assert }) => { 106 | class UserService { 107 | foo = 'bar' 108 | } 109 | const container = new Container<{ foo: 'bar' }>() 110 | const service = await container.make(UserService) 111 | 112 | expectTypeOf(service).toEqualTypeOf() 113 | assert.instanceOf(service, UserService) 114 | assert.equal(service.foo, 'bar') 115 | }) 116 | 117 | test('multiple calls to make should return a fresh instance', async ({ assert }) => { 118 | class UserService { 119 | foo = 'bar' 120 | } 121 | const container = new Container<{ foo: 'bar' }>() 122 | const service = await container.make(UserService) 123 | const service1 = await container.make(UserService) 124 | 125 | expectTypeOf(service).toEqualTypeOf() 126 | expectTypeOf(service1).toEqualTypeOf() 127 | assert.instanceOf(service, UserService) 128 | assert.instanceOf(service1, UserService) 129 | assert.notStrictEqual(service1, service) 130 | assert.equal(service.foo, 'bar') 131 | }) 132 | 133 | test('inject constructor dependencies as defined in containerInjections', async ({ assert }) => { 134 | class Database {} 135 | 136 | class UserService { 137 | static containerInjections = { 138 | _constructor: { 139 | dependencies: [Database], 140 | createError: (message: string) => new Error(message), 141 | }, 142 | } 143 | constructor(public db: Database) {} 144 | } 145 | 146 | const container = new Container<{ foo: 'bar' }>() 147 | const service = await container.make(UserService) 148 | 149 | expectTypeOf(service).toEqualTypeOf() 150 | assert.instanceOf(service, UserService) 151 | assert.instanceOf(service.db, Database) 152 | }) 153 | 154 | test('throw error when injecting non-class values to the constructor', async ({ assert }) => { 155 | assert.plan(2) 156 | 157 | class UserService { 158 | args: any[] 159 | 160 | static containerInjections = { 161 | _constructor: { 162 | dependencies: [{ foo: 'bar' }, 1, ['foo'], false, undefined, null, false], 163 | createError: (message: string) => new Error(message), 164 | }, 165 | } 166 | constructor(...args: any[]) { 167 | this.args = args 168 | } 169 | } 170 | 171 | const container = new Container<{ foo: 'bar' }>() 172 | try { 173 | await container.make(UserService) 174 | } catch (error) { 175 | assert.match(error.stack, /at createError \(.*known_make_class/) 176 | assert.equal(error.message, `Cannot inject "{ foo: 'bar' }" in "[class UserService]"`) 177 | } 178 | }) 179 | 180 | test('fail when class has dependencies when containerInjections are empty', async ({ 181 | assert, 182 | }) => { 183 | class UserService { 184 | static containerInjections = { 185 | _constructor: { 186 | dependencies: [], 187 | createError: (message: string) => new Error(message), 188 | }, 189 | } 190 | constructor(public name: string) {} 191 | } 192 | 193 | const container = new Container() 194 | await assert.rejects( 195 | () => container.make(UserService), 196 | 'Cannot construct "[class UserService]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?' 197 | ) 198 | }) 199 | 200 | test('raise error when injecting a primitive class', async ({ assert }) => { 201 | class UserService { 202 | static containerInjections = { 203 | _constructor: { 204 | dependencies: [String], 205 | createError: (message: string) => new Error(message), 206 | }, 207 | } 208 | constructor() {} 209 | } 210 | 211 | const container = new Container<{ foo: 'bar' }>() 212 | try { 213 | await container.make(UserService) 214 | } catch (error) { 215 | assert.match(error.stack, /at createError \(.*known_make_class/) 216 | assert.equal(error.message, 'Cannot inject "[Function: String]" in "[class UserService]"') 217 | } 218 | }) 219 | 220 | test('throw error when constructing primitive values', async ({ assert }) => { 221 | const container = new Container<{ foo: 'bar' }>() 222 | 223 | await assert.rejects( 224 | () => container.make(String), 225 | 'Cannot construct value "[Function: String]" using container' 226 | ) 227 | }) 228 | 229 | test('raise error when class has dependencies but no hints', async ({ assert }) => { 230 | class UserService { 231 | constructor(public config: { foo: string }) {} 232 | } 233 | 234 | const container = new Container<{ foo: 'bar' }>() 235 | await assert.rejects( 236 | () => container.make(UserService), 237 | 'Cannot construct "[class UserService]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?' 238 | ) 239 | }) 240 | 241 | test('work fine when runtime values satisfies dependencies', async ({ assert }) => { 242 | class UserService { 243 | constructor(public config: { foo: string }) {} 244 | } 245 | 246 | const container = new Container<{ foo: 'bar' }>() 247 | const service = await container.make(UserService, [{ foo: 'bar' }]) 248 | assert.equal(service.config.foo, 'bar') 249 | }) 250 | 251 | test('work fine when constructor parameter has a default value', async ({ assert }) => { 252 | class UserService { 253 | constructor(public config: { foo: string } = { foo: 'baz' }) {} 254 | } 255 | 256 | const container = new Container<{ foo: 'bar' }>() 257 | const service = await container.make(UserService) 258 | assert.equal(service.config.foo, 'baz') 259 | }) 260 | }) 261 | -------------------------------------------------------------------------------- /tests/container/make_class.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { Container } from '../../src/container.js' 13 | 14 | test.group('Container | Make class', () => { 15 | test('create a fresh instance of the container', ({ assert }) => { 16 | assert.instanceOf(new Container(), Container) 17 | }) 18 | 19 | test('throw error when unsupported data type is given to the container', async ({ assert }) => { 20 | const container = new Container() 21 | 22 | try { 23 | const obj = await container.make({ foo: 'bar' }) 24 | expectTypeOf(obj).toEqualTypeOf() 25 | } catch (error) { 26 | assert.equal(error.message, `Cannot construct value "{ foo: 'bar' }" using container`) 27 | } 28 | 29 | try { 30 | const numeric = await container.make(1) 31 | expectTypeOf(numeric).toEqualTypeOf() 32 | } catch (error) { 33 | assert.equal(error.message, `Cannot construct value "1" using container`) 34 | } 35 | 36 | try { 37 | const bool = await container.make(false) 38 | expectTypeOf(bool).toEqualTypeOf() 39 | } catch (error) { 40 | assert.equal(error.message, `Cannot construct value "false" using container`) 41 | } 42 | 43 | try { 44 | const notDefined = await container.make(undefined) 45 | expectTypeOf(notDefined).toEqualTypeOf() 46 | } catch (error) { 47 | assert.equal(error.message, `Cannot construct value "undefined" using container`) 48 | } 49 | 50 | try { 51 | const nullValue = await container.make(null) 52 | expectTypeOf(nullValue).toEqualTypeOf() 53 | } catch (error) { 54 | assert.equal(error.message, `Cannot construct value "null" using container`) 55 | } 56 | 57 | try { 58 | const mapValue = await container.make(new Map([[1, 1]])) 59 | expectTypeOf(mapValue).toEqualTypeOf() 60 | } catch (error) { 61 | assert.equal(error.message, `Cannot construct value "Map(1) { 1 => 1 }" using container`) 62 | } 63 | 64 | try { 65 | const setValue = await container.make(new Set([1])) 66 | expectTypeOf(setValue).toEqualTypeOf() 67 | } catch (error) { 68 | assert.equal(error.message, `Cannot construct value "Set(1) { 1 }" using container`) 69 | } 70 | 71 | try { 72 | const arrayValue = await container.make(['foo']) 73 | expectTypeOf(arrayValue).toEqualTypeOf() 74 | } catch (error) { 75 | assert.equal(error.message, `Cannot construct value "[ 'foo' ]" using container`) 76 | } 77 | 78 | function foo() {} 79 | try { 80 | const func = await container.make(foo) 81 | expectTypeOf(func).toEqualTypeOf() 82 | } catch (error) { 83 | assert.equal(error.message, `Cannot construct value "[Function: foo]" using container`) 84 | } 85 | }) 86 | 87 | test('throw error when unable to resolve a binding by name', async ({ assert }) => { 88 | const container = new Container() 89 | 90 | try { 91 | const obj = await container.make('bar') 92 | expectTypeOf(obj).toEqualTypeOf() 93 | } catch (error) { 94 | assert.equal(error.message, `Cannot resolve binding "bar" from the container`) 95 | } 96 | 97 | try { 98 | const obj = await container.make(Symbol('bar')) 99 | expectTypeOf(obj).toEqualTypeOf() 100 | } catch (error) { 101 | assert.equal(error.message, `Cannot resolve binding "Symbol(bar)" from the container`) 102 | } 103 | }) 104 | 105 | test('make instance of a class using the container', async ({ assert }) => { 106 | class UserService { 107 | foo = 'bar' 108 | } 109 | const container = new Container() 110 | const service = await container.make(UserService) 111 | 112 | expectTypeOf(service).toEqualTypeOf() 113 | assert.instanceOf(service, UserService) 114 | assert.equal(service.foo, 'bar') 115 | }) 116 | 117 | test('multiple calls to make should return a fresh instance', async ({ assert }) => { 118 | class UserService { 119 | foo = 'bar' 120 | } 121 | const container = new Container() 122 | const service = await container.make(UserService) 123 | const service1 = await container.make(UserService) 124 | 125 | expectTypeOf(service).toEqualTypeOf() 126 | expectTypeOf(service1).toEqualTypeOf() 127 | assert.instanceOf(service, UserService) 128 | assert.instanceOf(service1, UserService) 129 | assert.notStrictEqual(service1, service) 130 | assert.equal(service.foo, 'bar') 131 | }) 132 | 133 | test('inject constructor dependencies as defined in containerInjections', async ({ assert }) => { 134 | class Database {} 135 | 136 | class UserService { 137 | static containerInjections = { 138 | _constructor: { 139 | dependencies: [Database], 140 | }, 141 | } 142 | constructor(public db: Database) {} 143 | } 144 | 145 | const container = new Container() 146 | const service = await container.make(UserService) 147 | 148 | expectTypeOf(service).toEqualTypeOf() 149 | assert.instanceOf(service, UserService) 150 | assert.instanceOf(service.db, Database) 151 | }) 152 | 153 | test('throw error when injecting non-class values to the constructor', async ({ assert }) => { 154 | assert.plan(2) 155 | 156 | class UserService { 157 | args: any[] 158 | 159 | static containerInjections = { 160 | _constructor: { 161 | dependencies: [{ foo: 'bar' }, 1, ['foo'], false, undefined, null, false], 162 | createError: (message: string) => new Error(message), 163 | }, 164 | } 165 | constructor(...args: any[]) { 166 | this.args = args 167 | } 168 | } 169 | 170 | const container = new Container() 171 | try { 172 | await container.make(UserService) 173 | } catch (error) { 174 | assert.match(error.stack, /at createError \(.*make_class/) 175 | assert.equal(error.message, `Cannot inject "{ foo: 'bar' }" in "[class UserService]"`) 176 | } 177 | }) 178 | 179 | test('fail when class has dependencies but containerInjections are empty', async ({ assert }) => { 180 | class UserService { 181 | static containerInjections = { 182 | _constructor: { 183 | dependencies: [], 184 | }, 185 | } 186 | constructor(public name: string) {} 187 | } 188 | 189 | const container = new Container() 190 | await assert.rejects( 191 | () => container.make(UserService), 192 | 'Cannot construct "[class UserService]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?' 193 | ) 194 | }) 195 | 196 | test('raise error when injecting a primitive class', async ({ assert }) => { 197 | assert.plan(2) 198 | 199 | class UserService { 200 | static containerInjections = { 201 | _constructor: { 202 | dependencies: [String], 203 | createError: (message: string) => new Error(message), 204 | }, 205 | } 206 | constructor() {} 207 | } 208 | 209 | const container = new Container() 210 | try { 211 | await container.make(UserService) 212 | } catch (error) { 213 | assert.match(error.stack, /at createError \(.*make_class/) 214 | assert.equal(error.message, 'Cannot inject "[Function: String]" in "[class UserService]"') 215 | } 216 | }) 217 | 218 | test('throw error when constructing primitive values', async ({ assert }) => { 219 | const container = new Container() 220 | 221 | await assert.rejects( 222 | () => container.make(String), 223 | 'Cannot construct value "[Function: String]" using container' 224 | ) 225 | }) 226 | 227 | test('raise error when class has dependencies but no hints', async ({ assert }) => { 228 | class UserService { 229 | constructor(public config: { foo: string }) {} 230 | } 231 | 232 | const container = new Container() 233 | await assert.rejects( 234 | () => container.make(UserService), 235 | 'Cannot construct "[class UserService]" class. Container is not able to resolve its dependencies. Did you forget to use @inject() decorator?' 236 | ) 237 | }) 238 | 239 | test('work fine when runtime values satisfies dependencies', async ({ assert }) => { 240 | class UserService { 241 | constructor(public config: { foo: string }) {} 242 | } 243 | 244 | const container = new Container() 245 | const service = await container.make(UserService, [{ foo: 'bar' }]) 246 | assert.equal(service.config.foo, 'bar') 247 | }) 248 | 249 | test('work fine when constructor parameter has a default value', async ({ assert }) => { 250 | class UserService { 251 | constructor(public config: { foo: string } = { foo: 'baz' }) {} 252 | } 253 | 254 | const container = new Container() 255 | const service = await container.make(UserService) 256 | assert.equal(service.config.foo, 'baz') 257 | }) 258 | }) 259 | -------------------------------------------------------------------------------- /tests/container/make_class_via_inject.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import 'reflect-metadata' 11 | import { test } from '@japa/runner' 12 | import { EventEmitter } from 'node:events' 13 | import { expectTypeOf } from 'expect-type' 14 | import { Container } from '../../src/container.js' 15 | import { inject } from '../../src/decorators/inject.js' 16 | import type { BindingResolver } from '../../src/types.js' 17 | 18 | test.group('Container | Make class via inject', () => { 19 | test('inject constructor dependencies using @inject', async ({ assert }) => { 20 | class Database {} 21 | 22 | @inject() 23 | class UserService { 24 | constructor(public db: Database) {} 25 | } 26 | 27 | const container = new Container() 28 | const service = await container.make(UserService) 29 | 30 | expectTypeOf(service).toEqualTypeOf() 31 | assert.instanceOf(service, UserService) 32 | assert.instanceOf(service.db, Database) 33 | }) 34 | 35 | test('throw error when injecting primitive values', async ({ assert }) => { 36 | @inject() 37 | class UserService { 38 | args: any[] 39 | 40 | constructor(...args: any[]) { 41 | this.args = args 42 | } 43 | } 44 | 45 | const container = new Container() 46 | await assert.rejects( 47 | () => container.make(UserService), 48 | 'Cannot inject "[Function: Array]" in "[class UserService]"' 49 | ) 50 | }) 51 | 52 | test('construct nested dependencies', async ({ assert }) => { 53 | class Config {} 54 | 55 | @inject() 56 | class Encryption { 57 | constructor(public config: Config) {} 58 | } 59 | 60 | @inject() 61 | class UserService { 62 | constructor(public encryption: Encryption) {} 63 | } 64 | 65 | const container = new Container() 66 | const service = await container.make(UserService) 67 | 68 | assert.instanceOf(service, UserService) 69 | expectTypeOf(service).toEqualTypeOf() 70 | 71 | assert.instanceOf(service.encryption, Encryption) 72 | expectTypeOf(service.encryption).toEqualTypeOf() 73 | 74 | assert.instanceOf(service.encryption.config, Config) 75 | expectTypeOf(service.encryption.config).toEqualTypeOf() 76 | }) 77 | 78 | test('construct method dependencies', async ({ assert }) => { 79 | class Config {} 80 | 81 | @inject() 82 | class Encryption { 83 | constructor(public config: Config) {} 84 | } 85 | 86 | class UserService { 87 | @inject() 88 | find(encryption: Encryption) { 89 | return encryption 90 | } 91 | } 92 | 93 | const container = new Container() 94 | const encryption = await container.call(await container.make(UserService), 'find') 95 | 96 | assert.instanceOf(encryption, Encryption) 97 | expectTypeOf(encryption).toEqualTypeOf() 98 | 99 | assert.instanceOf(encryption.config, Config) 100 | expectTypeOf(encryption.config).toEqualTypeOf() 101 | }) 102 | 103 | test('inject constructor dependencies inside a sub-class', async ({ assert }) => { 104 | class Database {} 105 | 106 | @inject() 107 | class BaseService { 108 | constructor(public db: Database) {} 109 | } 110 | 111 | @inject() 112 | class UserService extends BaseService {} 113 | 114 | // @ts-expect-error 115 | assert.notStrictEqual(UserService.containerInjections, BaseService.containerInjections) 116 | 117 | const container = new Container() 118 | const service = await container.make(UserService) 119 | 120 | expectTypeOf(service).toEqualTypeOf() 121 | assert.instanceOf(service, UserService) 122 | assert.instanceOf(service, BaseService) 123 | assert.instanceOf(service.db, Database) 124 | }) 125 | 126 | test('inject sub-class constructor own dependencies', async ({ assert }) => { 127 | class Database {} 128 | class Emitter extends EventEmitter {} 129 | 130 | @inject() 131 | class BaseService { 132 | constructor(public db: Database) {} 133 | } 134 | 135 | @inject() 136 | class UserService extends BaseService { 137 | constructor( 138 | db: Database, 139 | public emitter: Emitter 140 | ) { 141 | super(db) 142 | } 143 | } 144 | 145 | const container = new Container() 146 | const service = await container.make(UserService) 147 | 148 | // @ts-expect-error 149 | assert.notStrictEqual(UserService.containerInjections, BaseService.containerInjections) 150 | 151 | expectTypeOf(service).toEqualTypeOf() 152 | assert.instanceOf(service, UserService) 153 | assert.instanceOf(service, BaseService) 154 | assert.instanceOf(service.db, Database) 155 | assert.instanceOf(service.emitter, Emitter) 156 | }) 157 | 158 | test('inject method dependencies inside a sub-class', async ({ assert }) => { 159 | class Database {} 160 | class Emitter extends EventEmitter {} 161 | 162 | class BaseService { 163 | @inject() 164 | foo(db: Database) { 165 | return db 166 | } 167 | } 168 | 169 | class UserService extends BaseService { 170 | @inject() 171 | bar(emitter: Emitter) { 172 | return emitter 173 | } 174 | } 175 | 176 | const container = new Container() 177 | const service = await container.make(UserService) 178 | const fooResult = await container.call(service, 'foo') 179 | const barResult = await container.call(service, 'bar') 180 | 181 | // @ts-expect-error 182 | assert.notStrictEqual(UserService.containerInjections, BaseService.containerInjections) 183 | 184 | expectTypeOf(service).toEqualTypeOf() 185 | expectTypeOf(fooResult).toEqualTypeOf() 186 | expectTypeOf(barResult).toEqualTypeOf() 187 | 188 | assert.instanceOf(fooResult, Database) 189 | assert.instanceOf(barResult, Emitter) 190 | }) 191 | 192 | test('raise exception when injecting primitive classes', async ({ assert }) => { 193 | @inject() 194 | class UserService { 195 | constructor(public db: string) {} 196 | } 197 | 198 | const container = new Container() 199 | await assert.rejects( 200 | () => container.make(UserService), 201 | 'Cannot inject "[Function: String]" in "[class UserService]"' 202 | ) 203 | }) 204 | 205 | test('raise exception when injecting a typescript type', async ({ assert }) => { 206 | type Db = {} 207 | 208 | @inject() 209 | class UserService { 210 | constructor(public db: Db) {} 211 | } 212 | 213 | const container = new Container() 214 | await assert.rejects( 215 | () => container.make(UserService), 216 | 'Cannot inject "[Function: Object]" in "[class UserService]"' 217 | ) 218 | }) 219 | 220 | test('raise exception when injecting a typescript interface', async ({ assert }) => { 221 | interface Db {} 222 | 223 | @inject() 224 | class UserService { 225 | constructor(public db: Db) {} 226 | } 227 | 228 | const container = new Container() 229 | await assert.rejects( 230 | () => container.make(UserService), 231 | 'Cannot inject "[Function: Object]" in "[class UserService]"' 232 | ) 233 | }) 234 | 235 | test('parallel calls to singleton injection should return the same value', async ({ assert }) => { 236 | class Encryption {} 237 | 238 | @inject() 239 | class UserService { 240 | constructor(public encryption: Encryption) {} 241 | } 242 | 243 | const container = new Container() 244 | container.singleton(Encryption, () => new Encryption()) 245 | 246 | const [service, service1] = await Promise.all([ 247 | container.make(UserService), 248 | container.make(UserService), 249 | ]) 250 | 251 | expectTypeOf(service).toEqualTypeOf() 252 | expectTypeOf(service1).toEqualTypeOf() 253 | assert.instanceOf(service, UserService) 254 | assert.instanceOf(service1, UserService) 255 | 256 | assert.notStrictEqual(service1, service) 257 | assert.strictEqual(service1.encryption, service.encryption) 258 | }) 259 | 260 | test('fail when singleton binding resolver fails', async ({ assert }) => { 261 | class Encryption {} 262 | 263 | @inject() 264 | class UserService { 265 | constructor(public encryption: Encryption) {} 266 | } 267 | 268 | const container = new Container() 269 | container.singleton(Encryption, () => { 270 | throw new Error('Cannot resolve') 271 | }) 272 | 273 | const results = await Promise.allSettled([ 274 | container.make(UserService), 275 | container.make(UserService), 276 | ]) 277 | 278 | assert.deepEqual( 279 | results.map((result) => result.status), 280 | ['rejected', 'rejected'] 281 | ) 282 | }) 283 | }) 284 | 285 | test.group('Container | Make class with contextual bindings', () => { 286 | test('resolve contextual bindings for a class constructor', async ({ assert }) => { 287 | const container = new Container() 288 | 289 | abstract class Hash { 290 | abstract make(value: string): string 291 | } 292 | 293 | @inject() 294 | class UsersController { 295 | constructor(public hash: Hash) {} 296 | } 297 | 298 | class Argon2 { 299 | make(value: string): string { 300 | return value.toUpperCase() 301 | } 302 | } 303 | 304 | const builder = container.when(UsersController).asksFor(Hash) 305 | builder.provide(() => { 306 | return new Argon2() 307 | }) 308 | 309 | expectTypeOf(builder.provide).parameters.toEqualTypeOf< 310 | [BindingResolver, Hash>] 311 | >() 312 | expectTypeOf(container.contextualBinding) 313 | .parameter(2) 314 | .toEqualTypeOf, Hash>>() 315 | 316 | const controller = await container.make(UsersController) 317 | expectTypeOf(controller).toEqualTypeOf() 318 | assert.instanceOf(controller.hash, Argon2) 319 | }) 320 | 321 | test('do not resolve contextual binding when parent is registered as a binding to the container', async ({ 322 | assert, 323 | }) => { 324 | const container = new Container() 325 | 326 | abstract class Hash { 327 | abstract make(value: string): string 328 | } 329 | 330 | class BaseHasher extends Hash { 331 | make(value: string): string { 332 | return value.toUpperCase() 333 | } 334 | } 335 | 336 | @inject() 337 | class UsersController { 338 | constructor(public hash: Hash) {} 339 | } 340 | 341 | class Argon2 { 342 | make(value: string): string { 343 | return value.toUpperCase() 344 | } 345 | } 346 | 347 | const builder = container.when(UsersController).asksFor(Hash) 348 | builder.provide(() => { 349 | return new Argon2() 350 | }) 351 | 352 | expectTypeOf(builder.provide).parameters.toEqualTypeOf< 353 | [BindingResolver, Hash>] 354 | >() 355 | expectTypeOf(container.contextualBinding) 356 | .parameter(2) 357 | .toEqualTypeOf, Hash>>() 358 | 359 | /** 360 | * As soon as a binding for the class is defined, the binding 361 | * callback will be source of truth. 362 | * 363 | * Contextual bindings are used when container performs constructor 364 | * building 365 | */ 366 | container.bind(UsersController, () => { 367 | return new UsersController(new BaseHasher()) 368 | }) 369 | 370 | const controller = await container.make(UsersController) 371 | expectTypeOf(controller).toEqualTypeOf() 372 | assert.instanceOf(controller.hash, BaseHasher) 373 | }) 374 | 375 | test('given preference to contextual binding when binding is also registered to the container', async ({ 376 | assert, 377 | }) => { 378 | const container = new Container() 379 | 380 | abstract class Hash { 381 | abstract make(value: string): string 382 | } 383 | 384 | @inject() 385 | class UsersController { 386 | constructor(public hash: Hash) {} 387 | } 388 | 389 | class Argon2 { 390 | make(value: string): string { 391 | return value.toUpperCase() 392 | } 393 | } 394 | 395 | class Bcrypt { 396 | make(value: string): string { 397 | return value.toUpperCase() 398 | } 399 | } 400 | 401 | const builder = container.when(UsersController).asksFor(Hash) 402 | builder.provide(() => { 403 | return new Argon2() 404 | }) 405 | 406 | expectTypeOf(builder.provide).parameters.toEqualTypeOf< 407 | [BindingResolver, Hash>] 408 | >() 409 | expectTypeOf(container.contextualBinding) 410 | .parameter(2) 411 | .toEqualTypeOf, Hash>>() 412 | 413 | /** 414 | * When the binding is registered in the container, we consider 415 | * it as the default value. 416 | * 417 | * Therefore, the contextual binding takes preference over it. 418 | */ 419 | container.bind(Hash, () => { 420 | return new Bcrypt() 421 | }) 422 | 423 | const controller = await container.make(UsersController) 424 | expectTypeOf(controller).toEqualTypeOf() 425 | assert.instanceOf(controller.hash, Argon2) 426 | }) 427 | 428 | test('re-use container to resolve the same binding', async ({ assert }) => { 429 | const container = new Container() 430 | 431 | abstract class Hash { 432 | abstract make(value: string): string 433 | } 434 | 435 | class BaseHasher extends Hash { 436 | make(value: string): string { 437 | return value.toUpperCase() 438 | } 439 | } 440 | 441 | @inject() 442 | class UsersController { 443 | constructor(public hash: Hash) {} 444 | } 445 | 446 | container.bind(Hash, () => new BaseHasher()) 447 | 448 | const builder = container.when(UsersController).asksFor(Hash) 449 | builder.provide((resolver) => { 450 | return resolver.make(Hash) 451 | }) 452 | 453 | expectTypeOf(builder.provide).parameters.toEqualTypeOf< 454 | [BindingResolver, Hash>] 455 | >() 456 | expectTypeOf(container.contextualBinding) 457 | .parameter(2) 458 | .toEqualTypeOf, Hash>>() 459 | 460 | const controller = await container.make(UsersController) 461 | expectTypeOf(controller).toEqualTypeOf() 462 | 463 | assert.instanceOf(controller.hash, BaseHasher) 464 | }) 465 | 466 | test('handle case when class has a contextual binding but not for the current binding', async ({ 467 | assert, 468 | }) => { 469 | const container = new Container() 470 | 471 | class Foo {} 472 | 473 | class Hash { 474 | make(value: string): string { 475 | return value.toUpperCase() 476 | } 477 | } 478 | 479 | @inject() 480 | class UsersController { 481 | constructor(public hash: Hash) {} 482 | } 483 | 484 | const builder = container.when(UsersController).asksFor(Foo) 485 | builder.provide(() => { 486 | return new Foo() 487 | }) 488 | 489 | expectTypeOf(builder.provide).parameters.toEqualTypeOf< 490 | [BindingResolver, Foo>] 491 | >() 492 | expectTypeOf(container.contextualBinding) 493 | .parameter(2) 494 | .toEqualTypeOf, Hash>>() 495 | 496 | const controller = await container.make(UsersController) 497 | expectTypeOf(controller).toEqualTypeOf() 498 | 499 | assert.instanceOf(controller.hash, Hash) 500 | }) 501 | 502 | test('raise error when unable to constructor class dependency', async ({ assert }) => { 503 | class UserService { 504 | constructor(public config: { foo: string }) {} 505 | } 506 | 507 | @inject() 508 | class UsersController { 509 | constructor(_: UserService) {} 510 | } 511 | 512 | const container = new Container<{ foo: 'bar' }>() 513 | await assert.rejects( 514 | () => container.make(UsersController), 515 | 'Cannot inject "[class UserService]" in "[class UsersController]"' 516 | ) 517 | }) 518 | }) 519 | -------------------------------------------------------------------------------- /tests/container/swap.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import 'reflect-metadata' 11 | import { test } from '@japa/runner' 12 | import { expectTypeOf } from 'expect-type' 13 | 14 | import { inject } from '../../index.js' 15 | import { Container } from '../../src/container.js' 16 | import type { BindingResolver } from '../../src/types.js' 17 | 18 | test.group('Container | swap', () => { 19 | test('swap a class implementation', async ({ assert }) => { 20 | class UserService { 21 | get() { 22 | return { 23 | id: 1, 24 | username: 'virk', 25 | } 26 | } 27 | } 28 | 29 | class UsersController { 30 | constructor() {} 31 | 32 | @inject() 33 | show(user: UserService) { 34 | return user.get() 35 | } 36 | } 37 | 38 | class FakedUserService extends UserService { 39 | get() { 40 | return { 41 | id: 1, 42 | username: 'faked-virk', 43 | } 44 | } 45 | } 46 | 47 | const container = new Container() 48 | container.swap(UserService, () => { 49 | return new FakedUserService() 50 | }) 51 | 52 | const controller = await container.make(UsersController) 53 | expectTypeOf(controller).toEqualTypeOf() 54 | 55 | const user = await container.call(controller, 'show') 56 | expectTypeOf(user).toEqualTypeOf<{ id: number; username: string }>() 57 | 58 | assert.deepEqual(user, { id: 1, username: 'faked-virk' }) 59 | }) 60 | 61 | test('restore class implementation', async ({ assert }) => { 62 | class UserService { 63 | get() { 64 | return { 65 | id: 1, 66 | username: 'virk', 67 | } 68 | } 69 | } 70 | 71 | class UsersController { 72 | constructor() {} 73 | 74 | @inject() 75 | show(user: UserService) { 76 | return user.get() 77 | } 78 | } 79 | 80 | class FakedUserService extends UserService { 81 | get() { 82 | return { 83 | id: 1, 84 | username: 'faked-virk', 85 | } 86 | } 87 | } 88 | 89 | const container = new Container() 90 | container.swap(UserService, () => { 91 | return new FakedUserService() 92 | }) 93 | 94 | const controller = await container.make(UsersController) 95 | expectTypeOf(controller).toEqualTypeOf() 96 | 97 | const user = await container.call(controller, 'show') 98 | expectTypeOf(user).toEqualTypeOf<{ id: number; username: string }>() 99 | assert.deepEqual(user, { id: 1, username: 'faked-virk' }) 100 | 101 | container.restore(UserService) 102 | 103 | const user1 = await container.call(controller, 'show') 104 | expectTypeOf(user1).toEqualTypeOf<{ id: number; username: string }>() 105 | assert.deepEqual(user1, { id: 1, username: 'virk' }) 106 | }) 107 | 108 | test('restore multiple implementations', async ({ assert }) => { 109 | class UserService { 110 | get() { 111 | return { 112 | id: 1, 113 | username: 'virk', 114 | } 115 | } 116 | } 117 | 118 | class UsersController { 119 | constructor() {} 120 | 121 | @inject() 122 | show(user: UserService) { 123 | return user.get() 124 | } 125 | } 126 | 127 | class FakedUserService extends UserService { 128 | get() { 129 | return { 130 | id: 1, 131 | username: 'faked-virk', 132 | } 133 | } 134 | } 135 | 136 | const container = new Container() 137 | container.swap(UserService, () => { 138 | return new FakedUserService() 139 | }) 140 | 141 | const controller = await container.make(UsersController) 142 | expectTypeOf(controller).toEqualTypeOf() 143 | 144 | const user = await container.call(controller, 'show') 145 | expectTypeOf(user).toEqualTypeOf<{ id: number; username: string }>() 146 | assert.deepEqual(user, { id: 1, username: 'faked-virk' }) 147 | 148 | container.restoreAll([UserService]) 149 | 150 | const user1 = await container.call(controller, 'show') 151 | expectTypeOf(user1).toEqualTypeOf<{ id: number; username: string }>() 152 | assert.deepEqual(user1, { id: 1, username: 'virk' }) 153 | }) 154 | 155 | test('restore all implementations', async ({ assert }) => { 156 | class UserService { 157 | get() { 158 | return { 159 | id: 1, 160 | username: 'virk', 161 | } 162 | } 163 | } 164 | 165 | class UsersController { 166 | constructor() {} 167 | 168 | @inject() 169 | show(user: UserService) { 170 | return user.get() 171 | } 172 | } 173 | 174 | class FakedUserService extends UserService { 175 | get() { 176 | return { 177 | id: 1, 178 | username: 'faked-virk', 179 | } 180 | } 181 | } 182 | 183 | const container = new Container() 184 | container.swap(UserService, () => { 185 | return new FakedUserService() 186 | }) 187 | 188 | const controller = await container.make(UsersController) 189 | expectTypeOf(controller).toEqualTypeOf() 190 | 191 | const user = await container.call(controller, 'show') 192 | expectTypeOf(user).toEqualTypeOf<{ id: number; username: string }>() 193 | assert.deepEqual(user, { id: 1, username: 'faked-virk' }) 194 | 195 | container.restoreAll() 196 | 197 | const user1 = await container.call(controller, 'show') 198 | expectTypeOf(user1).toEqualTypeOf<{ id: number; username: string }>() 199 | assert.deepEqual(user1, { id: 1, username: 'virk' }) 200 | }) 201 | 202 | test('disallow swap names other than class constructor', async ({ assert }) => { 203 | const container = new Container() 204 | 205 | assert.throws( 206 | // @ts-expect-error 207 | () => container.swap(1, () => {}), 208 | 'Cannot call swap on value "1". Only classes can be swapped' 209 | ) 210 | 211 | assert.throws( 212 | // @ts-expect-error 213 | () => container.swap([], () => {}), 214 | 'Cannot call swap on value "[]". Only classes can be swapped' 215 | ) 216 | 217 | assert.throws( 218 | // @ts-expect-error 219 | () => container.swap({}, () => {}), 220 | 'Cannot call swap on value "{}". Only classes can be swapped' 221 | ) 222 | }) 223 | 224 | test('use swap over contextual binding', async ({ assert }) => { 225 | const container = new Container() 226 | 227 | abstract class Hash { 228 | abstract make(value: string): string 229 | } 230 | 231 | class Argon2 { 232 | make(value: string): string { 233 | return value.toUpperCase() 234 | } 235 | } 236 | 237 | class FakedHash { 238 | make(_: string): string { 239 | return 'fake' 240 | } 241 | } 242 | 243 | @inject() 244 | class UsersController { 245 | constructor(public hash: Hash) {} 246 | } 247 | 248 | container.contextualBinding(UsersController, Hash, () => { 249 | return new Argon2() 250 | }) 251 | container.swap(Hash, () => { 252 | return new FakedHash() 253 | }) 254 | 255 | expectTypeOf(container.contextualBinding) 256 | .parameter(2) 257 | .toEqualTypeOf, Hash>>() 258 | 259 | const controller = await container.make(UsersController) 260 | expectTypeOf(controller).toEqualTypeOf() 261 | assert.instanceOf(controller.hash, FakedHash) 262 | assert.equal(controller.hash.make('foo'), 'fake') 263 | }) 264 | }) 265 | -------------------------------------------------------------------------------- /tests/enqueue.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { enqueue } from '../src/helpers.js' 12 | 13 | test.group('Enqueue', () => { 14 | test('parallel calls should invoke the underlying method once', async ({ assert }) => { 15 | const stack: string[] = [] 16 | const fn = enqueue(() => { 17 | stack.push('invoked') 18 | }) 19 | 20 | await Promise.all([fn(), fn(), fn()]) 21 | assert.deepEqual(stack, ['invoked']) 22 | }) 23 | 24 | test('get return value from the underlying method', async ({ assert }) => { 25 | const fn = enqueue(() => { 26 | return new Date().getTime() 27 | }) 28 | 29 | const times = await Promise.all([fn(), fn(), fn()]) 30 | assert.lengthOf(times, 3) 31 | assert.strictEqual(times[0].value, times[1].value) 32 | assert.strictEqual(times[1].value, times[2].value) 33 | }) 34 | 35 | test('get return value from the async underlying method', async ({ assert }) => { 36 | const fn = enqueue(async () => { 37 | return new Date().getTime() 38 | }) 39 | 40 | const times = await Promise.all([fn(), fn(), fn()]) 41 | assert.lengthOf(times, 3) 42 | assert.strictEqual(times[0].value, times[1].value) 43 | assert.strictEqual(times[1].value, times[2].value) 44 | }) 45 | 46 | test('get error from the underlying method', async ({ assert }) => { 47 | const fn = enqueue(() => { 48 | throw new Error('failed') 49 | }) 50 | 51 | await assert.rejects(() => Promise.all([fn(), fn(), fn()]), 'failed') 52 | }) 53 | 54 | test('get error from the underlying async method', async ({ assert }) => { 55 | const fn = enqueue(async () => { 56 | throw new Error('failed') 57 | }) 58 | 59 | await assert.rejects(() => Promise.all([fn(), fn(), fn()]), 'failed') 60 | }) 61 | 62 | test('get error for all underlying method calls', async ({ assert }) => { 63 | const fn = enqueue(() => { 64 | throw new Error('failed') 65 | }) 66 | 67 | const results = await Promise.allSettled([fn(), fn(), fn()]) 68 | assert.deepEqual( 69 | results.map((result) => result.status), 70 | ['rejected', 'rejected', 'rejected'] 71 | ) 72 | }) 73 | 74 | test('get error for all underlying async method calls', async ({ assert }) => { 75 | const fn = enqueue(async () => { 76 | throw new Error('failed') 77 | }) 78 | 79 | const results = await Promise.allSettled([fn(), fn(), fn()]) 80 | assert.deepEqual( 81 | results.map((result) => result.status), 82 | ['rejected', 'rejected', 'rejected'] 83 | ) 84 | }) 85 | 86 | test('cache value in multiple sequential calls', async ({ assert }) => { 87 | const fn = enqueue(async () => { 88 | return new Date() 89 | }) 90 | 91 | const date = await fn() 92 | const date1 = await fn() 93 | const date2 = await fn() 94 | assert.strictEqual(date.value, date1.value) 95 | assert.strictEqual(date1.value, date2.value) 96 | }) 97 | 98 | test('cache error in sequential calls', async ({ assert }) => { 99 | const fn = enqueue(async () => { 100 | throw new Error('failed') 101 | }) 102 | 103 | await assert.rejects(() => fn(), 'failed') 104 | await assert.rejects(() => fn(), 'failed') 105 | await assert.rejects(() => fn(), 'failed') 106 | await assert.rejects(() => fn(), 'failed') 107 | }) 108 | 109 | test('resolve queue promises in the order they are registered', async ({ assert }) => { 110 | const stack: string[] = [] 111 | const fn = enqueue(() => { 112 | stack.push('invoked') 113 | }) 114 | 115 | const firstWrapper = async () => { 116 | await fn() 117 | stack.push('first') 118 | } 119 | 120 | const secondWrapper = async () => { 121 | await fn() 122 | stack.push('second') 123 | } 124 | 125 | const thirdWrapper = async () => { 126 | await fn() 127 | stack.push('third') 128 | } 129 | 130 | await firstWrapper() 131 | await secondWrapper() 132 | await thirdWrapper() 133 | 134 | assert.deepEqual(stack, ['invoked', 'first', 'second', 'third']) 135 | }) 136 | 137 | test('resolve parallel queue promises in the order they are registered', async ({ assert }) => { 138 | const stack: string[] = [] 139 | const fn = enqueue(() => { 140 | stack.push('invoked') 141 | }) 142 | 143 | const firstWrapper = async () => { 144 | await fn() 145 | stack.push('first') 146 | } 147 | 148 | const secondWrapper = async () => { 149 | await fn() 150 | stack.push('second') 151 | } 152 | 153 | const thirdWrapper = async () => { 154 | await fn() 155 | stack.push('third') 156 | } 157 | 158 | await Promise.all([firstWrapper(), secondWrapper(), thirdWrapper()]) 159 | assert.deepEqual(stack, ['invoked', 'first', 'second', 'third']) 160 | }) 161 | 162 | test('resolve parallel queue promises with allSettled', async ({ assert }) => { 163 | const stack: string[] = [] 164 | const fn = enqueue(() => { 165 | stack.push('invoked') 166 | }) 167 | 168 | const firstWrapper = async () => { 169 | await fn() 170 | stack.push('first') 171 | } 172 | 173 | const secondWrapper = async () => { 174 | await fn() 175 | stack.push('second') 176 | } 177 | 178 | const thirdWrapper = async () => { 179 | await fn() 180 | stack.push('third') 181 | } 182 | 183 | await Promise.allSettled([firstWrapper(), secondWrapper(), thirdWrapper()]) 184 | assert.deepEqual(stack, ['invoked', 'first', 'second', 'third']) 185 | }) 186 | 187 | test('reject parallel queue promises in the order they are registered', async ({ assert }) => { 188 | const stack: string[] = [] 189 | const fn = enqueue(async () => { 190 | throw new Error('Failed') 191 | }) 192 | 193 | const firstWrapper = async () => { 194 | try { 195 | await fn() 196 | } catch { 197 | stack.push('handled by first') 198 | } 199 | } 200 | 201 | const secondWrapper = async () => { 202 | try { 203 | await fn() 204 | } catch { 205 | stack.push('handled by second') 206 | } 207 | } 208 | 209 | const thirdWrapper = async () => { 210 | try { 211 | await fn() 212 | } catch { 213 | stack.push('handled by third') 214 | } 215 | } 216 | 217 | await Promise.all([firstWrapper(), secondWrapper(), thirdWrapper()]) 218 | assert.deepEqual(stack, ['handled by first', 'handled by second', 'handled by third']) 219 | }) 220 | 221 | test('reject parallel queue promises in the order they are registered with allSettled', async ({ 222 | assert, 223 | }) => { 224 | const stack: string[] = [] 225 | const fn = enqueue(async () => { 226 | throw new Error('Failed') 227 | }) 228 | 229 | const firstWrapper = async () => { 230 | try { 231 | await fn() 232 | } catch (error) { 233 | stack.push('handled by first') 234 | throw error 235 | } 236 | } 237 | 238 | const secondWrapper = async () => { 239 | try { 240 | await fn() 241 | } catch (error) { 242 | stack.push('handled by second') 243 | throw error 244 | } 245 | } 246 | 247 | const thirdWrapper = async () => { 248 | try { 249 | await fn() 250 | } catch (error) { 251 | stack.push('handled by third') 252 | throw error 253 | } 254 | } 255 | 256 | await Promise.allSettled([firstWrapper(), secondWrapper(), thirdWrapper()]) 257 | assert.deepEqual(stack, ['handled by first', 'handled by second', 'handled by third']) 258 | }) 259 | }) 260 | -------------------------------------------------------------------------------- /tests/module_caller/to_callable.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | 12 | import { Container } from '../../src/container.js' 13 | import { moduleCaller } from '../../src/module_caller.js' 14 | import { ContainerResolver } from '../../src/resolver.js' 15 | 16 | test.group('moduleCaller | toCallable', () => { 17 | test('make callable from module caller', async ({ assert }) => { 18 | class HomeController {} 19 | assert.isFunction(moduleCaller(HomeController, 'handle').toCallable()) 20 | }) 21 | 22 | test('pass fixed container instance to the callable', async ({ assert }) => { 23 | class HomeController { 24 | handle(args: string[]) { 25 | args.push('invoked') 26 | } 27 | } 28 | 29 | const args: string[] = [] 30 | const container = new Container() 31 | 32 | const fn = moduleCaller(HomeController, 'handle').toCallable(container) 33 | await fn(args) 34 | assert.deepEqual(args, ['invoked']) 35 | }) 36 | 37 | test('pass runtime resolver to the callable', async ({ assert }) => { 38 | class HomeController { 39 | async handle(resolver: ContainerResolver) { 40 | const args = await resolver.make('args') 41 | args.push('invoked') 42 | } 43 | } 44 | 45 | const container = new Container() 46 | const resolver = container.createResolver() 47 | resolver.bindValue('args', []) 48 | 49 | const fn = moduleCaller(HomeController, 'handle').toCallable() 50 | 51 | await fn(resolver, resolver) 52 | assert.deepEqual(await resolver.make('args'), ['invoked']) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/module_caller/to_handle_method.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | 12 | import { Container } from '../../src/container.js' 13 | import { moduleCaller } from '../../src/module_caller.js' 14 | import { ContainerResolver } from '../../src/resolver.js' 15 | 16 | test.group('moduleCaller | toHandleMethod', () => { 17 | test('make handle method object from module caller', async ({ assert }) => { 18 | class HomeController {} 19 | assert.isFunction(moduleCaller(HomeController, 'handle').toHandleMethod().handle) 20 | }) 21 | 22 | test('pass fixed container instance to the handle method object', async ({ assert }) => { 23 | class HomeController { 24 | handle(args: string[]) { 25 | args.push('invoked') 26 | } 27 | } 28 | 29 | const args: string[] = [] 30 | const container = new Container() 31 | 32 | const handler = moduleCaller(HomeController, 'handle').toHandleMethod(container) 33 | await handler.handle(args) 34 | assert.deepEqual(args, ['invoked']) 35 | }) 36 | 37 | test('pass runtime resolver to the handle method object', async ({ assert }) => { 38 | class HomeController { 39 | async handle(resolver: ContainerResolver) { 40 | const args = await resolver.make('args') 41 | args.push('invoked') 42 | } 43 | } 44 | 45 | const container = new Container() 46 | const resolver = container.createResolver() 47 | resolver.bindValue('args', []) 48 | 49 | const handler = moduleCaller(HomeController, 'handle').toHandleMethod() 50 | 51 | await handler.handle(resolver, resolver) 52 | assert.deepEqual(await resolver.make('args'), ['invoked']) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/module_expression/parse.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { moduleExpression } from '../../src/module_expression.js' 12 | 13 | test.group('moduleExpression | parse', () => { 14 | test('parse module expression with no methods', async ({ assert }) => { 15 | assert.deepEqual(moduleExpression('#controllers/users_controller', import.meta.url).parse(), [ 16 | '#controllers/users_controller', 17 | 'handle', 18 | ]) 19 | }) 20 | 21 | test('parse module expression with method name', async ({ assert }) => { 22 | assert.deepEqual( 23 | moduleExpression('#controllers/users_controller.index', import.meta.url).parse(), 24 | ['#controllers/users_controller', 'index'] 25 | ) 26 | }) 27 | 28 | test('parse module expression with multiple dot separators inside it', async ({ assert }) => { 29 | assert.deepEqual( 30 | moduleExpression('#controllers/users.controller.index', import.meta.url).parse(), 31 | ['#controllers/users.controller', 'index'] 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/module_expression/to_callable.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { fileURLToPath } from 'node:url' 12 | import { Container } from '../../src/container.js' 13 | import { moduleExpression } from '../../src/module_expression.js' 14 | 15 | const BASE_URL = new URL('../app/', import.meta.url) 16 | const BASE_PATH = fileURLToPath(BASE_URL) 17 | 18 | test.group('moduleExpression | toCallable', (group) => { 19 | group.each.setup(({ context }) => { 20 | context.fs.baseUrl = BASE_URL 21 | context.fs.basePath = BASE_PATH 22 | }) 23 | 24 | test('make callable from module expression', async ({ assert }) => { 25 | assert.isFunction( 26 | moduleExpression('#controllers/auth/users_controller', import.meta.url).toCallable() 27 | ) 28 | }) 29 | 30 | test('pass fixed container instance to the callable', async ({ assert, fs }) => { 31 | await fs.create( 32 | 'controllers/auth/users_controller.ts', 33 | ` 34 | export default class UsersController { 35 | handle(args) { 36 | args.push('invoked') 37 | } 38 | } 39 | ` 40 | ) 41 | 42 | const args: string[] = [] 43 | const container = new Container() 44 | const fn = moduleExpression('#controllers/auth/users_controller', import.meta.url).toCallable( 45 | container 46 | ) 47 | 48 | await fn(args) 49 | assert.deepEqual(args, ['invoked']) 50 | }) 51 | 52 | test('pass runtime resolver to the callable', async ({ assert, fs }) => { 53 | await fs.create( 54 | 'controllers/auth/admin_controller.ts', 55 | ` 56 | export default class AdminController { 57 | async handle(resolver) { 58 | const args = await resolver.make('args') 59 | args.push('invoked') 60 | } 61 | } 62 | ` 63 | ) 64 | 65 | const container = new Container() 66 | const resolver = container.createResolver() 67 | resolver.bindValue('args', []) 68 | const fn = moduleExpression('#controllers/auth/admin_controller', import.meta.url).toCallable() 69 | 70 | await fn(resolver, resolver) 71 | assert.deepEqual(await resolver.make('args'), ['invoked']) 72 | }) 73 | 74 | test('raise exception when module is missing default export', async ({ assert, fs }) => { 75 | await fs.create( 76 | 'controllers/auth/posts_controller.ts', 77 | ` 78 | export class PostsController { 79 | handle(args) { 80 | args.push('invoked') 81 | } 82 | } 83 | ` 84 | ) 85 | 86 | const args: string[] = [] 87 | const container = new Container() 88 | const resolver = container.createResolver() 89 | const fn = moduleExpression('#controllers/auth/posts_controller', import.meta.url).toCallable() 90 | 91 | await assert.rejects( 92 | () => fn(resolver, args), 93 | 'Missing export default from "#controllers/auth/posts_controller" module' 94 | ) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /tests/module_expression/to_handle_method.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { fileURLToPath } from 'node:url' 12 | import { Container } from '../../src/container.js' 13 | import { moduleExpression } from '../../src/module_expression.js' 14 | 15 | const BASE_URL = new URL('../app/', import.meta.url) 16 | const BASE_PATH = fileURLToPath(BASE_URL) 17 | 18 | test.group('moduleExpression | toHandleMethod', (group) => { 19 | group.each.setup(({ context }) => { 20 | context.fs.baseUrl = BASE_URL 21 | context.fs.basePath = BASE_PATH 22 | }) 23 | 24 | test('make handle method object from module expression', async ({ assert }) => { 25 | assert.isFunction( 26 | moduleExpression('#controllers/users_controller', import.meta.url).toHandleMethod().handle 27 | ) 28 | }) 29 | 30 | test('pass fixed container instance to the handle method object', async ({ assert, fs }) => { 31 | await fs.create( 32 | 'controllers/users_controller.ts', 33 | ` 34 | export default class UsersController { 35 | handle(args) { 36 | args.push('invoked') 37 | } 38 | } 39 | ` 40 | ) 41 | 42 | const args: string[] = [] 43 | const container = new Container() 44 | const handler = moduleExpression( 45 | '#controllers/users_controller', 46 | import.meta.url 47 | ).toHandleMethod(container) 48 | 49 | await handler.handle(args) 50 | assert.deepEqual(args, ['invoked']) 51 | }) 52 | 53 | test('pass runtime resolver to the handle method object', async ({ assert, fs }) => { 54 | await fs.create( 55 | 'controllers/admin_controller.ts', 56 | ` 57 | export default class AdminController { 58 | async handle(resolver) { 59 | const args = await resolver.make('args') 60 | args.push('invoked') 61 | } 62 | } 63 | ` 64 | ) 65 | 66 | const container = new Container() 67 | const resolver = container.createResolver() 68 | resolver.bindValue('args', []) 69 | const handler = moduleExpression( 70 | '#controllers/admin_controller', 71 | import.meta.url 72 | ).toHandleMethod() 73 | 74 | await handler.handle(resolver, resolver) 75 | assert.deepEqual(await resolver.make('args'), ['invoked']) 76 | }) 77 | 78 | test('raise exception when module is missing default export', async ({ assert, fs }) => { 79 | await fs.create( 80 | 'controllers/posts_controller.ts', 81 | ` 82 | export class PostsController { 83 | handle(args) { 84 | args.push('invoked') 85 | } 86 | } 87 | ` 88 | ) 89 | 90 | const args: string[] = [] 91 | const container = new Container() 92 | const resolver = container.createResolver() 93 | const provider = moduleExpression( 94 | '#controllers/posts_controller', 95 | import.meta.url 96 | ).toHandleMethod() 97 | 98 | await assert.rejects( 99 | () => provider.handle(resolver, args), 100 | 'Missing export default from "#controllers/posts_controller" module' 101 | ) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /tests/module_importer/to_callable.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { fileURLToPath } from 'node:url' 12 | import { Container } from '../../src/container.js' 13 | import { moduleImporter } from '../../src/module_importer.js' 14 | 15 | const BASE_URL = new URL('../app/', import.meta.url) 16 | const BASE_PATH = fileURLToPath(BASE_URL) 17 | 18 | test.group('moduleImporter | toCallable', (group) => { 19 | group.each.setup(({ context }) => { 20 | context.fs.baseUrl = BASE_URL 21 | context.fs.basePath = BASE_PATH 22 | }) 23 | 24 | test('make callable from module importer', async ({ assert }) => { 25 | assert.isFunction( 26 | // @ts-expect-error 27 | moduleImporter(() => import('#middleware/auth'), 'handle').toCallable() 28 | ) 29 | }) 30 | 31 | test('pass fixed container instance to the callable', async ({ assert, fs }) => { 32 | await fs.create( 33 | 'middleware/silent_auth.ts', 34 | ` 35 | export default class SilentAuthMiddleware { 36 | handle(args) { 37 | args.push('invoked') 38 | } 39 | } 40 | ` 41 | ) 42 | 43 | const args: string[] = [] 44 | const container = new Container() 45 | 46 | // @ts-expect-error 47 | const fn = moduleImporter(() => import('#middleware/silent_auth'), 'handle').toCallable( 48 | container 49 | ) 50 | 51 | await fn(args) 52 | assert.deepEqual(args, ['invoked']) 53 | }) 54 | 55 | test('pass runtime resolver to the callable', async ({ assert, fs }) => { 56 | await fs.create( 57 | 'middleware/silent_auth_v1.ts', 58 | ` 59 | export default class SilentAuthMiddleware { 60 | async handle(resolver) { 61 | const args = await resolver.make('args') 62 | args.push('invoked') 63 | } 64 | } 65 | ` 66 | ) 67 | 68 | const container = new Container() 69 | const resolver = container.createResolver() 70 | resolver.bindValue('args', []) 71 | 72 | // @ts-expect-error 73 | const fn = moduleImporter(() => import('#middleware/silent_auth_v1'), 'handle').toCallable() 74 | 75 | await fn(resolver, resolver) 76 | assert.deepEqual(await resolver.make('args'), ['invoked']) 77 | }) 78 | 79 | test('raise exception when module is missing default export', async ({ assert, fs }) => { 80 | await fs.create( 81 | 'middleware/silent_auth_v2.ts', 82 | ` 83 | export class SilentAuthMiddleware { 84 | async handle(resolver) { 85 | const args = await resolver.make('args') 86 | args.push('invoked') 87 | } 88 | } 89 | ` 90 | ) 91 | 92 | const args: string[] = [] 93 | const container = new Container() 94 | 95 | // @ts-expect-error 96 | const fn = moduleImporter(() => import('#middleware/silent_auth_v2'), 'handle').toCallable( 97 | container 98 | ) 99 | 100 | await assert.rejects( 101 | () => fn(args), 102 | `Missing "export default" from lazy import "()=>import('#middleware/silent_auth_v2')"` 103 | ) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /tests/module_importer/to_handle_method.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { fileURLToPath } from 'node:url' 12 | import { Container } from '../../src/container.js' 13 | import { moduleImporter } from '../../src/module_importer.js' 14 | 15 | const BASE_URL = new URL('../app/', import.meta.url) 16 | const BASE_PATH = fileURLToPath(BASE_URL) 17 | 18 | test.group('moduleImporter | toHandleMethod', (group) => { 19 | group.each.setup(({ context }) => { 20 | context.fs.baseUrl = BASE_URL 21 | context.fs.basePath = BASE_PATH 22 | }) 23 | 24 | test('make handle method object from module importer', async ({ assert }) => { 25 | assert.isFunction( 26 | // @ts-expect-error 27 | moduleImporter(() => import('#middleware/auth'), 'handle').toHandleMethod().handle 28 | ) 29 | }) 30 | 31 | test('pass fixed container instance to the handle method object', async ({ assert, fs }) => { 32 | await fs.create( 33 | 'middleware/silent_auth_v3.ts', 34 | ` 35 | export default class SilentAuthMiddleware { 36 | handle(args) { 37 | args.push('invoked') 38 | } 39 | } 40 | ` 41 | ) 42 | 43 | const args: string[] = [] 44 | const container = new Container() 45 | 46 | const handler = moduleImporter( 47 | // @ts-expect-error 48 | () => import('#middleware/silent_auth_v3'), 49 | 'handle' 50 | ).toHandleMethod(container) 51 | 52 | await handler.handle(args) 53 | assert.deepEqual(args, ['invoked']) 54 | }) 55 | 56 | test('pass runtime resolver to the handle method object', async ({ assert, fs }) => { 57 | await fs.create( 58 | 'middleware/silent_auth_v4.ts', 59 | ` 60 | export default class SilentAuthMiddleware { 61 | async handle(resolver) { 62 | const args = await resolver.make('args') 63 | args.push('invoked') 64 | } 65 | } 66 | ` 67 | ) 68 | 69 | const container = new Container() 70 | const resolver = container.createResolver() 71 | resolver.bindValue('args', []) 72 | 73 | const handler = moduleImporter( 74 | // @ts-expect-error 75 | () => import('#middleware/silent_auth_v4'), 76 | 'handle' 77 | ).toHandleMethod() 78 | 79 | await handler.handle(resolver, resolver) 80 | assert.deepEqual(await resolver.make('args'), ['invoked']) 81 | }) 82 | 83 | test('raise exception when module is missing default export', async ({ assert, fs }) => { 84 | await fs.create( 85 | 'middleware/silent_auth_v5.ts', 86 | ` 87 | export class SilentAuthMiddleware { 88 | async handle(resolver) { 89 | const args = await resolver.make('args') 90 | args.push('invoked') 91 | } 92 | } 93 | ` 94 | ) 95 | 96 | const args: string[] = [] 97 | const container = new Container() 98 | 99 | const handler = moduleImporter( 100 | // @ts-expect-error 101 | () => import('#middleware/silent_auth_v5'), 102 | 'handle' 103 | ).toHandleMethod(container) 104 | 105 | await assert.rejects( 106 | () => handler.handle(args), 107 | `Missing "export default" from lazy import "()=>import('#middleware/silent_auth_v5')"` 108 | ) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /tests/provider.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { RuntimeException } from '@poppinss/utils/exception' 13 | 14 | import { Container } from '../src/container.js' 15 | import { containerProvider } from '../src/provider.js' 16 | 17 | test.group('Provider', () => { 18 | test('return empty array when class has no dependencies', async ({ assert }) => { 19 | class UserService {} 20 | const container = new Container() 21 | const resolver = container.createResolver() 22 | 23 | const dependencies = await containerProvider(UserService, 'constructor', resolver) 24 | 25 | assert.deepEqual(dependencies, []) 26 | expectTypeOf(dependencies).toEqualTypeOf() 27 | }) 28 | 29 | test('use runtime values when class has no dependencies', async ({ assert }) => { 30 | class UserService {} 31 | const container = new Container() 32 | const resolver = container.createResolver() 33 | 34 | const dependencies = await containerProvider(UserService, 'constructor', resolver, [ 35 | 'foo', 36 | { foo: 'bar' }, 37 | ]) 38 | 39 | assert.deepEqual(dependencies, ['foo', { foo: 'bar' }]) 40 | expectTypeOf(dependencies).toEqualTypeOf() 41 | }) 42 | 43 | test('make class dependencies using the resolver', async ({ assert }) => { 44 | class Database {} 45 | class UserService { 46 | static containerInjections = { 47 | constructor: { 48 | dependencies: [Database], 49 | createError: (message: string) => new RuntimeException(message), 50 | }, 51 | } 52 | } 53 | 54 | const container = new Container() 55 | const resolver = container.createResolver() 56 | 57 | const dependencies = await containerProvider(UserService, 'constructor', resolver) 58 | 59 | assert.deepEqual(dependencies, [new Database()]) 60 | expectTypeOf(dependencies).toEqualTypeOf() 61 | }) 62 | 63 | test('give priority to runtime values over defined dependencies', async ({ assert }) => { 64 | class Database {} 65 | class UserService { 66 | static containerInjections = { 67 | constructor: { 68 | dependencies: [Database], 69 | createError: (message: string) => new RuntimeException(message), 70 | }, 71 | } 72 | } 73 | 74 | const container = new Container() 75 | const resolver = container.createResolver() 76 | 77 | const dependencies = await containerProvider(UserService, 'constructor', resolver, [ 78 | { foo: 'bar' }, 79 | ]) 80 | 81 | assert.deepEqual(dependencies, [{ foo: 'bar' }]) 82 | expectTypeOf(dependencies).toEqualTypeOf() 83 | }) 84 | 85 | test('use all runtime values regardless of the dependencies length', async ({ assert }) => { 86 | class Database {} 87 | class UserService { 88 | static containerInjections = { 89 | constructor: { 90 | dependencies: [Database], 91 | createError: (message: string) => new RuntimeException(message), 92 | }, 93 | } 94 | } 95 | 96 | const container = new Container() 97 | const resolver = container.createResolver() 98 | 99 | const dependencies = await containerProvider(UserService, 'constructor', resolver, [ 100 | undefined, 101 | { foo: 'bar' }, 102 | ]) 103 | 104 | assert.deepEqual(dependencies, [new Database(), { foo: 'bar' }]) 105 | expectTypeOf(dependencies).toEqualTypeOf() 106 | }) 107 | 108 | test('dis-allow primitive constructors', async ({ assert }) => { 109 | class UserService { 110 | static containerInjections = { 111 | constructor: { 112 | dependencies: [String], 113 | createError: (message: string) => new RuntimeException(message), 114 | }, 115 | } 116 | } 117 | 118 | const container = new Container() 119 | const resolver = container.createResolver() 120 | 121 | await assert.rejects(async () => { 122 | await containerProvider(UserService, 'constructor', resolver, []) 123 | }, 'Cannot inject "[Function: String]" in "[class UserService]"') 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /tests/resolver.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/fold 3 | * 4 | * (c) AdonisJS 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { expectTypeOf } from 'expect-type' 12 | import { Container } from '../src/container.js' 13 | import { ContainerProvider } from '../src/types.js' 14 | 15 | test.group('Resolver', () => { 16 | test('give priority to resolver values over binding values', async ({ assert }) => { 17 | class UserService { 18 | name?: string 19 | } 20 | 21 | const container = new Container() 22 | const resolver = container.createResolver() 23 | 24 | const service = new UserService() 25 | service.name = 'container_service' 26 | 27 | const service1 = new UserService() 28 | service1.name = 'resolver_service' 29 | 30 | container.bindValue(UserService, service) 31 | resolver.bindValue(UserService, service1) 32 | 33 | const resolvedService = await resolver.make(UserService) 34 | 35 | expectTypeOf(resolvedService).toEqualTypeOf() 36 | assert.strictEqual(resolvedService, service1) 37 | assert.strictEqual(resolvedService.name, 'resolver_service') 38 | }) 39 | 40 | test('use static containerProvider to construct a class', async ({ assert }) => { 41 | assert.plan(4) 42 | 43 | class UserService { 44 | static containerProvider: ContainerProvider = ( 45 | binding, 46 | property, 47 | resolver, 48 | defaultProvider, 49 | runtimeValues 50 | ) => { 51 | assert.deepEqual(binding, UserService) 52 | assert.deepEqual(this, UserService) 53 | assert.equal(property, '_constructor') 54 | return defaultProvider(binding, property, resolver, runtimeValues) 55 | } 56 | name?: string 57 | } 58 | 59 | const container = new Container() 60 | const resolver = container.createResolver() 61 | 62 | const resolvedService = await resolver.make(UserService) 63 | 64 | expectTypeOf(resolvedService).toEqualTypeOf() 65 | assert.instanceOf(resolvedService, UserService) 66 | }) 67 | 68 | test('use static containerProvider to call a method', async ({ assert }) => { 69 | assert.plan(3) 70 | 71 | class UserService { 72 | static containerProvider: ContainerProvider = ( 73 | binding, 74 | property, 75 | resolver, 76 | defaultProvider, 77 | runtimeValues 78 | ) => { 79 | assert.deepEqual(binding, UserService) 80 | assert.deepEqual(this, UserService) 81 | assert.equal(property, 'store') 82 | return defaultProvider(binding, property, resolver, runtimeValues) 83 | } 84 | 85 | name?: string 86 | 87 | store() {} 88 | } 89 | 90 | const container = new Container() 91 | const resolver = container.createResolver() 92 | 93 | await resolver.call(new UserService(), 'store') 94 | }) 95 | 96 | test('disallow binding names other than string symbol or class constructor', async ({ 97 | assert, 98 | }) => { 99 | const container = new Container() 100 | const resolver = container.createResolver() 101 | 102 | assert.throws( 103 | // @ts-expect-error 104 | () => resolver.bindValue(1, 1), 105 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 106 | ) 107 | 108 | assert.throws( 109 | // @ts-expect-error 110 | () => resolver.bindValue([], 1), 111 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 112 | ) 113 | 114 | assert.throws( 115 | // @ts-expect-error 116 | () => resolver.bindValue({}, 1), 117 | 'The container binding key must be of type "string", "symbol", or a "class constructor"' 118 | ) 119 | }) 120 | 121 | test('find if a binding exists', async ({ assert }) => { 122 | const container = new Container() 123 | const resolver = container.createResolver() 124 | class Route {} 125 | 126 | const routeSymbol = Symbol('route') 127 | 128 | container.bind(Route, () => new Route()) 129 | resolver.bindValue('route', new Route()) 130 | container.bindValue(routeSymbol, new Route()) 131 | 132 | assert.isTrue(resolver.hasBinding(Route)) 133 | assert.isTrue(resolver.hasBinding('route')) 134 | assert.isTrue(resolver.hasBinding(routeSymbol)) 135 | assert.isFalse(resolver.hasBinding('db')) 136 | }) 137 | 138 | test('find if all bindings exists', async ({ assert }) => { 139 | const container = new Container() 140 | const resolver = container.createResolver() 141 | class Route {} 142 | 143 | const routeSymbol = Symbol('route') 144 | 145 | container.bind(Route, () => new Route()) 146 | resolver.bindValue('route', new Route()) 147 | container.bindValue(routeSymbol, new Route()) 148 | 149 | assert.isTrue(resolver.hasAllBindings([Route, 'route', routeSymbol])) 150 | assert.isFalse(resolver.hasAllBindings([Route, 'db', routeSymbol])) 151 | }) 152 | }) 153 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "types": ["@types/node", "reflect-metadata"] 9 | } 10 | } 11 | --------------------------------------------------------------------------------