├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── eslint.config.mjs ├── examples ├── babel │ ├── .babelrc │ ├── package.json │ └── src │ │ ├── index.js │ │ └── services │ │ ├── dependentService.js │ │ └── testService.js ├── koa │ ├── README.md │ ├── index.js │ ├── package.json │ ├── repositories │ │ └── messageRepository.js │ └── services │ │ └── MessageService.js ├── simple │ ├── index.js │ ├── package.json │ ├── repositories │ │ └── Stuffs.js │ └── services │ │ ├── ClassicalService.js │ │ └── functionalService.js └── typescript │ ├── package.json │ ├── src │ ├── index.ts │ └── services │ │ ├── DependentService.ts │ │ └── TestService.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── function-tokenizer.test.ts.snap │ ├── awilix.test.ts │ ├── container.disposing.test.ts │ ├── container.test.ts │ ├── fixture │ │ ├── index.js │ │ ├── repositories │ │ │ ├── answerRepository.js │ │ │ └── someUtil.js │ │ └── services │ │ │ ├── anotherService.js │ │ │ └── mainService.js │ ├── function-tokenizer.test.ts │ ├── inheritance.test.js │ ├── integration.test.ts │ ├── lifetime.test.ts │ ├── list-modules.test.ts │ ├── load-modules.test.ts │ ├── local-injections.test.ts │ ├── param-parser.bugs.test.ts │ ├── param-parser.test.ts │ ├── resolvers.test.ts │ ├── rollup.test.ts │ └── utils.test.ts ├── awilix.ts ├── container.ts ├── errors.ts ├── function-tokenizer.ts ├── injection-mode.ts ├── lifetime.ts ├── list-modules.ts ├── load-module-native.d.ts ├── load-module-native.js ├── load-modules.ts ├── param-parser.ts ├── resolvers.ts └── utils.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 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 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Name of the pipeline 2 | name: CI 3 | 4 | # When pushing to `master` or when there is a PR for the branch. 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | ci: 13 | name: Lint & Test (Node ${{ matrix.version }}) 14 | runs-on: ubuntu-22.04 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | version: 19 | - 16 20 | - 18 21 | - 20 22 | - 22 23 | - current 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Setup Node 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.version }} 32 | cache: 'npm' 33 | 34 | - name: Install Packages 35 | run: npm ci 36 | 37 | - name: Build 38 | run: npm run build 39 | 40 | - if: ${{ matrix.version == 'current' }} 41 | name: Lint 42 | run: npm run lint 43 | 44 | - name: Test 45 | run: npm run cover 46 | 47 | - if: ${{ matrix.version == 'current' }} 48 | name: Coveralls 49 | uses: coverallsapp/github-action@v2 50 | 51 | # Cancel running workflows for the same branch when a new one is started. 52 | concurrency: 53 | group: ci-${{ github.ref }} 54 | cancel-in-progress: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies and Node-related 2 | node_modules 3 | npm-debug.log 4 | examples/babel/package-lock.json 5 | 6 | # Code Coverage 7 | coverage 8 | 9 | # Build 10 | dist 11 | lib 12 | 13 | # IDE 14 | .vscode/ 15 | .idea/ 16 | 17 | # System 18 | .DS_Store 19 | .log 20 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/lint-staged && npm test 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | __tests__ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v12.0.5 2 | 3 | - Fix parameter parsing for classes by improving `constructor` scanning heuristics 4 | - Update packages 5 | 6 | # v12.0.4 7 | 8 | - Add `react-native` export key, move up `browser` key 9 | - Update packages 10 | 11 | # v12.0.3 12 | 13 | - Use default import for `fast-glob` 14 | 15 | # v12.0.2 16 | 17 | - Rename `awilix.browser.js` to `awilix.browser.mjs`, most tools prefer this 18 | - More `exports` conditions 19 | 20 | # v12.0.1 21 | 22 | - Fix browser build 23 | 24 | # v12.0.0 25 | 26 | - **BREAKING**: Bump TypeScript transpilation target for browser build from `ES5` to `ES2020` 27 | - Update packages 28 | - Add `exports` field to `package.json`, allowing you to explicitly import the browser build using `import { createContainer } from 'awilix/browser'` 29 | 30 | # v11.0.4 31 | 32 | - Undo all `exports` changes as it broke downstream consumers 33 | 34 | # v11.0.3 35 | 36 | - Add `lib` to exports lists 37 | 38 | # v11.0.2 39 | 40 | - Add `default` entry in every export 41 | 42 | # v11.0.1 43 | 44 | - Add "exports" field to package.json 45 | 46 | # v11.0.0 47 | 48 | - **BREAKING**: Drop Node 14 support, add Node 22 to build matrix. 49 | - Migrate to ESLint v9 50 | - Upgrade packages 51 | 52 | ### BREAKING CHANGES 53 | 54 | #### Drop Node 14 support 55 | 56 | Node 14 is no longer officially supported, as it is not an active LTS anymore. 57 | 58 | # v10.0.2 59 | 60 | - Add back `createBuildResolver` and `createDisposableResolver` exports ([#358](https://github.com/jeffijoe/awilix/pull/358)) 61 | 62 | # v10.0.1 63 | 64 | - Add back some type exports ([#351](https://github.com/jeffijoe/awilix/pull/351)) 65 | 66 | # v10.0.0 67 | 68 | - Add (optional, off by default) strict mode to enforce extra correctness checks in both resolution and registration ([#349](https://github.com/jeffijoe/awilix/pull/349) by [@fnimick](https://github.com/fnimick)) 69 | - Reduce the publicly accessible API surface to only that which is needed to use Awilix. This is 70 | potentially a breaking change if you were using any of the internal type definitions ([#349](https://github.com/jeffijoe/awilix/pull/349) by [@fnimick](https://github.com/fnimick)) 71 | 72 | # v9.0.0 73 | 74 | - Upgrade packages 75 | - Fix `aliasTo` to allow passing `symbol` ([#342](https://github.com/jeffijoe/awilix/pull/342) by [@zb-sj](https://github.com/zb-sj)) 76 | - Migrate from TSLint to ESLint 77 | - **BREAKING CHANGE**: Drop Node 12 support for real this time (updated the `engines` field) 78 | 79 | # v8.0.1 80 | 81 | - Upgrade packages 82 | - Move CI from Travis to GitHub Actions 83 | 84 | # v8.0.0 85 | 86 | - **BREAKING**: Drop Node 12 support, add Node 18 to build matrix 87 | - **BREAKING**: [#300](https://github.com/jeffijoe/awilix/issues/300) Rename `awilix.module.js` to `awilix.module.mjs` 88 | - [#293](https://github.com/jeffijoe/awilix/issues/293) Update packages, including `fast-glob` 89 | 90 | ### BREAKING CHANGES 91 | 92 | #### Drop Node 12 support 93 | 94 | Node 12 is no longer officially supported, as it is not an active LTS anymore. 95 | 96 | #### Rename `awilix.module.js` to `awilix.module.mjs` 97 | 98 | I am not sure if this is actually a breaking change, but if someone is importing 99 | the module directly using the file name, then it would be. 100 | 101 | # v7.0.3 102 | 103 | - Use `LoadedModuleDescriptor` in `NameFormatter` delegate parameter 104 | - Update packages 105 | 106 | # v7.0.2 107 | 108 | - [#291](https://github.com/jeffijoe/awilix/issues/291) Fix `listModules` regression on Windows 109 | - Update packages 110 | 111 | # v7.0.1 112 | 113 | - [#288](https://github.com/jeffijoe/awilix/issues/288) Don't use `Error.captureStackTrace` on unsupported platforms 114 | 115 | # v7.0.0 116 | 117 | - **BREAKING**: [#286](https://github.com/jeffijoe/awilix/issues/286) Support `Symbol.toStringTag`. This should fix [es5-shim/480](https://github.com/es-shims/es5-shim/issues/480). 118 | - Update packages 119 | 120 | ### BREAKING CHANGES 121 | 122 | #### Cradle JSON and `inspect` representation changed 123 | 124 | The Awilix Cradle's string representation when used by `util.inspect`, `.toJSON()` and others returned `[AwilixContainer.cradle]`. This has been 125 | changed to `[object AwilixContainerCradle]` to align with other common string representations in JavaScript. 126 | 127 | #### `Symbol.toStringTag` now implemented 128 | 129 | When using `Object.prototype.toString.call(container.cradle)`, it would return `[object Object]`. With this change, it now returns `[object AwilixContainerCradle]`. 130 | 131 | # v6.1.0 132 | 133 | - [#284](https://github.com/jeffijoe/awilix/pull/284) Use `fast-glob` instead of `glob` ([Sebastian Plaza](https://github.com/sebaplaza)) 134 | - Update packages 135 | 136 | # v6.0.0 137 | 138 | Please see the list of breaking changes below. 139 | 140 | - Update packages 141 | - **BREAKING**: [#198](https://github.com/jeffijoe/awilix/issues/198) Don't parse parameters from base class 142 | - [#270](https://github.com/jeffijoe/awilix/issues/270) Fix exponential performance slowdown for large containers 143 | - This was done by not relying on `rollUpRegistrations` in the `resolve` path. As a trade-off, performance 144 | for iterating the `cradle` proxy has degraded in order to guarantee accuracy. We consider this acceptable as iterating 145 | the `cradle` is not something one should be doing for anything besides debugging. Thanks to [@code-ape](https://github.com/code-ape) 146 | for the diagnosis and for coming up with a fix! 147 | 148 | ### BREAKING CHANGES 149 | 150 | - The `container.registrations` getter on a scoped container no longer rolls up registrations from its' parent. 151 | - In `CLASSIC` mode, when parsing the constructor of a derived class, Awilix will no longer parse the base class' constructor in 152 | case the derived class' defined constructor does not define any arguments. However, if the derived class does _not_ define a constructor, 153 | then Awilix will parse the base class' constructor. Please keep in mind that this only works for native classes, as Awilix works on the 154 | `toString` representation of the class/function in order to determine when a class with no defined constructor is encountered. 155 | - Renamed `container.has` to `container.hasRegistration` to avoid ambiguity. _Does it have a registration? Does it have a cached module? Who knows? Let's gooo!_ 156 | 157 | # v5.0.1 158 | 159 | - Improve internal `uniq` function performance by using a `Set` ([#253](https://github.com/jeffijoe/awilix/pull/253), [Anderson Leite](https://github.com/andersonleite)) 160 | - Update packages 161 | 162 | # v5.0.0 163 | 164 | - Improve resolve typing ([#247](https://github.com/jeffijoe/awilix/pull/247), [Goran Mržljak](https://github.com/mrzli)) 165 | - Update packages 166 | 167 | ### BREAKING CHANGES 168 | 169 | Dropped Node 10 support. Minimum supported Node version is now 12. 170 | 171 | # v4.3.4 172 | 173 | - Move rollup-plugin-copy to devDependencies ([#236](https://github.com/jeffijoe/awilix/pull/236), [Evan Sosenko](https://github.com/razor-x)) 174 | 175 | # v4.3.3 176 | 177 | - Update packages 178 | 179 | # v4.3.2 180 | 181 | - Convert paths to file URLs in `loadModules` with ESM, fixes [#225](https://github.com/jeffijoe/awilix/issues/225). ([#227](https://github.com/jeffijoe/awilix/pull/227), [Jamie Corkhill](https://github.com/JamieCorkhill)) 182 | 183 | # v4.3.1 184 | 185 | - `GlobWithOptions` now includes `BuildResolverOptions` instead of `ResolverOptions` (fixes [#214](https://github.com/jeffijoe/awilix/issues/214)) 186 | 187 | # v4.3.0 188 | 189 | - Add support for [Native Node ES modules](https://nodejs.org/api/esm.html) on Node v14+ ([#211](https://github.com/jeffijoe/awilix/pull/211), [Richard Simko](https://github.com/richardsimko)) 190 | 191 | # v4.2.7 192 | 193 | - Fixes AwilixResolutionError throwing TypeError if resolution stack contains symbols ([#205](https://github.com/jeffijoe/awilix/pull/205), [astephens25](https://github.com/astephens25)) 194 | - Update packages 195 | 196 | # v4.2.6 197 | 198 | - Fix return type for `createScope` when using a cradle typing. ([#182](https://github.com/jeffijoe/awilix/pull/182), [moltar](https://github.com/moltar)) 199 | - Remove `yarn.lock`, contributing docs now recommend `npm`. 200 | - Update packages, upgrade to Prettier 2.0 201 | 202 | # v4.2.5 203 | 204 | - Add optional generic parameter to container typing. Allows for a typed `ICradle`. ([#169](https://github.com/jeffijoe/awilix/pull/169), [roikoren755](https://github.com/roikoren755)) 205 | 206 | # v4.2.4 207 | 208 | - Fix issue with parsing comments ([#165](https://github.com/jeffijoe/awilix/pull/165), reported by [Jamie Corkhill](https://github.com/JamieCorkhill)) 209 | 210 | # v4.2.3 211 | 212 | - Fix issue where calling `JSON.stringify` on the cradle would result in memory leak ([#153](https://github.com/jeffijoe/awilix/pull/153), [berndartmueller](https://github.com/berndartmueller)) 213 | - Update packages 214 | 215 | # v4.2.2 216 | 217 | - Fix issue where the tokenizer was being too eager. ([#30](https://github.com/jeffijoe/awilix/issues/130)) 218 | - Make tests pass on Node v12 219 | - Update packages 220 | 221 | # v4.2.1 222 | 223 | - Fix `register` option in `loadModules` ([#124](https://github.com/jeffijoe/awilix/issues/124)) 224 | - Update packages 225 | 226 | # v4.2.0 227 | 228 | - Add `has` method to container to check for an existing registration ([#119](https://github.com/jeffijoe/awilix/pull/119), [faustbrian](https://github.com/faustbrian)) 229 | 230 | # v4.1.0 231 | 232 | - Extract dependencies from base class when no parameters were extracted. This works for ES6 classes as well as the old-school prototype approach to inheritance. Uses `Object.getPrototypeOf`. ([#107](https://github.com/jeffijoe/awilix/issues/107)) 233 | - Allow auto-loading of named exports that expose a `RESOLVER` symbol prop. ([#115](https://github.com/jeffijoe/awilix/pull/115)) 234 | 235 | # v4.0.1 236 | 237 | - Support returning the `cradle` in `async` functions ([#109](https://github.com/jeffijoe/awilix/issues/109), [andyfleming](https://github.com/andyfleming))) 238 | - Update packages 239 | 240 | # v4.0.0 241 | 242 | - **[BREAKING CHANGE]**: Scoped containers no longer use the parent's cache for `Lifetime.SCOPED` registrations [(#92)](https://github.com/jeffijoe/awilix/issues/92) 243 | - Change the `"react-native"` module path to use `awilix.browser.js` [(#104)](https://github.com/jeffijoe/awilix/issues/104) 244 | - Update packages 245 | 246 | Awilix v4 corrects a misunderstanding in how the scoped caching should work. For full context, please see [issue #92](https://github.com/jeffijoe/awilix/issues/92), but the TL:DR version is that prior `v4`, scoped registrations (`Lifetime.SCOPED` / `.scoped()`) would travel up the family tree and use a parent's cached resolution if there is any. If there is not, the resolving scope would cache it locally. 247 | 248 | While this was by design, it was not very useful, and it was designed based on a misunderstanding of how [Unity's `HierarchicalLifetimeManager` works](https://github.com/unitycontainer/unity/wiki/Unity-Lifetime-Managers#hierarchicallifetimemanager). In `v4`, `Lifetime.SCOPED` now works the same way: _the container performing the resolution also caches it, not looking outside itself for cached resolutions of `Lifetime.SCOPED` registrations_. 249 | 250 | A prime use case for this is having a scoped logger, as well as a root logger. This is actually what prompted this change. 251 | 252 | ```js 253 | // logger.js 254 | export class Logger { 255 | constructor(loggerPrefix = 'root') { 256 | this.prefix = loggerPrefix 257 | } 258 | 259 | log(message) { 260 | console.log(`[${this.prefix}]: ${message}`) 261 | } 262 | } 263 | ``` 264 | 265 | ```js 266 | // app.js 267 | import { Logger } from './logger' 268 | import { createContainer, asClass, InjectionMode } from 'awilix' 269 | 270 | const container = createContainer({ 271 | injectionMode: InjectionMode.CLASSIC, 272 | }).register({ 273 | logger: asClass(Logger).scoped(), 274 | }) 275 | 276 | const scope = container.createScope() 277 | scope.register({ 278 | loggerPrefix: asValue('dope scope'), 279 | }) 280 | 281 | const rootLogger = container.resolve('logger') 282 | const scopeLogger = scope.resolve('logger') 283 | 284 | rootLogger.log('yo!') // [root]: yo! 285 | scopeLogger.log('wassup!') // [dope scope]: wassup! 286 | ``` 287 | 288 | Prior to `v4`, the `scopeLogger` would have resolved to the same as `rootLogger` because it would ask it's ancestors if they had a `logger` cached. 289 | Now it works as you would probably expect it to: it keeps it's own cache. 290 | 291 | # v3.0.9 292 | 293 | - Updated packages. 294 | 295 | # v3.0.8 296 | 297 | - Add support for parsing async and generator functions; these no longer break the parser. ([#90](https://github.com/jeffijoe/awilix/issues/90)) 298 | - Update dependencies. 299 | 300 | # v3.0.7 301 | 302 | - Skip code comments in parser ([#87](https://github.com/jeffijoe/awilix/issues/87)) 303 | - Make the parser smarter by including full member expression paths so we get less false positives 304 | when scanning for the constructor token. 305 | 306 | # v3.0.6 307 | 308 | - Update `container.cradle` typing to be `any` ([#83](https://github.com/jeffijoe/awilix/issues/83), [Ackos95](https://github.com/Ackos95)) 309 | 310 | # v3.0.5 311 | 312 | - Updated dependencies 313 | - Fix TS 2.7 compilation issue 314 | - Fix the `GlobWithOptions` type to include `LifetimeType` 315 | 316 | # v3.0.4 317 | 318 | - Fix [#76](https://github.com/jeffijoe/awilix/issues/76): don't overwrite declarations when building with Rollup. 319 | 320 | # v3.0.3 321 | 322 | - Adjust Rollup config to use latest [config format](https://gist.github.com/Rich-Harris/d472c50732dab03efeb37472b08a3f32) 323 | 324 | # v3.0.2 325 | 326 | - Updated packages, fix an internal typing issue as a result of updated typings. 327 | 328 | # v3.0.1 329 | 330 | - Use `Reflect.construct()` instead of `new` internally; fixes TS transpilation issue. 331 | - Add note on browser support to README. 332 | 333 | # v3.0.0 334 | 335 | A lot of cool stuff has made it into version 3, and a few things were broken in 336 | the process. I have done my best to list everything here. 337 | 338 | ## ✨ New Features 339 | 340 | With v3 comes a few new cool features. 341 | 342 | ### Disposer support ([#48](https://github.com/jeffijoe/awilix/issues/48)) 343 | 344 | This has been a very requested feature. The idea is you can tell Awilix how to 345 | dispose of a dependency—for example, to close a database connection—when calling 346 | `container.dispose()`. 347 | 348 | ```js 349 | const pg = require('pg') 350 | const { createContainer, asFunction } = require('awilix') 351 | const container = createContainer() 352 | .register({ 353 | pool: ( 354 | asFunction(() => new pg.Pool({ ... })) 355 | .singleton() 356 | .disposer((pool) => pool.end()) 357 | ) 358 | }) 359 | 360 | // .. later, but only if a `pool` was ever created 361 | container.dispose().then(() => { 362 | console.log('One disposable connection.. disposed! Huehehehe') 363 | }) 364 | ``` 365 | 366 | ### `alias` resolver ([#55](https://github.com/jeffijoe/awilix/issues/55)) 367 | 368 | This new resolver lets you alias a registration. This is best illustrated with 369 | an example: 370 | 371 | ```js 372 | const { alias, asValue, createContainer } = require('awilix') 373 | 374 | const container = createContainer() 375 | 376 | container.register({ 377 | laughingOutLoud: asValue('hahahahaha'), 378 | lol: alias('laughingOutLoud'), 379 | }) 380 | 381 | container.resolve('lol') // 'hahahahaha' 382 | ``` 383 | 384 | It's essentially the exact same as calling 385 | `container.resolve('laughingOutLoad')`, but `lol` might be easier to type out in 386 | your constructors. 😎 387 | 388 | ### Default values in constructors/functions ([#46](https://github.com/jeffijoe/awilix/issues/46)) 389 | 390 | This is a pretty _small_ feature but was the most difficult to land, mainly 391 | because I had to write a smarter 392 | [parser](https://github.com/jeffijoe/awilix/tree/master/src/param-parser.ts) and 393 | [tokenizer](https://github.com/jeffijoe/awilix/tree/master/src/function-tokenizer.ts), 394 | not to mention they are now way better at skipping over code. Check out 395 | [the tests](https://github.com/jeffijoe/awilix/tree/master/src/__tests__/param-parser.test.ts#L149), 396 | it's pretty wild. 397 | 398 | ```js 399 | class MyClass { 400 | constructor(db, timeout = 1000) { /*...*/ } 401 | } 402 | 403 | container.register({ 404 | db: asFunction(..) 405 | }) 406 | 407 | // Look! No errors!! :D 408 | container.build(MyClass) instanceof MyClass // true 409 | ``` 410 | 411 | ### Official support for running in the browser ([#69](https://github.com/jeffijoe/awilix/issues/69)) 412 | 413 | Awilix now ships with 4 module flavors: CommonJS (same old), ES Modules for 414 | Node, ES Modules for the Browser and UMD. 415 | 416 | Please see the 417 | [Universal Module](https://github.com/jeffijoe/awilix#universal-module-browser-support) 418 | section in the readme for details. 419 | 420 | ## 🚨 Known breaking changes 421 | 422 | The following is a list of known breaking changes. If there's any I've missed 423 | feel free to let me know. 424 | 425 | ### The entire library is now written in TypeScript! ([#49](https://github.com/jeffijoe/awilix/issues/49)) 426 | 427 | This means a bunch of interfaces have been renamed and made more correct. If 428 | you're a TypeScript user, this is great news for you. 😄 429 | 430 | ### `ResolutionMode` is now `InjectionMode` ([#57](https://github.com/jeffijoe/awilix/issues/57)) 431 | 432 | - `ResolutionMode.js` renamed to `injection-mode.ts` 433 | - `ResolutionMode` renamed to `InjectionMode` 434 | 435 | ### "Registrations" are now "Resolvers" ([#51](https://github.com/jeffijoe/awilix/issues/51)) 436 | 437 | The terminology is now "you _register_ a **resolver** to a **name**". 438 | 439 | - TypeScript interfaces renamed 440 | - `REGISTRATION` symbol renamed to `RESOLVER` 441 | - `registrations.js` renamed to `resolvers.ts` 442 | - `registrationOptions` in `loadModules` renamed to `resolverOptions` 443 | 444 | ### `registerClass`, `registerFunction` and `registerValue` removed ([#60](https://github.com/jeffijoe/awilix/issues/60)) 445 | 446 | This was done to simplify the API surface, and also simplifies the 447 | implementation greatly (less overloads). You should be using 448 | `container.register` with `asClass`, `asFunction` and `asValue` instead. 449 | 450 | ### Resolver configuration chaining API is now immutable ([#62](https://github.com/jeffijoe/awilix/issues/62)) 451 | 452 | This simplifies the TypeScript types and is also considered a good practice. All 453 | configuration functions rely on `this`, meaning you **should not do**: 454 | 455 | ```js 456 | // I don't know why you would, but DONT DO THIS! 457 | const singleton = asClass(MyClass).singleton 458 | singleton() 459 | ``` 460 | 461 | However, this also means you can now "split" a resolver to configure it 462 | differently. For example: 463 | 464 | ```js 465 | class GenericSender { 466 | constructor(transport) { 467 | this.transport = transport 468 | } 469 | 470 | send() { 471 | if (this.transport === 'email') { 472 | // ... etc 473 | } 474 | } 475 | 476 | dispose() { /*...*/ } 477 | } 478 | 479 | const base = asClass(GenericSender).scoped().disposer((g) => g.dispose()) 480 | const emailSender = base.inject(() => ({ transport: 'email' }) 481 | const pushSender = base.inject(() => ({ transport: 'push' }) 482 | 483 | container.register({ 484 | emailSender, 485 | pushSender 486 | }) 487 | ``` 488 | 489 | ### Removed `AwilixNotAFunctionError` in favor of a generic `AwilixTypeError` ([#52](https://github.com/jeffijoe/awilix/issues/52)) 490 | 491 | This _should_ not have an impact on userland code but I thought I'd mention it. 492 | 493 | There are a bunch of internal uses of this error, so I thought it made sense to 494 | consolidate them into one error type. 495 | 496 | ## 👀 Other cool changes 497 | 498 | - Code is now formatted with Prettier 499 | - Awilix is now using `husky` + `lint-staged` to lint, format and test every 500 | commit to ensure top code quality. 501 | - Switched to Jest from Mocha 502 | - Switched from eslint to tslint 503 | - Rewrote the function parameter parser, it is now much better at correctly 504 | skipping over default value expressions to reach the next parameter. 505 | - Most (if not all) of the code is now documented and should be readable. 506 | 507 | --- 508 | 509 | # 2.12.0 510 | 511 | - Deprecated the `registerFunction`, `registerValue` and `registerClass` 512 | shortcuts. 513 | 514 | # 2.11.1 515 | 516 | - Fix typings for `container.build` 517 | 518 | # 2.11.0 519 | 520 | - Add support for `container.build()` - see 521 | [relevant docs](https://github.com/jeffijoe/awilix#containerbuild) 522 | 523 | # 2.10.0 524 | 525 | - Add support for `Object.keys()` on the cradle; now returns the names of 526 | available modules that can be resolved by accessing them. 527 | - There's a gotcha though; `Object.getOwnPropertyDescriptor()` will return a 528 | gibberish descriptor. This is required for the keys to show up in the 529 | result. 530 | - Fix iterating over cradle - generator now yields registration names, thanks 531 | [@neerfri](https://github.com/neerfri)! 532 | ([#40](https://github.com/jeffijoe/awilix/issues/40)) 533 | 534 | # 2.9.0 535 | 536 | - Fix issue with `console.log` on the cradle throwing an error. 537 | ([#7](https://github.com/jeffijoe/awilix/issues/7)) 538 | - This _should_ not break anything, but just to be safe I did a minor version 539 | bump. 540 | - Add support for `Symbol`s (although not recommended). 541 | 542 | # 2.8.4 543 | 544 | - Change `RegistrationOptions` typing to union of string and options 545 | 546 | # 2.8.3 547 | 548 | - Fix typing for `REGISTRATION` symbol 549 | 550 | # 2.8.2 551 | 552 | - Fix typing for `loadModules` — it didn't allow the shortcut version of 553 | `['glob.js', Lifetime.SCOPED]` 554 | - Add Prettier formatting as well as `lint-staged` to keep the tests passing and 555 | the code fresh before committing. 556 | 557 | # 2.8.1 558 | 559 | - Remove `is-plain-object` and `is-string`, use simple checks instead. Trying to 560 | keep the dependencies as thin as possible. 561 | 562 | # 2.8.0 563 | 564 | - **[NEW]**: Support inline registration options 565 | ([#34](https://github.com/jeffijoe/awilix/issues/34)) 566 | 567 | # 2.7.1 568 | 569 | - **[FIXED]**: `container.loadModules()` typing fix, thanks 570 | [@dboune](https://github.com/dboune)! 571 | 572 | # 2.7.0 573 | 574 | - **[BREAKING]**: Custom `isClass` function that will treat 575 | `function Capital () {}` as a class due to the capital first letter of the 576 | function name. This is to improve compatibility with Babel's ES5 code 577 | generator, and is also a pretty commonly accepted standard naming convention. 578 | ([#28](https://github.com/jeffijoe/awilix/issues/28)) 579 | - **[NEW]**: Added support for passing in a `register` function to 580 | `loadModules`. ([#28](https://github.com/jeffijoe/awilix/issues/28)) 581 | 582 | # 2.6.2 583 | 584 | - **[FIXED]**: Parsing regression in 2.6.1 585 | ([#30](https://github.com/jeffijoe/awilix/issues/30)) 586 | 587 | # 2.6.1 588 | 589 | - **[FIXED]**: Implemented a crude arguments parser to replace regex. 590 | ([#30](https://github.com/jeffijoe/awilix/issues/30)) 591 | 592 | # 2.6.0 593 | 594 | - **[NEW]**: infer function name for `registerClass`/`registerFunction` 595 | ([#26](https://github.com/jeffijoe/awilix/issues/26)) 596 | - **[FIXED]**: Corrected some TypeScript typings related to `registerClass` and 597 | `registerFunction`. 598 | 599 | # 2.5.0 600 | 601 | - **[NEW]**: Implemented per-module locals injection 602 | ([#24](https://github.com/jeffijoe/awilix/issues/24)). 603 | - Fixed issue where passing a `Lifetime` like 604 | `.registerFunction('name', func, Lifetime.SCOPED)` didn't work. 605 | - Documented `asClass`, `asValue` and `asFunction`. 606 | - **[FIXED]**: nasty options leaking when using 607 | `registerClass/Function({ test1: [Test1, { }], test2: [Test2, { }] })`. 608 | 609 | # 2.4.0 610 | 611 | - **[BREAKING]**: Guard assertions added to `asFunction` and `asClass`. This 612 | will prevent some nasty runtime behaviors. 613 | ([#20](https://github.com/jeffijoe/awilix/issues/20)), thanks 614 | [@zer0tonin](https://github.com/zer0tonin)! 615 | 616 | # 2.3.0 617 | 618 | - **[NEW]**: Classic dependency resolution mode using parameter name matching 619 | implemented, thanks to 620 | [@cjhoward92](https://github.com/jeffijoe/awilix/pull/21)! This is an 621 | alternative to the default proxy mechanism. 622 | - **[BREAKING]**: The `registerX({ name: [value, options]})` pattern is not 623 | needed for `registerValue` because it is so simple is requires no 624 | configuration. It was causing trouble when attempting to register an array as 625 | a value, because the `register` function would think it was the 626 | value-options-array pattern when it shouldn't be. **This change is breaking in 627 | the sense that it solves the unexpected behavior, but it breaks existing 628 | registrations that would register arrays by using 629 | `registerValue({ name: [[1, 2]] })` (multi-dimensional array to work around 630 | the pre-2.3.0 behavior)** 631 | - [chore]: Updated packages. 632 | 633 | # 2.2.6 634 | 635 | - Pass in the module descriptor to `formatName` - thanks @anasinnyk! 636 | - Fixed some issues with TypeScript definitions. 637 | 638 | # 2.2.5 639 | 640 | - Fixed `registerFunction` return type definition - thanks @ycros! 641 | 642 | # 2.2.4 643 | 644 | - TypeScript definitions - thanks @blove! 645 | 646 | # 2.2.3 647 | 648 | - Webpack 2 compatibility - thanks @ewrogers! 649 | 650 | # 2.2.2 651 | 652 | - `console.log`ing the container will, instead of throwing an error, display a 653 | string summary of the container. Fixes #7. 654 | - started logging changes to a changelog (sorry about being so late to the 655 | party) 656 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Clone repo, run `npm install` to install all dependencies, run `npm run build` to create an initial build, and then 4 | `npm run test -- --watchAll` to start writing code. 5 | 6 | For code coverage, run `npm run cover`. 7 | 8 | If you submit a PR, please aim for 100% code coverage and no linting errors. 9 | Travis will fail if there are linting errors. Thank you for considering 10 | contributing. :) 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Jeff Hansen 2016 to present. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import ts from 'typescript-eslint' 3 | 4 | export default [ 5 | js.configs.recommended, 6 | ...ts.configs.recommended, 7 | { 8 | languageOptions: { 9 | ecmaVersion: 5, 10 | sourceType: 'script', 11 | 12 | parserOptions: { 13 | project: true, 14 | tsconfigRootDir: './', 15 | }, 16 | }, 17 | 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | }, 21 | }, 22 | { 23 | files: ['**/__tests__/*.test.ts'], 24 | 25 | rules: { 26 | '@typescript-eslint/no-unused-vars': 'off', 27 | '@typescript-eslint/no-require-imports': 'off', 28 | }, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /examples/babel/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node dist/index.js", 8 | "build": "babel src -d dist" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "babel-cli": "^6.24.1", 14 | "babel-preset-es2015": "^6.24.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/babel/src/index.js: -------------------------------------------------------------------------------- 1 | import { asClass, createContainer, InjectionMode } from '../../../' 2 | import { TestService } from './services/testService' 3 | import { DependentService } from './services/dependentService' 4 | 5 | const container = createContainer({ 6 | injectionMode: InjectionMode.CLASSIC, 7 | strict: true, 8 | }) 9 | 10 | container.register({ 11 | testService: asClass(TestService), 12 | dep: asClass(DependentService), 13 | }) 14 | 15 | const depService = container.cradle.dep 16 | console.log(depService.getInnerData()) 17 | -------------------------------------------------------------------------------- /examples/babel/src/services/dependentService.js: -------------------------------------------------------------------------------- 1 | export class DependentService { 2 | constructor(testService) { 3 | this.testService = testService 4 | } 5 | 6 | getInnerData() { 7 | return this.testService.getData() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/babel/src/services/testService.js: -------------------------------------------------------------------------------- 1 | export class TestService { 2 | constructor() { 3 | this.data = 'Hello world!' 4 | } 5 | 6 | getData() { 7 | return this.data 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/koa/README.md: -------------------------------------------------------------------------------- 1 | # Awilix Koa example 2 | 3 | In this example we learn how scopes can simplify passing request state to our framework-independent services. 4 | 5 | * `/repositories` contains a repository that is written using the factory pattern. Note how it depends on a connection string and that it is only ever constructed **once!** 6 | * `/services` contains a service that is resolved for each request. Note how it depends on a `currentUser` that is set for each request. 7 | * `index.js` creates the Koa app and the container. 8 | 9 | To start: 10 | 11 | ``` 12 | npm install 13 | npm start 14 | ``` 15 | 16 | Go to `http://localhost:4321/messages?userId=1` to see it in action - pay attention to your terminal! 17 | -------------------------------------------------------------------------------- /examples/koa/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const KoaRouter = require('koa-router') 3 | 4 | // Importing Awilix relatively here, use `awilix` in your 5 | // own setup. 6 | const awilix = require('../..') 7 | 8 | // Destructuring to make it nicer. 9 | const { createContainer, asValue, asFunction, asClass } = awilix 10 | 11 | // Create a Koa app. 12 | const app = new Koa() 13 | const router = new KoaRouter() 14 | 15 | // Create a container. 16 | const container = createContainer({ strict: true }) 17 | 18 | // Register useful stuff 19 | const MessageService = require('./services/MessageService') 20 | const makeMessageRepository = require('./repositories/messageRepository') 21 | container.register({ 22 | // used by the repository; registered. 23 | DB_CONNECTION_STRING: asValue('localhost:1234', { 24 | lifetime: awilix.Lifetime.SINGLETON, 25 | }), 26 | // resolved for each request. 27 | messageService: asClass(MessageService).scoped(), 28 | // only resolved once 29 | messageRepository: asFunction(makeMessageRepository).singleton(), 30 | }) 31 | 32 | // For each request we want a custom scope. 33 | app.use((ctx, next) => { 34 | console.log('Registering scoped stuff') 35 | ctx.scope = container.createScope() 36 | // based on the query string, let's make a user.. 37 | ctx.scope.register({ 38 | // This is where you'd use something like Passport, 39 | // and retrieve the req.user or something. 40 | currentUser: asValue({ 41 | id: ctx.request.query.userId, 42 | }), 43 | }) 44 | 45 | return next() 46 | }) 47 | 48 | // Register a route.. 49 | router.get('/messages', (ctx) => { 50 | // Use the scope to resolve the message service. 51 | const messageService = ctx.scope.resolve('messageService') 52 | return messageService.findMessages().then((messages) => { 53 | ctx.body = messages 54 | ctx.status = 200 55 | }) 56 | }) 57 | 58 | // use the routes. 59 | app.use(router.routes()) 60 | app.use(router.allowedMethods()) 61 | 62 | const PORT = 4321 63 | app.listen(PORT, () => { 64 | console.log('Awilix Example running on port', PORT) 65 | }) 66 | -------------------------------------------------------------------------------- /examples/koa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "koa": "^2.0.0", 14 | "koa-router": "^7.0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/koa/repositories/messageRepository.js: -------------------------------------------------------------------------------- 1 | // this is just a dummy.. 2 | const messages = { 3 | 1: [ 4 | { 5 | message: 'hello', 6 | }, 7 | { 8 | message: 'world', 9 | }, 10 | ], 11 | 12 | 2: [ 13 | { 14 | message: 'damn son', 15 | }, 16 | ], 17 | } 18 | 19 | /** 20 | * We're using a factory function this time. 21 | */ 22 | module.exports = function makeMessageRepository({ DB_CONNECTION_STRING }) { 23 | // Imagine using the connection string for something useful.. 24 | console.log( 25 | 'Message repository constructed with connection string', 26 | DB_CONNECTION_STRING, 27 | ) 28 | 29 | function findMessagesForUser(userId) { 30 | return Promise.resolve(messages[userId]) 31 | } 32 | 33 | return { 34 | findMessagesForUser, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/koa/services/MessageService.js: -------------------------------------------------------------------------------- 1 | module.exports = class MessageService { 2 | constructor({ currentUser, messageRepository }) { 3 | console.log('creating message service, user ID: ', currentUser.id) 4 | this.currentUser = currentUser 5 | this.messages = messageRepository 6 | } 7 | 8 | findMessages() { 9 | return this.messages.findMessagesForUser(this.currentUser.id) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | // Import Awilix 2 | const awilix = require('../..') 3 | 4 | // Create a container. 5 | const container = awilix.createContainer({ 6 | strict: true, 7 | }) 8 | 9 | // Register some value.. We depend on this in `Stuffs.js` 10 | container.register('database', awilix.asValue('stuffs_db')) 11 | 12 | // Auto-load our services and our repositories. 13 | const opts = { 14 | // We want ClassicalService to be registered as classicalService. 15 | formatName: 'camelCase', 16 | cwd: __dirname, 17 | } 18 | container.loadModules( 19 | [ 20 | // Glob patterns 21 | 'services/*.js', 22 | 'repositories/*.js', 23 | ], 24 | opts, 25 | ) 26 | 27 | // 2 ways to resolve the same service. 28 | const classicalServiceFromCradle = container.cradle.classicalService 29 | const classicalService = container.resolve('classicalService') 30 | 31 | console.log( 32 | 'Resolved to the same type:', 33 | classicalService.constructor === classicalServiceFromCradle.constructor, 34 | ) 35 | 36 | // This will return false because the default is to return a new instance 37 | // when resolving. 38 | console.log( 39 | 'Resolved to the same instance:', 40 | classicalService === classicalServiceFromCradle, 41 | ) 42 | 43 | // Let's do something! 44 | classicalService.actAllCool().then((r) => { 45 | console.log('Result from classical service:', r) 46 | }) 47 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awilix-example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Jeff Hansen ", 10 | "license": "MIT", 11 | "dependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/simple/repositories/Stuffs.js: -------------------------------------------------------------------------------- 1 | function StuffRepository({ database }) { 2 | console.log('StuffRepository created with database', database) 3 | } 4 | 5 | StuffRepository.prototype.getStuff = function (someArg) { 6 | return Promise.resolve({ 7 | someProperty: someArg, 8 | secret: 'sshhhh!!!!!!!!!!', 9 | }) 10 | } 11 | 12 | module.exports = (opts) => new StuffRepository(opts) 13 | -------------------------------------------------------------------------------- /examples/simple/services/ClassicalService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example of a "classical" service, 3 | * meaning that the service is actually a class 4 | * that takes it's dependencies in the constructor. 5 | */ 6 | 7 | // Use classes, prototypes, whatever you want. 8 | // Using class here for demo purposes. Java and .NET devs will 9 | // feel right at home. 10 | class ClassicalService { 11 | // Classic example of constructor injection. 12 | constructor(opts) { 13 | this.functionalService = opts.functionalService 14 | } 15 | 16 | // The awesome method we're calling in index.js. 17 | actAllCool() { 18 | // Look ma'! No require! 19 | return this.functionalService 20 | .getStuffAndDeleteSecret('be cool') 21 | .then((stuff) => { 22 | stuff.isCool = true 23 | return stuff 24 | }) 25 | } 26 | } 27 | 28 | module.exports = ClassicalService 29 | -------------------------------------------------------------------------------- /examples/simple/services/functionalService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example of a "functional" service, 3 | * meaning that instead of exporting a class, 4 | * you export each function instead. 5 | * 6 | * Functions might depend on other things, so we will 7 | * use `container.bindAll` and `container.register` 8 | * to hook them up. When this is done, whenever 9 | * the functions are called, their first parameter 10 | * will always be the container itself. 11 | */ 12 | 13 | // By exporting this function as-is, we can inject mocks 14 | // as the first argument!!!!! 15 | function getStuffAndDeleteSecret(opts, someArgument) { 16 | // We depend on "stuffs" repository. 17 | const stuffs = opts.stuffs 18 | 19 | // We may now carry on. 20 | return stuffs.getStuff(someArgument).then((stuff) => { 21 | // Modify return value. Just to prove this is testable. 22 | delete stuff.secret 23 | return stuff 24 | }) 25 | } 26 | 27 | // NOTE: When using ES6 import-export, you can simply use `export default`. 28 | module.exports = function (opts) { 29 | // "opts" is the container "cradle", whenever a property getter is invoked, it will 30 | // result in a resolution. 31 | return { 32 | getStuffAndDeleteSecret: getStuffAndDeleteSecret.bind(null, { 33 | stuffs: opts.stuffs, 34 | }), 35 | } 36 | } 37 | 38 | module.exports.getStuffAndDeleteSecret = getStuffAndDeleteSecret 39 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "node dist/index.js", 8 | "build": "tsc" 9 | }, 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /examples/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | // This is largely for testing, but import what we need 2 | import { createContainer, asClass, InjectionMode } from '../../..' 3 | import TestService from './services/TestService' 4 | import DependentService from './services/DependentService' 5 | 6 | interface ICradle { 7 | testService: TestService 8 | depService: DependentService 9 | } 10 | 11 | // Create the container 12 | const container = createContainer({ 13 | injectionMode: InjectionMode.CLASSIC, 14 | strict: true, 15 | }) 16 | 17 | // Register the classes 18 | container.register({ 19 | testService: asClass(TestService), 20 | depService: asClass(DependentService).classic(), 21 | }) 22 | 23 | // Resolve a dependency using the cradle. 24 | const dep1 = container.cradle.depService 25 | // Resolve a dependency using `resolve` 26 | const dep2 = container.resolve('depService') 27 | 28 | // Test that all is well, should produce 'Hello world!' 29 | console.log(dep1.getInnerData()) 30 | console.log(dep2.getInnerData()) 31 | -------------------------------------------------------------------------------- /examples/typescript/src/services/DependentService.ts: -------------------------------------------------------------------------------- 1 | import TestService from './TestService' 2 | 3 | export default class DependentService { 4 | testService: TestService 5 | 6 | constructor(testService: TestService) { 7 | this.testService = testService 8 | } 9 | 10 | getInnerData(): string { 11 | return this.testService.getData() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript/src/services/TestService.ts: -------------------------------------------------------------------------------- 1 | export default class TestService { 2 | data: string 3 | 4 | constructor() { 5 | this.data = 'Hello world!' 6 | } 7 | 8 | getData(): string { 9 | return this.data 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "sourceMap": false, 7 | "target": "es5", 8 | "noImplicitAny": true, 9 | "rootDir": "./src", 10 | }, 11 | "include": ["**/*.ts"], 12 | "exclude": ["dist", "node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awilix", 3 | "version": "12.0.5", 4 | "description": "Extremely powerful dependency injection container.", 5 | "main": "lib/awilix.js", 6 | "module": "lib/awilix.module.mjs", 7 | "jsnext:main": "lib/awilix.module.mjs", 8 | "browser": "lib/awilix.browser.mjs", 9 | "umd:main": "lib/awilix.umd.js", 10 | "react-native": "lib/awilix.browser.mjs", 11 | "typings": "lib/awilix.d.ts", 12 | "engines": { 13 | "node": ">=16.3.0" 14 | }, 15 | "exports": { 16 | ".": { 17 | "browser": { 18 | "import": "./lib/awilix.browser.mjs", 19 | "default": "./lib/awilix.umd.js" 20 | }, 21 | "react-native": "./lib/awilix.browser.mjs", 22 | "import": "./lib/awilix.module.mjs", 23 | "types": "./lib/awilix.d.ts", 24 | "workerd": "./lib/awilix.browser.mjs", 25 | "default": "./lib/awilix.js" 26 | }, 27 | "./browser": { 28 | "import": "./lib/awilix.browser.mjs", 29 | "types": "./lib/awilix.d.ts", 30 | "workerd": "./lib/awilix.browser.mjs", 31 | "browser": "./lib/awilix.browser.mjs", 32 | "default": "./lib/awilix.umd.js" 33 | }, 34 | "./lib/*.js": { 35 | "types": "./lib/*.d.ts", 36 | "default": "./lib/*.js" 37 | }, 38 | "./lib/*": { 39 | "default": "./lib/*.js" 40 | } 41 | }, 42 | "scripts": { 43 | "build": "rimraf lib && tsc -p tsconfig.build.json && rollup -c", 44 | "check": "tsc -p tsconfig.json --noEmit --pretty", 45 | "test": "npm run check && jest", 46 | "lint": "npm run check && eslint --fix \"{src,examples}/**/*.ts\" && prettier --write \"{src,examples}/**/*.{ts,js}\"", 47 | "cover": "npm run test -- --coverage", 48 | "publish:pre": "npm run lint && npm run build && npm run cover", 49 | "publish:post": "npm publish && git push --follow-tags", 50 | "release:prerelease": "npm run publish:pre && npm version prerelease --preid alpha && npm run publish:post", 51 | "release:patch": "npm run publish:pre && npm version patch && npm run publish:post", 52 | "release:minor": "npm run publish:pre && npm version minor && npm run publish:post", 53 | "release:major": "npm run publish:pre && npm version major && npm run publish:post" 54 | }, 55 | "files": [ 56 | "lib", 57 | "LICENSE.md", 58 | "README.md" 59 | ], 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/jeffijoe/awilix.git" 63 | }, 64 | "keywords": [ 65 | "dependency-injection", 66 | "di", 67 | "container", 68 | "soc", 69 | "service-locator" 70 | ], 71 | "author": "Jeff Hansen ", 72 | "license": "MIT", 73 | "bugs": { 74 | "url": "https://github.com/jeffijoe/awilix/issues" 75 | }, 76 | "homepage": "https://github.com/jeffijoe/awilix#readme", 77 | "devDependencies": { 78 | "@babel/core": "^7.26.10", 79 | "@babel/plugin-transform-runtime": "^7.26.10", 80 | "@babel/preset-env": "^7.26.9", 81 | "@babel/runtime": "^7.26.10", 82 | "@rollup/plugin-commonjs": "^28.0.3", 83 | "@rollup/plugin-node-resolve": "^16.0.1", 84 | "@rollup/plugin-replace": "^6.0.2", 85 | "@types/jest": "^29.5.14", 86 | "@types/node": "^22.13.10", 87 | "babel-jest": "^29.7.0", 88 | "eslint": "^9.22.0", 89 | "husky": "^9.1.7", 90 | "jest": "^29.7.0", 91 | "lint-staged": "^15.5.0", 92 | "prettier": "^3.5.3", 93 | "rimraf": "^6.0.1", 94 | "rollup": "^4.35.0", 95 | "rollup-plugin-copy": "^3.5.0", 96 | "rollup-plugin-typescript2": "^0.36.0", 97 | "smid": "^0.1.1", 98 | "ts-jest": "^29.2.6", 99 | "tslib": "^2.8.1", 100 | "typescript": "^5.8.2", 101 | "typescript-eslint": "^8.26.1" 102 | }, 103 | "dependencies": { 104 | "camel-case": "^4.1.2", 105 | "fast-glob": "^3.3.3" 106 | }, 107 | "lint-staged": { 108 | "*.ts": [ 109 | "eslint --fix", 110 | "prettier --write" 111 | ] 112 | }, 113 | "prettier": { 114 | "semi": false, 115 | "singleQuote": true 116 | }, 117 | "babel": { 118 | "presets": [ 119 | [ 120 | "@babel/preset-env", 121 | { 122 | "targets": { 123 | "node": "14.0.0" 124 | } 125 | } 126 | ] 127 | ], 128 | "plugins": [ 129 | "@babel/plugin-transform-runtime" 130 | ] 131 | }, 132 | "typesync": { 133 | "ignorePackages": [ 134 | "@babel/preset-env", 135 | "@babel/core", 136 | "@babel/plugin-transform-runtime", 137 | "prettier", 138 | "rimraf", 139 | "istanbul" 140 | ] 141 | }, 142 | "jest": { 143 | "testRegex": "(/__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$", 144 | "testEnvironment": "node", 145 | "coveragePathIgnorePatterns": [ 146 | "/node_modules/", 147 | "__tests__", 148 | "lib", 149 | "src/load-module-native.js", 150 | "src/awilix.ts" 151 | ], 152 | "moduleFileExtensions": [ 153 | "ts", 154 | "tsx", 155 | "js" 156 | ], 157 | "transform": { 158 | "^.+\\.tsx?$": [ 159 | "ts-jest", 160 | { 161 | "useESM": true 162 | } 163 | ], 164 | "^.+\\.m?jsx?$": "babel-jest" 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import typescriptCompiler from 'typescript' 3 | import replace from '@rollup/plugin-replace' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import resolve from '@rollup/plugin-node-resolve' 6 | import copy from 'rollup-plugin-copy' 7 | 8 | const comment = '/* removed in browser build */' 9 | const ignoredWarnings = ['UNUSED_EXTERNAL_IMPORT'] 10 | 11 | const tsOpts = { 12 | cacheRoot: './node_modules/.rpt2', 13 | typescript: typescriptCompiler, 14 | tsconfig: 'tsconfig.build.json', 15 | tsconfigOverride: { 16 | compilerOptions: { 17 | // Don't emit declarations, that's done by the regular build. 18 | declaration: false, 19 | module: 'ESNext', 20 | }, 21 | }, 22 | } 23 | 24 | export default [ 25 | // Build 1: ES modules for Node. 26 | { 27 | input: 'src/awilix.ts', 28 | external: [ 29 | 'fast-glob', 30 | 'path', 31 | 'url', 32 | 'util', 33 | 'camel-case', 34 | './load-module-native.js', 35 | ], 36 | treeshake: { moduleSideEffects: 'no-external' }, 37 | onwarn, 38 | output: [ 39 | { 40 | file: 'lib/awilix.module.mjs', 41 | format: 'es', 42 | }, 43 | ], 44 | plugins: [ 45 | // Copy the native module loader 46 | copy({ 47 | targets: [{ src: 'src/load-module-native.js', dest: 'lib' }], 48 | }), 49 | typescript(tsOpts), 50 | ], 51 | }, 52 | // Build 2: ES modules for browser builds. 53 | { 54 | input: 'src/awilix.ts', 55 | external: ['fast-glob', 'path', 'url', 'util'], 56 | treeshake: { moduleSideEffects: 'no-external' }, 57 | onwarn, 58 | output: [ 59 | { 60 | name: 'Awilix', 61 | file: 'lib/awilix.browser.mjs', 62 | format: 'es', 63 | }, 64 | { 65 | name: 'Awilix', 66 | file: 'lib/awilix.umd.js', 67 | format: 'umd', 68 | }, 69 | ], 70 | plugins: [ 71 | // Removes stuff that won't work in the browser 72 | // which also means node-only stuff like `path`, `util` and `fast-glob` 73 | // will be shaken off. 74 | replace({ 75 | delimiters: ['', ''], 76 | preventAssignment: true, 77 | 'loadModules,': 78 | 'loadModules: () => { throw new Error("loadModules is not supported in the browser.") },', 79 | '[util.inspect.custom]: inspect,': comment, 80 | '[util.inspect.custom]: toStringRepresentationFn,': comment, 81 | 'case util.inspect.custom:': '', 82 | "import { camelCase } from 'camel-case'": 83 | 'const camelCase = null as any', 84 | [`export { 85 | type GlobWithOptions, 86 | type ListModulesOptions, 87 | type ModuleDescriptor, 88 | listModules, 89 | } from './list-modules'`]: comment, 90 | "import * as util from 'util'": '', 91 | }), 92 | typescript( 93 | Object.assign({}, tsOpts, { 94 | tsconfigOverride: { 95 | compilerOptions: { 96 | target: 'ES2020', 97 | declaration: false, 98 | noUnusedLocals: false, 99 | module: 'ESNext', 100 | }, 101 | }, 102 | }), 103 | ), 104 | resolve({ 105 | preferBuiltins: true, 106 | }), 107 | commonjs(), 108 | ], 109 | }, 110 | ] 111 | 112 | /** 113 | * Ignores certain warnings. 114 | */ 115 | function onwarn(warning, next) { 116 | if (ignoredWarnings.includes(warning.code)) { 117 | return 118 | } 119 | 120 | next(warning) 121 | } 122 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/function-tokenizer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`tokenizer can skip function calls 1`] = ` 4 | [ 5 | { 6 | "type": "function", 7 | }, 8 | { 9 | "type": "ident", 10 | "value": "funcCalls", 11 | }, 12 | { 13 | "type": "(", 14 | }, 15 | { 16 | "type": "ident", 17 | "value": "first", 18 | }, 19 | { 20 | "type": "=", 21 | }, 22 | { 23 | "type": ",", 24 | }, 25 | { 26 | "type": "ident", 27 | "value": "other", 28 | }, 29 | { 30 | "type": "=", 31 | }, 32 | { 33 | "type": ")", 34 | }, 35 | { 36 | "type": "EOF", 37 | }, 38 | ] 39 | `; 40 | 41 | exports[`tokenizer can skip interpolated strings 1`] = ` 42 | [ 43 | { 44 | "type": "function", 45 | }, 46 | { 47 | "type": "ident", 48 | "value": "intstring1", 49 | }, 50 | { 51 | "type": "(", 52 | }, 53 | { 54 | "type": "ident", 55 | "value": "p1", 56 | }, 57 | { 58 | "type": "=", 59 | }, 60 | { 61 | "type": ",", 62 | }, 63 | { 64 | "type": "ident", 65 | "value": "p2", 66 | }, 67 | { 68 | "type": "=", 69 | }, 70 | { 71 | "type": ")", 72 | }, 73 | { 74 | "type": "EOF", 75 | }, 76 | ] 77 | `; 78 | 79 | exports[`tokenizer can skip interpolated strings 2`] = ` 80 | [ 81 | { 82 | "type": "function", 83 | }, 84 | { 85 | "type": "ident", 86 | "value": "intstring2", 87 | }, 88 | { 89 | "type": "(", 90 | }, 91 | { 92 | "type": "ident", 93 | "value": "deep", 94 | }, 95 | { 96 | "type": "=", 97 | }, 98 | { 99 | "type": ",", 100 | }, 101 | { 102 | "type": "ident", 103 | "value": "asFuck", 104 | }, 105 | { 106 | "type": "=", 107 | }, 108 | { 109 | "type": ")", 110 | }, 111 | { 112 | "type": "EOF", 113 | }, 114 | ] 115 | `; 116 | 117 | exports[`tokenizer can skip object literals 1`] = ` 118 | [ 119 | { 120 | "type": "function", 121 | }, 122 | { 123 | "type": "ident", 124 | "value": "obj", 125 | }, 126 | { 127 | "type": "(", 128 | }, 129 | { 130 | "type": "ident", 131 | "value": "p1", 132 | }, 133 | { 134 | "type": "=", 135 | }, 136 | { 137 | "type": ",", 138 | }, 139 | { 140 | "type": "ident", 141 | "value": "p2", 142 | }, 143 | { 144 | "type": "=", 145 | }, 146 | { 147 | "type": ",", 148 | }, 149 | { 150 | "type": "ident", 151 | "value": "p3", 152 | }, 153 | { 154 | "type": "=", 155 | }, 156 | { 157 | "type": ")", 158 | }, 159 | { 160 | "type": "EOF", 161 | }, 162 | ] 163 | `; 164 | 165 | exports[`tokenizer can skip strings with escape seqs in them 1`] = ` 166 | [ 167 | { 168 | "type": "function", 169 | }, 170 | { 171 | "type": "ident", 172 | "value": "rofl", 173 | }, 174 | { 175 | "type": "(", 176 | }, 177 | { 178 | "type": "ident", 179 | "value": "p1", 180 | }, 181 | { 182 | "type": "=", 183 | }, 184 | { 185 | "type": ",", 186 | }, 187 | { 188 | "type": "ident", 189 | "value": "p2", 190 | }, 191 | { 192 | "type": "=", 193 | }, 194 | { 195 | "type": ",", 196 | }, 197 | { 198 | "type": "ident", 199 | "value": "p3", 200 | }, 201 | { 202 | "type": "=", 203 | }, 204 | { 205 | "type": ")", 206 | }, 207 | { 208 | "type": "EOF", 209 | }, 210 | ] 211 | `; 212 | 213 | exports[`tokenizer can tokenize arrow functions 1`] = ` 214 | [ 215 | { 216 | "type": "(", 217 | }, 218 | { 219 | "type": "ident", 220 | "value": "first", 221 | }, 222 | { 223 | "type": "=", 224 | }, 225 | { 226 | "type": ",", 227 | }, 228 | { 229 | "type": "ident", 230 | "value": "other", 231 | }, 232 | { 233 | "type": "=", 234 | }, 235 | { 236 | "type": ")", 237 | }, 238 | { 239 | "type": "=", 240 | }, 241 | { 242 | "type": "EOF", 243 | }, 244 | ] 245 | `; 246 | 247 | exports[`tokenizer does not require function name 1`] = ` 248 | [ 249 | { 250 | "type": "function", 251 | }, 252 | { 253 | "type": "(", 254 | }, 255 | { 256 | "type": "ident", 257 | "value": "first", 258 | }, 259 | { 260 | "type": ",", 261 | }, 262 | { 263 | "type": "ident", 264 | "value": "second", 265 | }, 266 | { 267 | "type": ")", 268 | }, 269 | { 270 | "type": "EOF", 271 | }, 272 | ] 273 | `; 274 | 275 | exports[`tokenizer includes equals token but skips value correctly 1`] = ` 276 | [ 277 | { 278 | "type": "function", 279 | }, 280 | { 281 | "type": "ident", 282 | "value": "rofl", 283 | }, 284 | { 285 | "type": "(", 286 | }, 287 | { 288 | "type": "ident", 289 | "value": "p1", 290 | }, 291 | { 292 | "type": ",", 293 | }, 294 | { 295 | "type": "ident", 296 | "value": "p2", 297 | }, 298 | { 299 | "type": "=", 300 | }, 301 | { 302 | "type": ",", 303 | }, 304 | { 305 | "type": "ident", 306 | "value": "p3", 307 | }, 308 | { 309 | "type": "=", 310 | }, 311 | { 312 | "type": ")", 313 | }, 314 | { 315 | "type": "EOF", 316 | }, 317 | ] 318 | `; 319 | 320 | exports[`tokenizer includes equals token but skips value correctly 2`] = ` 321 | [ 322 | { 323 | "type": "function", 324 | }, 325 | { 326 | "type": "ident", 327 | "value": "rofl", 328 | }, 329 | { 330 | "type": "(", 331 | }, 332 | { 333 | "type": "ident", 334 | "value": "p1", 335 | }, 336 | { 337 | "type": "=", 338 | }, 339 | { 340 | "type": ",", 341 | }, 342 | { 343 | "type": "ident", 344 | "value": "p2", 345 | }, 346 | { 347 | "type": "=", 348 | }, 349 | { 350 | "type": ",", 351 | }, 352 | { 353 | "type": "ident", 354 | "value": "p3", 355 | }, 356 | { 357 | "type": "=", 358 | }, 359 | { 360 | "type": ")", 361 | }, 362 | { 363 | "type": "EOF", 364 | }, 365 | ] 366 | `; 367 | -------------------------------------------------------------------------------- /src/__tests__/awilix.test.ts: -------------------------------------------------------------------------------- 1 | import * as awilix from '../awilix' 2 | import { createContainer } from '../container' 3 | import { listModules } from '../list-modules' 4 | import { AwilixResolutionError } from '../errors' 5 | import { asValue, asClass, asFunction, aliasTo } from '../resolvers' 6 | 7 | describe('awilix', () => { 8 | it('exists', () => { 9 | expect(awilix).toBeDefined() 10 | }) 11 | 12 | it('has a createContainer function', () => { 13 | expect(awilix).toHaveProperty('createContainer') 14 | expect(awilix.createContainer).toBe(createContainer) 15 | }) 16 | 17 | it('has a listModules function', () => { 18 | expect(awilix).toHaveProperty('listModules') 19 | expect(awilix.listModules).toBe(listModules) 20 | }) 21 | 22 | it('has an AwilixResolutionError function', () => { 23 | expect(awilix).toHaveProperty('AwilixResolutionError') 24 | expect(awilix.AwilixResolutionError).toBe(AwilixResolutionError) 25 | }) 26 | 27 | it('has the asValue, asClass, asFunction and aliasTo functions', () => { 28 | expect(awilix.asValue).toBe(asValue) 29 | expect(awilix.asClass).toBe(asClass) 30 | expect(awilix.asFunction).toBe(asFunction) 31 | expect(awilix.aliasTo).toBe(aliasTo) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/__tests__/container.disposing.test.ts: -------------------------------------------------------------------------------- 1 | import { createContainer } from '../container' 2 | import { asClass, asFunction } from '../resolvers' 3 | import { InjectionMode } from '../injection-mode' 4 | import { AwilixContainer } from '../awilix' 5 | 6 | class TestClass { 7 | constructor(funcDisposer: TestFunc) { 8 | /**/ 9 | } 10 | 11 | dispose() { 12 | /**/ 13 | } 14 | } 15 | 16 | interface TestFunc { 17 | destroy(): Promise 18 | } 19 | 20 | function testFunc(notDisposer: object): TestFunc { 21 | return { 22 | destroy() { 23 | return Promise.resolve(42) 24 | }, 25 | } 26 | } 27 | 28 | describe('disposing container', () => { 29 | let order: Array 30 | let container: AwilixContainer 31 | let scope: AwilixContainer 32 | beforeEach(() => { 33 | order = [] 34 | container = createContainer({ 35 | injectionMode: InjectionMode.CLASSIC, 36 | }).register({ 37 | notDisposer: asFunction(() => ({})).scoped(), 38 | scopedButRegedAtRoot: asFunction(testFunc) 39 | .scoped() 40 | .disposer((t) => { 41 | order.push(2) 42 | return t.destroy() 43 | }), 44 | funcDisposer: asFunction(testFunc) 45 | .singleton() 46 | .disposer((t) => { 47 | order.push(2) 48 | return t.destroy() 49 | }), 50 | }) 51 | 52 | scope = container.createScope().register({ 53 | classDisposer: asClass(TestClass, { 54 | dispose: (t) => { 55 | order.push(1) 56 | return t.dispose() 57 | }, 58 | }).scoped(), 59 | }) 60 | }) 61 | 62 | it('calls the disposers in the correct order', async () => { 63 | scope.resolve('funcDisposer') 64 | scope.resolve('classDisposer') 65 | await container.dispose() 66 | expect(order).toEqual([2]) 67 | }) 68 | 69 | it('when the scope is disposed directly, disposes scoped instances but does not dispose the root singletons', async () => { 70 | scope.resolve('funcDisposer') 71 | scope.resolve('classDisposer') 72 | scope.resolve('scopedButRegedAtRoot') 73 | await scope.dispose() 74 | expect(order).toEqual([1, 2]) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /src/__tests__/fixture/index.js: -------------------------------------------------------------------------------- 1 | const awilix = require('../../awilix') 2 | 3 | module.exports = function () { 4 | const opts = { cwd: __dirname } 5 | return awilix 6 | .createContainer() 7 | .register({ 8 | conn: awilix.asValue({}), 9 | }) 10 | .loadModules( 11 | [ 12 | ['services/*.js', awilix.Lifetime.SCOPED], 13 | ['repositories/*.js', { injector: () => ({ timeout: 10 }) }], 14 | ], 15 | opts, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/__tests__/fixture/repositories/answerRepository.js: -------------------------------------------------------------------------------- 1 | const THE_UNIVERSAL_ANSWER = 42 2 | 3 | module.exports = function ({ timeout, conn }) { 4 | /* istanbul ignore next */ 5 | if (!timeout) { 6 | throw new Error('No timeout specified') 7 | } 8 | 9 | /* istanbul ignore next */ 10 | if (!conn) { 11 | throw new Error('No conn specified') 12 | } 13 | 14 | const getAnswerFor = function (question) { 15 | return new Promise((resolve) => { 16 | setTimeout(() => resolve(THE_UNIVERSAL_ANSWER), timeout) 17 | }) 18 | } 19 | 20 | return { 21 | getAnswerFor, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/fixture/repositories/someUtil.js: -------------------------------------------------------------------------------- 1 | module.exports.someUtil = function someUtil() { 2 | // This module has no default export, 3 | // and so should not be registered. 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/fixture/services/anotherService.js: -------------------------------------------------------------------------------- 1 | class AnotherService { 2 | constructor(answerRepository) { 3 | this.repo = answerRepository 4 | } 5 | } 6 | 7 | module.exports.AnotherService = AnotherService 8 | 9 | module.exports.default = function (deps) { 10 | return new AnotherService(deps.answerRepository) 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/fixture/services/mainService.js: -------------------------------------------------------------------------------- 1 | module.exports = function ({ answerRepository }) { 2 | const getTheAnswer = function (question) { 3 | const repo = answerRepository 4 | return repo.getAnswerFor(question).then((theAnswer) => { 5 | return `The answer to "${question}" is: ${theAnswer}` 6 | }) 7 | } 8 | 9 | return { 10 | getTheAnswer, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/__tests__/function-tokenizer.test.ts: -------------------------------------------------------------------------------- 1 | import { createTokenizer, Token } from '../function-tokenizer' 2 | 3 | describe('tokenizer', () => { 4 | it('returns the expected tokens for a function', () => { 5 | const tokens = getTokens('function lol(p1)') 6 | expect(tokens).toEqual([ 7 | { type: 'function' }, 8 | { type: 'ident', value: 'lol' }, 9 | { type: '(' }, 10 | { type: 'ident', value: 'p1' }, 11 | { type: ')' }, 12 | { type: 'EOF' }, 13 | ]) 14 | }) 15 | 16 | it('returns the expected tokens for a class', () => { 17 | const tokens = getTokens('class Hah { constructor(p1, p2) {}}') 18 | expect(tokens).toEqual([ 19 | { type: 'class' }, 20 | { type: 'ident', value: 'Hah' }, 21 | { type: 'ident', value: 'constructor' }, 22 | { type: '(' }, 23 | { type: 'ident', value: 'p1' }, 24 | { type: ',' }, 25 | { type: 'ident', value: 'p2' }, 26 | { type: ')' }, 27 | { type: 'EOF' }, 28 | ]) 29 | }) 30 | 31 | it('does not require the constructor to be first', () => { 32 | const tokens = getTokens( 33 | 'class Hah { wee = "propinit"; method(lol, haha = 123, p = "i shall constructor your jimmies") {} constructor(p1, p2) {}}', 34 | ) 35 | expect(tokens).toContainEqual({ type: 'ident', value: 'constructor' }) 36 | expect(tokens).toContainEqual({ type: 'ident', value: 'p1' }) 37 | expect(tokens).toContainEqual({ type: 'ident', value: 'p2' }) 38 | }) 39 | 40 | it('includes equals token but skips value correctly', () => { 41 | expect( 42 | getTokens('function rofl(p1, p2 = hah, p3 = 123.45)'), 43 | ).toMatchSnapshot() 44 | 45 | expect( 46 | getTokens( 47 | `function rofl(p1 = 'wee', p2 = "woo", p3 = \`haha "lol" 'dude'\`)`, 48 | ), 49 | ).toMatchSnapshot() 50 | }) 51 | 52 | it('can skip strings with escape seqs in them', () => { 53 | expect( 54 | getTokens( 55 | `function rofl(p1 = 'we\\'e', p2 = "wo\\"o", p3 = \`haha \\\`lol" 'dude'\`)`, 56 | ), 57 | ).toMatchSnapshot() 58 | }) 59 | 60 | it('can skip interpolated strings', () => { 61 | expect( 62 | getTokens(`function intstring1(p1 = \`Hello \${world}\`, p2 = 123)`), 63 | ).toMatchSnapshot() 64 | expect( 65 | getTokens( 66 | `function intstring2(deep = \`Hello$ \${stuff && \`another $\{func(\`"haha"\`, man = 123)}\`}\`, asFuck = 123)`, 67 | ), 68 | ).toMatchSnapshot() 69 | }) 70 | 71 | it('can skip object literals', () => { 72 | expect( 73 | getTokens( 74 | `function obj(p1 = {waddup: 'homie'}, p2 = ({ you: gotThis('dawg:)', \`$\{bruh("hah" && fn(1,2,3))}\`) }), p3 = 123)`, 75 | ), 76 | ).toMatchSnapshot() 77 | }) 78 | 79 | it('can skip function calls', () => { 80 | expect( 81 | getTokens( 82 | `function funcCalls(first = require('path'), other = require(\`some-$\{module && "yeah"}\`))`, 83 | ), 84 | ).toMatchSnapshot() 85 | }) 86 | 87 | it('can tokenize arrow functions', () => { 88 | expect( 89 | getTokens( 90 | `(first = require('path'), other = require(\`some-$\{module && "yeah"}\`)) => {}`, 91 | ), 92 | ).toMatchSnapshot() 93 | }) 94 | 95 | it('does not require function name', () => { 96 | expect(getTokens(`function (first, second)`)).toMatchSnapshot() 97 | }) 98 | }) 99 | 100 | function getTokens(source: string) { 101 | const tokenizer = createTokenizer(source) 102 | let t: Token 103 | const tokens: Array = [] 104 | do { 105 | t = tokenizer.next() 106 | tokens.push(t) 107 | } while (t.type !== 'EOF') 108 | return tokens 109 | } 110 | -------------------------------------------------------------------------------- /src/__tests__/inheritance.test.js: -------------------------------------------------------------------------------- 1 | import { createContainer } from '../container' 2 | import { InjectionMode } from '../injection-mode' 3 | import { asClass, asValue } from '../resolvers' 4 | 5 | // NOTE: this test is in JS because TS won't allow the super(...arguments) trick. 6 | 7 | test('parses parent classes if there are no declared parameters', () => { 8 | const container = createContainer({ injectionMode: InjectionMode.CLASSIC }) 9 | 10 | class Level1 { 11 | constructor(level1Arg1, level1Arg2) { 12 | this.arg1 = level1Arg1 13 | this.arg2 = level1Arg2 14 | } 15 | } 16 | 17 | class Level2 extends Level1 {} 18 | 19 | class Level3 extends Level2 { 20 | constructor(level3Arg1) { 21 | super(...arguments) 22 | } 23 | } 24 | 25 | container.register({ 26 | level1Arg1: asValue(1), 27 | level1Arg2: asValue(2), 28 | level3Arg1: asValue(4), 29 | level1: asClass(Level1), 30 | level2: asClass(Level2), 31 | level3: asClass(Level3), 32 | }) 33 | 34 | expect(container.resolve('level1')).toEqual( 35 | expect.objectContaining({ 36 | arg1: 1, 37 | arg2: 2, 38 | }), 39 | ) 40 | 41 | expect(container.resolve('level2')).toEqual( 42 | expect.objectContaining({ 43 | arg1: 1, 44 | arg2: 2, 45 | }), 46 | ) 47 | 48 | expect(container.resolve('level3')).toEqual( 49 | expect.objectContaining({ 50 | arg1: 4, 51 | arg2: undefined, 52 | }), 53 | ) 54 | }) 55 | -------------------------------------------------------------------------------- /src/__tests__/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { Lifetime } from '../lifetime' 2 | 3 | const AnotherService = 4 | require('./fixture/services/anotherService').AnotherService 5 | const fixture = require('./fixture') 6 | 7 | describe('integration tests', () => { 8 | it('bootstraps everything so the answer can be resolved', () => { 9 | const container = fixture() 10 | const anotherService = container.resolve('anotherService') 11 | expect(anotherService).toBeInstanceOf(AnotherService) 12 | expect(typeof anotherService.repo).toBe('object') 13 | expect(typeof anotherService.repo.getAnswerFor).toBe('function') 14 | expect(Object.keys(container.registrations).length).toBe(4) 15 | }) 16 | 17 | it('registered all services as scoped', () => { 18 | const container = fixture() 19 | const scope1 = container.createScope() 20 | const scope2 = container.createScope() 21 | 22 | expect(container.registrations.mainService.lifetime).toBe(Lifetime.SCOPED) 23 | expect(container.registrations.anotherService.lifetime).toBe( 24 | Lifetime.SCOPED, 25 | ) 26 | expect(container.registrations.answerRepository.lifetime).toBe( 27 | Lifetime.TRANSIENT, 28 | ) 29 | 30 | expect(scope1.resolve('mainService')).toBe(scope1.resolve('mainService')) 31 | expect(scope1.resolve('mainService')).not.toBe( 32 | scope2.resolve('mainService'), 33 | ) 34 | 35 | expect(scope1.resolve('anotherService')).toBe( 36 | scope1.resolve('anotherService'), 37 | ) 38 | expect(scope1.resolve('anotherService')).not.toBe( 39 | scope2.resolve('anotherService'), 40 | ) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/__tests__/lifetime.test.ts: -------------------------------------------------------------------------------- 1 | import { Lifetime, isLifetimeLonger } from '../lifetime' 2 | 3 | describe('isLifetimeLonger', () => { 4 | it('correctly compares lifetimes', () => { 5 | expect(isLifetimeLonger(Lifetime.TRANSIENT, Lifetime.TRANSIENT)).toBe(false) 6 | expect(isLifetimeLonger(Lifetime.TRANSIENT, Lifetime.SCOPED)).toBe(false) 7 | expect(isLifetimeLonger(Lifetime.TRANSIENT, Lifetime.SINGLETON)).toBe(false) 8 | expect(isLifetimeLonger(Lifetime.SCOPED, Lifetime.TRANSIENT)).toBe(true) 9 | expect(isLifetimeLonger(Lifetime.SCOPED, Lifetime.SCOPED)).toBe(false) 10 | expect(isLifetimeLonger(Lifetime.SCOPED, Lifetime.SINGLETON)).toBe(false) 11 | expect(isLifetimeLonger(Lifetime.SINGLETON, Lifetime.TRANSIENT)).toBe(true) 12 | expect(isLifetimeLonger(Lifetime.SINGLETON, Lifetime.SCOPED)).toBe(true) 13 | expect(isLifetimeLonger(Lifetime.SINGLETON, Lifetime.SINGLETON)).toBe(false) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/__tests__/list-modules.test.ts: -------------------------------------------------------------------------------- 1 | import { listModules } from '../list-modules' 2 | 3 | describe('listModules', () => { 4 | it('can find the modules in lib', () => { 5 | const result = listModules('../*.ts', { cwd: __dirname }) 6 | expect(result.some((x) => x.name === 'container')).toBeTruthy() 7 | }) 8 | 9 | it('replaces Windows path separators with Posix path separators', () => { 10 | const result = listModules('..\\*.ts', { cwd: __dirname }) 11 | expect(result.some((x) => x.name === 'container')).toBeTruthy() 12 | }) 13 | 14 | it('can find the modules in src without cwd', () => { 15 | const result = listModules('src/*.ts') 16 | expect(result.some((x) => x.name === 'container')).toBeTruthy() 17 | }) 18 | 19 | it('handles dots in module names', () => { 20 | const result = listModules('*.{ts,js}', { cwd: __dirname }) 21 | expect(result.find((x) => x.name === 'container.test')).toBeDefined() 22 | }) 23 | 24 | it('returns a path', () => { 25 | const result = listModules('../*.ts', { cwd: __dirname }) 26 | const createContainerModule = result.find((x) => x.name === 'container')! 27 | expect(createContainerModule.name).toBe('container') 28 | expect(createContainerModule.path).toContain('container.ts') 29 | }) 30 | 31 | it('supports an array of globs', () => { 32 | const result = listModules(['src/*.ts']) 33 | const createContainerModule = result.find((x) => x.name === 'container')! 34 | expect(createContainerModule.name).toBe('container') 35 | expect(createContainerModule.path).toContain('container.ts') 36 | }) 37 | 38 | it('supports array-opts syntax', () => { 39 | const opts = { value: 'yep' } 40 | const result = listModules([['src/*.ts', opts as any]]) 41 | 42 | const createContainerModule = result.find((x) => x.name === 'container')! 43 | expect(createContainerModule.name).toBe('container') 44 | expect(createContainerModule.path).toContain('container.ts') 45 | expect(createContainerModule.opts).toEqual(opts) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/__tests__/load-modules.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { loadModules, LoadModulesOptions } from '../load-modules' 3 | import { createContainer } from '../container' 4 | import { Lifetime } from '../lifetime' 5 | import { InjectionMode } from '../injection-mode' 6 | import { asFunction, RESOLVER, BuildResolver, asValue } from '../resolvers' 7 | 8 | const lookupResultFor = (modules: any) => 9 | Object.keys(modules).map((key) => ({ 10 | name: key.replace('.js', ''), 11 | path: key, 12 | opts: null, 13 | })) 14 | 15 | describe('loadModules', () => { 16 | it('registers loaded modules with the container using the name of the file', () => { 17 | const container = createContainer() 18 | 19 | class SomeClass {} 20 | 21 | const modules: any = { 22 | 'nope.js': undefined, 23 | 'standard.js': jest.fn(() => 42), 24 | 'default.js': { default: jest.fn(() => 1337) }, 25 | 'someClass.js': SomeClass, 26 | } 27 | const moduleLookupResult = lookupResultFor(modules) 28 | const deps = { 29 | container, 30 | listModules: jest.fn(() => moduleLookupResult), 31 | require: jest.fn((path) => modules[path]), 32 | } 33 | 34 | const result = loadModules(deps, 'anything') 35 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 36 | expect(Object.keys(container.registrations).length).toBe(3) 37 | expect(container.resolve('standard')).toBe(42) 38 | expect(container.resolve('default')).toBe(1337) 39 | expect(container.resolve('someClass')).toBeInstanceOf(SomeClass) 40 | }) 41 | 42 | it('registers loaded modules async when using native modules', async () => { 43 | const container = createContainer() 44 | 45 | class SomeClass {} 46 | 47 | const modules: any = { 48 | 'nope.js': undefined, 49 | 'standard.js': jest.fn(() => 42), 50 | 'default.js': { default: jest.fn(() => 1337) }, 51 | 'someClass.js': SomeClass, 52 | } 53 | 54 | const moduleLookupResult = lookupResultFor(modules) 55 | const deps = { 56 | container, 57 | listModules: jest.fn(() => moduleLookupResult), 58 | require: jest.fn(async (p) => modules[path.parse(p).base]), 59 | } 60 | 61 | const result = await loadModules(deps, 'anything', { esModules: true }) 62 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 63 | expect(Object.keys(container.registrations).length).toBe(3) 64 | expect(container.resolve('standard')).toBe(42) 65 | expect(container.resolve('default')).toBe(1337) 66 | expect(container.resolve('someClass')).toBeInstanceOf(SomeClass) 67 | }) 68 | 69 | it('registers non-default export modules containing RESOLVER token with the container', () => { 70 | const container = createContainer() 71 | 72 | class SomeNonDefaultClass { 73 | static [RESOLVER] = {} 74 | } 75 | 76 | const modules: any = { 77 | 'someIgnoredName.js': { SomeNonDefaultClass }, 78 | } 79 | const moduleLookupResult = lookupResultFor(modules) 80 | const deps = { 81 | container, 82 | listModules: jest.fn(() => moduleLookupResult), 83 | require: jest.fn((path) => modules[path]), 84 | } 85 | 86 | const result = loadModules(deps, 'anything') 87 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 88 | expect(Object.keys(container.registrations).length).toBe(1) 89 | // Note the capital first letter because the export key name is used instead of the filename 90 | expect(container.resolve('SomeNonDefaultClass')).toBeInstanceOf( 91 | SomeNonDefaultClass, 92 | ) 93 | }) 94 | 95 | it('does not register non-default modules without a RESOLVER token', () => { 96 | const container = createContainer() 97 | 98 | class SomeClass {} 99 | 100 | const modules: any = { 101 | 'nopeClass.js': { SomeClass }, 102 | } 103 | const moduleLookupResult = lookupResultFor(modules) 104 | const deps = { 105 | container, 106 | listModules: jest.fn(() => moduleLookupResult), 107 | require: jest.fn((path) => modules[path]), 108 | } 109 | 110 | const result = loadModules(deps, 'anything') 111 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 112 | expect(Object.keys(container.registrations).length).toBe(0) 113 | }) 114 | 115 | it('registers multiple loaded modules from one file with the container', () => { 116 | const container = createContainer() 117 | 118 | class SomeClass {} 119 | class SomeNonDefaultClass { 120 | static [RESOLVER] = {} 121 | } 122 | class SomeNamedNonDefaultClass { 123 | static [RESOLVER] = { 124 | name: 'nameOverride', 125 | } 126 | } 127 | 128 | const modules: any = { 129 | 'mixedFile.js': { 130 | default: SomeClass, 131 | nonDefault: SomeNonDefaultClass, 132 | namedNonDefault: SomeNamedNonDefaultClass, 133 | }, 134 | } 135 | const moduleLookupResult = lookupResultFor(modules) 136 | const deps = { 137 | container, 138 | listModules: jest.fn(() => moduleLookupResult), 139 | require: jest.fn((path) => modules[path]), 140 | } 141 | 142 | const result = loadModules(deps, 'anything') 143 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 144 | expect(Object.keys(container.registrations).length).toBe(3) 145 | expect(container.resolve('mixedFile')).toBeInstanceOf(SomeClass) 146 | expect(container.resolve('nonDefault')).toBeInstanceOf(SomeNonDefaultClass) 147 | expect(container.resolve('nameOverride')).toBeInstanceOf( 148 | SomeNamedNonDefaultClass, 149 | ) 150 | }) 151 | 152 | it('registers only the last module with a certain name with the container', () => { 153 | const container = createContainer() 154 | 155 | class SomeClass {} 156 | class SomeNonDefaultClass { 157 | static [RESOLVER] = {} 158 | } 159 | class SomeNamedNonDefaultClass { 160 | static [RESOLVER] = { 161 | name: 'nameOverride', 162 | } 163 | } 164 | 165 | const modules: any = { 166 | 'mixedFileOne.js': { 167 | default: SomeClass, 168 | nameOverride: SomeNonDefaultClass, 169 | // this will override the above named export with its specified name 170 | namedNonDefault: SomeNamedNonDefaultClass, 171 | }, 172 | 'mixedFileTwo.js': { 173 | // this will override the default export from mixedFileOne 174 | mixedFileOne: SomeNonDefaultClass, 175 | }, 176 | } 177 | 178 | const moduleLookupResult = lookupResultFor(modules) 179 | const deps = { 180 | container, 181 | listModules: jest.fn(() => moduleLookupResult), 182 | require: jest.fn((path) => modules[path]), 183 | } 184 | 185 | const result = loadModules(deps, 'anything') 186 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 187 | expect(Object.keys(container.registrations).length).toBe(2) 188 | expect(container.resolve('mixedFileOne')).toBeInstanceOf( 189 | SomeNonDefaultClass, 190 | ) 191 | expect(container.resolve('nameOverride')).toBeInstanceOf( 192 | SomeNamedNonDefaultClass, 193 | ) 194 | }) 195 | 196 | it('uses built-in formatter when given a formatName as a string', () => { 197 | const container = createContainer() 198 | const modules: any = { 199 | 'SomeClass.js': jest.fn(() => 42), 200 | } 201 | const moduleLookupResult = lookupResultFor(modules) 202 | const deps = { 203 | container, 204 | listModules: jest.fn(() => moduleLookupResult), 205 | require: jest.fn((path) => modules[path]), 206 | } 207 | const opts: LoadModulesOptions = { 208 | formatName: 'camelCase', 209 | } 210 | const result = loadModules(deps, 'anything', opts) 211 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 212 | const reg = container.registrations.someClass 213 | expect(reg).toBeTruthy() 214 | }) 215 | 216 | it('uses the function passed in as formatName', () => { 217 | const container = createContainer() 218 | const modules: any = { 219 | 'SomeClass.js': jest.fn(() => 42), 220 | } 221 | const moduleLookupResult = lookupResultFor(modules) 222 | const deps = { 223 | container, 224 | listModules: jest.fn(() => moduleLookupResult), 225 | require: jest.fn((path) => modules[path]), 226 | } 227 | const opts: LoadModulesOptions = { 228 | formatName: (name, descriptor) => { 229 | expect(descriptor.path).toBeTruthy() 230 | return name + 'IsGreat' 231 | }, 232 | } 233 | const result = loadModules(deps, 'anything', opts) 234 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 235 | const reg = container.registrations.SomeClassIsGreat 236 | expect(reg).toBeTruthy() 237 | }) 238 | 239 | it('does nothing with the name if the string formatName does not match a formatter', () => { 240 | const container = createContainer() 241 | const modules: any = { 242 | 'SomeClass.js': jest.fn(() => 42), 243 | } 244 | const moduleLookupResult = lookupResultFor(modules) 245 | const deps = { 246 | container, 247 | listModules: jest.fn(() => moduleLookupResult), 248 | require: jest.fn((path) => modules[path]), 249 | } 250 | const opts: any = { 251 | formatName: 'unknownformatternope', 252 | } 253 | const result = loadModules(deps, 'anything', opts) 254 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 255 | const reg = container.registrations.SomeClass 256 | expect(reg).toBeTruthy() 257 | }) 258 | 259 | it('defaults to transient lifetime if option is unreadable', () => { 260 | const container = createContainer() 261 | const modules: any = { 262 | 'test.js': jest.fn(() => 42), 263 | } 264 | const moduleLookupResult = lookupResultFor(modules) 265 | const deps = { 266 | container, 267 | listModules: jest.fn(() => moduleLookupResult), 268 | require: jest.fn((path) => modules[path]), 269 | } 270 | const opts = { 271 | resolverOptions: {}, 272 | } 273 | const result = loadModules(deps, 'anything', opts) 274 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 275 | const reg = container.registrations.test 276 | expect(reg).toBeTruthy() 277 | }) 278 | 279 | it('supports passing in a register function', () => { 280 | const container = createContainer() 281 | const moduleSpy = jest.fn(() => () => 42) 282 | const modules: any = { 283 | 'test.js': moduleSpy, 284 | } 285 | const moduleLookupResult = lookupResultFor(modules) 286 | const registerSpy = jest.fn(asFunction) 287 | const deps = { 288 | container, 289 | listModules: jest.fn(() => moduleLookupResult), 290 | require: jest.fn((path) => modules[path]), 291 | } 292 | const regOpts = { 293 | register: registerSpy, 294 | lifetime: Lifetime.SCOPED, 295 | } 296 | const opts = { 297 | resolverOptions: regOpts, 298 | } 299 | const result = loadModules(deps, 'anything', opts) 300 | expect(result).toEqual({ loadedModules: moduleLookupResult }) 301 | const reg = container.registrations.test 302 | expect(reg).toBeTruthy() 303 | expect(registerSpy).toHaveBeenCalledWith(moduleSpy, regOpts) 304 | }) 305 | 306 | it('supports array opts syntax with string (lifetime)', () => { 307 | const container = createContainer() 308 | const modules: any = { 309 | 'test.js': jest.fn(() => 42), 310 | 'test2.js': jest.fn(() => 42), 311 | } 312 | 313 | const deps = { 314 | container, 315 | listModules: jest.fn(() => [ 316 | { name: 'test', path: 'test.js', opts: Lifetime.SCOPED }, 317 | { name: 'test2', path: 'test2.js', opts: null }, 318 | ]), 319 | require: jest.fn((path) => modules[path]), 320 | } 321 | 322 | loadModules(deps, 'anything', { 323 | resolverOptions: { 324 | lifetime: Lifetime.SINGLETON, 325 | }, 326 | }) 327 | 328 | expect(container.registrations.test.lifetime).toBe(Lifetime.SCOPED) 329 | expect(container.registrations.test2.lifetime).toBe(Lifetime.SINGLETON) 330 | }) 331 | 332 | it('supports array opts syntax with object', () => { 333 | const container = createContainer() 334 | const modules: any = { 335 | 'test.js': jest.fn(() => 42), 336 | 'test2.js': jest.fn(() => 42), 337 | } 338 | 339 | const deps = { 340 | container, 341 | listModules: jest.fn(() => [ 342 | { name: 'test', path: 'test.js', opts: { lifetime: Lifetime.SCOPED } }, 343 | { name: 'test2', path: 'test2.js', opts: null }, 344 | ]), 345 | require: jest.fn((path) => modules[path]), 346 | } 347 | 348 | loadModules(deps, 'anything', { 349 | resolverOptions: { 350 | lifetime: Lifetime.SINGLETON, 351 | }, 352 | }) 353 | 354 | expect(container.registrations.test.lifetime).toBe(Lifetime.SCOPED) 355 | expect(container.registrations.test2.lifetime).toBe(Lifetime.SINGLETON) 356 | }) 357 | 358 | it('supports passing in a default injectionMode', () => { 359 | const container = createContainer() 360 | const modules: any = { 361 | 'test.js': jest.fn(() => 42), 362 | 'test2.js': jest.fn(() => 42), 363 | } 364 | 365 | const deps = { 366 | container, 367 | listModules: jest.fn(() => [ 368 | { 369 | name: 'test', 370 | path: 'test.js', 371 | opts: { injectionMode: InjectionMode.PROXY }, 372 | }, 373 | { name: 'test2', path: 'test2.js', opts: null }, 374 | ]), 375 | require: jest.fn((path) => modules[path]), 376 | } 377 | 378 | loadModules(deps, 'anything', { 379 | resolverOptions: { 380 | injectionMode: InjectionMode.CLASSIC, 381 | }, 382 | }) 383 | 384 | expect( 385 | (container.registrations.test as BuildResolver).injectionMode, 386 | ).toBe(InjectionMode.PROXY) 387 | expect( 388 | (container.registrations.test2 as BuildResolver).injectionMode, 389 | ).toBe(InjectionMode.CLASSIC) 390 | }) 391 | 392 | describe('inline config via REGISTRATION symbol', () => { 393 | it('uses the inline config over anything else', () => { 394 | const container = createContainer() 395 | const test1Func = jest.fn(() => 42) 396 | ;(test1Func as any)[RESOLVER] = { 397 | injectionMode: InjectionMode.PROXY, 398 | } 399 | 400 | class Test2Class {} 401 | 402 | ;(Test2Class as any)[RESOLVER] = { 403 | lifetime: Lifetime.SCOPED, 404 | } 405 | 406 | class Test3Class {} 407 | 408 | ;(Test3Class as any)[RESOLVER] = { 409 | register: asValue, 410 | } 411 | 412 | const modules: any = { 413 | 'test.js': test1Func, 414 | 'test2.js': Test2Class, 415 | 'test3.js': Test3Class, 416 | } 417 | 418 | const deps = { 419 | container, 420 | listModules: jest.fn(() => [ 421 | { name: 'test', path: 'test.js', opts: null }, 422 | { name: 'test2', path: 'test2.js', opts: null }, 423 | { name: 'test3', path: 'test3.js', opts: null }, 424 | ]), 425 | require: jest.fn((path) => modules[path]), 426 | } 427 | 428 | loadModules(deps, 'anything', { 429 | resolverOptions: { 430 | injectionMode: InjectionMode.CLASSIC, 431 | }, 432 | }) 433 | 434 | expect(container.registrations.test.lifetime).toBe(Lifetime.TRANSIENT) 435 | expect( 436 | (container.registrations.test as BuildResolver).injectionMode, 437 | ).toBe(InjectionMode.PROXY) 438 | expect(container.registrations.test2.lifetime).toBe(Lifetime.SCOPED) 439 | expect( 440 | (container.registrations.test2 as BuildResolver).injectionMode, 441 | ).toBe(InjectionMode.CLASSIC) 442 | expect(container.resolve('test3')).toBe(Test3Class) 443 | }) 444 | 445 | it('allows setting a name to register as', () => { 446 | const container = createContainer() 447 | const test1Func = jest.fn(() => 42) 448 | ;(test1Func as any)[RESOLVER] = { 449 | name: 'awesome', 450 | lifetime: Lifetime.SINGLETON, 451 | } 452 | 453 | const test2Func = jest.fn(() => 42) 454 | const modules: any = { 455 | 'test.js': test1Func, 456 | 'test2.js': test2Func, 457 | } 458 | 459 | const deps = { 460 | container, 461 | listModules: jest.fn(() => [ 462 | { name: 'test', path: 'test.js', opts: null }, 463 | { name: 'test2', path: 'test2.js', opts: null }, 464 | ]), 465 | require: jest.fn((path) => modules[path]), 466 | } 467 | 468 | loadModules(deps, 'anything', { 469 | formatName: () => 'formatNameCalled', 470 | resolverOptions: { 471 | lifetime: Lifetime.SCOPED, 472 | }, 473 | }) 474 | 475 | expect(container.registrations.awesome.lifetime).toBe(Lifetime.SINGLETON) 476 | expect(container.registrations.formatNameCalled.lifetime).toBe( 477 | Lifetime.SCOPED, 478 | ) 479 | }) 480 | }) 481 | }) 482 | -------------------------------------------------------------------------------- /src/__tests__/local-injections.test.ts: -------------------------------------------------------------------------------- 1 | import { createContainer } from '../container' 2 | import { asClass, asFunction } from '../resolvers' 3 | import { InjectionMode } from '../injection-mode' 4 | 5 | class Test { 6 | value: any 7 | constructor({ value }: any) { 8 | this.value = value 9 | } 10 | } 11 | 12 | class TestClassic { 13 | test: any 14 | value: any 15 | constructor(test: any, value: any) { 16 | this.test = test 17 | this.value = value 18 | } 19 | } 20 | 21 | const makeTest = ({ value }: any) => ({ value, isTest: true }) 22 | const makeCLassicTest = (test: any, value: any) => ({ value, test }) 23 | 24 | const injector = () => ({ value: 42 }) 25 | 26 | describe('local injections', () => { 27 | it('invokes the injector and provides the result to the constructor', () => { 28 | const container = createContainer().register({ 29 | test: asClass(Test).inject(injector), 30 | testClassic: asClass(TestClassic).inject(injector).classic(), 31 | test2: asClass(Test), 32 | }) 33 | 34 | expect(container.cradle.test.value).toBe(42) 35 | 36 | expect(container.cradle.testClassic.test).toBeInstanceOf(Test) 37 | expect(container.cradle.testClassic.value).toBe(42) 38 | 39 | expect(() => container.cradle.test2.value).toThrowError( 40 | /Could not resolve 'value'/, 41 | ) 42 | }) 43 | 44 | it('supported by registerClass', () => { 45 | const container = createContainer().register({ 46 | test: asClass(Test, { injector }), 47 | testClassic: asClass(TestClassic, { 48 | injector, 49 | injectionMode: InjectionMode.CLASSIC, 50 | }), 51 | test2: asClass(Test), 52 | }) 53 | 54 | expect(container.cradle.test.value).toBe(42) 55 | 56 | expect(container.cradle.testClassic.test).toBeInstanceOf(Test) 57 | expect(container.cradle.testClassic.value).toBe(42) 58 | expect(() => container.cradle.test2.value).toThrowError( 59 | /Could not resolve 'value'/, 60 | ) 61 | }) 62 | 63 | it('supported by registerFunction', () => { 64 | const container = createContainer().register({ 65 | test: asFunction(makeTest, { injector }), 66 | testClassic: asFunction(makeCLassicTest, { 67 | injector, 68 | injectionMode: InjectionMode.CLASSIC, 69 | }), 70 | test2: asFunction(makeTest), 71 | }) 72 | 73 | expect(container.cradle.test.value).toBe(42) 74 | 75 | expect(container.cradle.testClassic.test.isTest).toBe(true) 76 | expect(container.cradle.testClassic.value).toBe(42) 77 | expect(() => container.cradle.test2.value).toThrowError( 78 | /Could not resolve 'value'/, 79 | ) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/__tests__/param-parser.bugs.test.ts: -------------------------------------------------------------------------------- 1 | import { parseParameterList } from '../param-parser' 2 | 3 | // https://github.com/jeffijoe/awilix/issues/130 4 | // 5 | // The bug was caused by the tokenizer trying to skip the expression at 6 | // the `=`, but it caused it to skip over the constructor. 7 | // The solution was to add a tokenizer flag to put it into "dumb" mode 8 | // when the parser is skipping to the constructor token. This disables 9 | // the "smart-skipping" of expressions. 10 | test('#130', () => { 11 | const code = ` 12 | class Service2 { 13 | /** 14 | * Comment out the return statement and parsing works again. 15 | * 16 | * @param {string} aParam 17 | */ 18 | static someStaticMethod(aParam) { 19 | return typeof aParam !== 'undefined' ? aParam : 'no param'; 20 | } 21 | 22 | /** 23 | * This constructor fails because of the static method above. 24 | * 25 | * @param {function} injectedService 26 | */ 27 | constructor(injectedService) { 28 | console.log(Service2.someStaticMethod(this)); 29 | injectedService(this); 30 | } 31 | } 32 | ` 33 | const params = parseParameterList(code) 34 | expect(params).toEqual([{ name: 'injectedService', optional: false }]) 35 | }) 36 | 37 | // https://github.com/jeffijoe/awilix/issues/164 38 | // 39 | // Caused by the `skipUntil` in the tokenizer trying to parse strings when it should just be 40 | // dumb and only care about scanning until the end of the line (single comment) or the end 41 | // of a block comment marker. 42 | test('#164', () => { 43 | const classCode = ` 44 | class TestClass { 45 | constructor(injectedService // ' 46 | 47 | ) {} 48 | } 49 | ` 50 | 51 | expect(parseParameterList(classCode)).toEqual([ 52 | { name: 'injectedService', optional: false }, 53 | ]) 54 | 55 | const funcCode = ` 56 | ( // " 57 | 58 | ) => {} 59 | ` 60 | 61 | expect(parseParameterList(funcCode)).toEqual([]) 62 | }) 63 | 64 | // https://github.com/jeffijoe/awilix/issues/390 65 | // 66 | // The paran-less arrow function path was being hit after having skipped all 67 | // tokens until finding the `constructor` token. 68 | // The problem was that the tokenizer is instructed to treat `foo.constructor` 69 | // as a single identifier to simplify looking for constructor tokens, but 70 | // it wasn't taking `foo?.constructor` into account since at the time optional 71 | // chaining was not part of the language at runtime. 72 | // The fix was to consider `?` as part of the identifier as well. 73 | // This would be problematic if TypeScript's optional parameter syntax `param?` 74 | // was visible in the stringified class code, but that gets transpiled away. 75 | describe('#390', () => { 76 | test('without a constructor', () => { 77 | const classCode = ` 78 | class TestClass { 79 | // Fails because of the two questions marks occurring with the constructor keyword. 80 | bar() { 81 | return this?.constructor?.name 82 | } 83 | 84 | clone() { 85 | return new this?.constructor() 86 | } 87 | 88 | clone2() { 89 | const constructor = this?.constructor 90 | return constructor.call(this) 91 | } 92 | }` 93 | 94 | expect(parseParameterList(classCode)).toEqual(null) 95 | }) 96 | 97 | test('with a constructor', () => { 98 | const classCode = ` 99 | class TestClass { 100 | // Fails because of the two questions marks occurring with the constructor keyword. 101 | bar() { 102 | return this?.constructor?.name 103 | } 104 | 105 | clone() { 106 | return new this?.constructor() 107 | } 108 | 109 | clone2() { 110 | const constructor = this?.constructor 111 | return constructor.call(this) 112 | } 113 | 114 | constructor(injectedService) {} 115 | }` 116 | 117 | expect(parseParameterList(classCode)).toEqual([ 118 | { 119 | name: 'injectedService', 120 | optional: false, 121 | }, 122 | ]) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/__tests__/param-parser.test.ts: -------------------------------------------------------------------------------- 1 | import { parseParameterList } from '../param-parser' 2 | 3 | describe('parseParameterList', () => { 4 | it('returns an empty array when invalid input is given', () => { 5 | expect(parseParameterList('')).toEqual([]) 6 | }) 7 | 8 | it('supports regular functions', () => { 9 | expect( 10 | parseParameterList( 11 | function hello(dep1: any, dep2: any) { 12 | /**/ 13 | }.toString(), 14 | ), 15 | ).toEqual([ 16 | { name: 'dep1', optional: false }, 17 | { name: 'dep2', optional: false }, 18 | ]) 19 | expect( 20 | parseParameterList( 21 | function (dep1: any, dep2: any) { 22 | /**/ 23 | }.toString(), 24 | ), 25 | ).toEqual([ 26 | { name: 'dep1', optional: false }, 27 | { name: 'dep2', optional: false }, 28 | ]) 29 | expect( 30 | parseParameterList( 31 | function (dep1: any, dep2: any, dep3: any) { 32 | /**/ 33 | }.toString(), 34 | ), 35 | ).toEqual([ 36 | { name: 'dep1', optional: false }, 37 | { name: 'dep2', optional: false }, 38 | { name: 'dep3', optional: false }, 39 | ]) 40 | expect( 41 | parseParameterList( 42 | function hello() { 43 | /**/ 44 | }.toString(), 45 | ), 46 | ).toEqual([]) 47 | expect( 48 | parseParameterList( 49 | function () { 50 | /**/ 51 | }.toString(), 52 | ), 53 | ).toEqual([]) 54 | }) 55 | 56 | it('supports regular functions with default params', () => { 57 | expect( 58 | parseParameterList( 59 | `function hello( 60 | dep1 = 'frick off ricky', 61 | dep2 = 'shut up randy, go eat some cheeseburgers' 62 | ) { 63 | }`, 64 | ), 65 | ).toEqual([ 66 | { name: 'dep1', optional: true }, 67 | { name: 'dep2', optional: true }, 68 | ]) 69 | }) 70 | 71 | it('supports arrow functions', () => { 72 | expect( 73 | parseParameterList( 74 | ((dep1: any, dep2: any) => { 75 | /**/ 76 | }).toString(), 77 | ), 78 | ).toEqual([ 79 | { name: 'dep1', optional: false }, 80 | { name: 'dep2', optional: false }, 81 | ]) 82 | expect( 83 | parseParameterList(((dep1: any, dep2: any) => 42).toString()), 84 | ).toEqual([ 85 | { name: 'dep1', optional: false }, 86 | { name: 'dep2', optional: false }, 87 | ]) 88 | expect( 89 | parseParameterList( 90 | ((dep1: any, dep2: any, dep3: any) => { 91 | /**/ 92 | }).toString(), 93 | ), 94 | ).toEqual([ 95 | { name: 'dep1', optional: false }, 96 | { name: 'dep2', optional: false }, 97 | { name: 'dep3', optional: false }, 98 | ]) 99 | expect( 100 | parseParameterList( 101 | (() => { 102 | /**/ 103 | }).toString(), 104 | ), 105 | ).toEqual([]) 106 | expect(parseParameterList((() => 42).toString())).toEqual([]) 107 | expect(parseParameterList(`dep1 => lol`)).toEqual([ 108 | { name: 'dep1', optional: false }, 109 | ]) 110 | }) 111 | 112 | it('supports arrow function with default params', () => { 113 | expect( 114 | parseParameterList( 115 | ((dep1 = 123, dep2 = 456) => { 116 | /**/ 117 | }).toString(), 118 | ), 119 | ).toEqual([ 120 | { name: 'dep1', optional: true }, 121 | { name: 'dep2', optional: true }, 122 | ]) 123 | expect( 124 | parseParameterList(((dep1 = 123, dep2 = 456) => 789).toString()), 125 | ).toEqual([ 126 | { name: 'dep1', optional: true }, 127 | { name: 'dep2', optional: true }, 128 | ]) 129 | }) 130 | 131 | it('supports class constructors', () => { 132 | class Test { 133 | dep1: any 134 | constructor(dep1: any, dep2: any) { 135 | this.dep1 = dep1 136 | } 137 | 138 | method() { 139 | /**/ 140 | } 141 | } 142 | 143 | expect(parseParameterList(Test.prototype.constructor.toString())).toEqual([ 144 | { name: 'dep1', optional: false }, 145 | { name: 'dep2', optional: false }, 146 | ]) 147 | }) 148 | 149 | it('supports class constructors with a bunch of other shit before it', () => { 150 | const cls = ` 151 | class Rofl { 152 | @dec(1, 3, { obj: value }) 153 | someMethod(yeah, boi) { 154 | const ctor = Rofl.prototype.constructor.call(this, 2) 155 | return 'heyo' + \` gimme $\{mayo + 10}\` 156 | } 157 | 158 | static prop = '123' 159 | @decorated stuff = func(1, 'two') 160 | 161 | constructor(dep1, dep2 = 123) { 162 | 163 | } 164 | } 165 | ` 166 | expect(parseParameterList(cls)).toEqual([ 167 | { name: 'dep1', optional: false }, 168 | { name: 'dep2', optional: true }, 169 | ]) 170 | }) 171 | 172 | it('returns null when no constructor is defined', () => { 173 | class Test { 174 | dep1: any 175 | 176 | method() { 177 | /**/ 178 | } 179 | } 180 | 181 | expect(parseParameterList(Test.prototype.constructor.toString())).toBe(null) 182 | 183 | expect(parseParameterList('class Lol extends Wee {}')).toBe(null) 184 | }) 185 | 186 | it('supports carriage return in function signature', () => { 187 | expect(parseParameterList(`function (\r\ndep1,\r\ndep2\r\n) {}`)).toEqual([ 188 | { name: 'dep1', optional: false }, 189 | { name: 'dep2', optional: false }, 190 | ]) 191 | }) 192 | 193 | it('supports weird formatting', () => { 194 | expect( 195 | parseParameterList(`function( dep1 \n,\r\n dep2 = 123 \r\n) {}`), 196 | ).toEqual([ 197 | { name: 'dep1', optional: false }, 198 | { name: 'dep2', optional: true }, 199 | ]) 200 | }) 201 | 202 | it('supports the problem posted in issue #30', () => { 203 | const fn = function (a: any) { 204 | return {} 205 | } 206 | 207 | function fn2(a: any) { 208 | return {} 209 | } 210 | 211 | expect(parseParameterList(fn.toString())).toEqual([ 212 | { name: 'a', optional: false }, 213 | ]) 214 | expect(parseParameterList(fn2.toString())).toEqual([ 215 | { name: 'a', optional: false }, 216 | ]) 217 | }) 218 | 219 | it('skips line comments', () => { 220 | const cls = ` 221 | class UserController { 222 | // We are using constructor injection. 223 | constructor(userService) { 224 | // Save a reference to our dependency. 225 | this.userService = userService; 226 | } 227 | 228 | // imagine ctx is our HTTP request context... 229 | getUser(ctx) { 230 | return this.userService.getUser(ctx.params.id) 231 | } 232 | } 233 | ` 234 | 235 | expect(parseParameterList(cls)).toEqual([ 236 | { name: 'userService', optional: false }, 237 | ]) 238 | }) 239 | 240 | it('skips block comments', () => { 241 | const cls = ` 242 | class UserController { 243 | /* 244 | We are using constructor injection. 245 | */ 246 | constructor(userService) { 247 | // Save a reference to our dependency. 248 | this.userService = userService; 249 | } 250 | 251 | // imagine ctx is our HTTP request context... 252 | getUser(ctx) { 253 | return this.userService.getUser(ctx.params.id) 254 | } 255 | } 256 | ` 257 | 258 | expect(parseParameterList(cls)).toEqual([ 259 | { name: 'userService', optional: false }, 260 | ]) 261 | }) 262 | 263 | it('does not get confused when referencing constructor elsewhere', () => { 264 | const cls = ` 265 | class UserController { 266 | @decorator(MyThing.prototype.constructor) 267 | someField 268 | 269 | /* 270 | We are using constructor injection. 271 | */ 272 | constructor(userService) { 273 | // Save a reference to our dependency. 274 | this.userService = userService; 275 | } 276 | 277 | // imagine ctx is our HTTP request context... 278 | getUser(ctx) { 279 | return this.userService.getUser(ctx.params.id) 280 | } 281 | } 282 | ` 283 | 284 | expect(parseParameterList(cls)).toEqual([ 285 | { name: 'userService', optional: false }, 286 | ]) 287 | }) 288 | 289 | it('skips async keyword', () => { 290 | expect(parseParameterList(`async function (first, second) {}`)).toEqual([ 291 | { 292 | name: 'first', 293 | optional: false, 294 | }, 295 | { 296 | name: 'second', 297 | optional: false, 298 | }, 299 | ]) 300 | expect(parseParameterList(`async (first, second) => {}`)).toEqual([ 301 | { 302 | name: 'first', 303 | optional: false, 304 | }, 305 | { 306 | name: 'second', 307 | optional: false, 308 | }, 309 | ]) 310 | expect(parseParameterList(`async () => {}`)).toEqual([]) 311 | expect(parseParameterList(`async => {}`)).toEqual([ 312 | { 313 | name: 'async', 314 | optional: false, 315 | }, 316 | ]) 317 | }) 318 | 319 | it('skips generator star', () => { 320 | expect(parseParameterList(`async function* (first, second) {}`)).toEqual([ 321 | { 322 | name: 'first', 323 | optional: false, 324 | }, 325 | { 326 | name: 'second', 327 | optional: false, 328 | }, 329 | ]) 330 | expect(parseParameterList(`async function *(first, second) {}`)).toEqual([ 331 | { 332 | name: 'first', 333 | optional: false, 334 | }, 335 | { 336 | name: 'second', 337 | optional: false, 338 | }, 339 | ]) 340 | }) 341 | }) 342 | -------------------------------------------------------------------------------- /src/__tests__/resolvers.test.ts: -------------------------------------------------------------------------------- 1 | import { throws } from 'smid' 2 | import { asValue, asFunction, asClass } from '../resolvers' 3 | import { createContainer, AwilixContainer } from '../container' 4 | import { Lifetime } from '../lifetime' 5 | import { InjectionMode } from '../injection-mode' 6 | import { AwilixTypeError } from '../errors' 7 | import { aliasTo } from '../awilix' 8 | 9 | const testFn = () => 1337 10 | const depsFn = (testClass: any) => testClass 11 | const multiDeps = (testClass: any, needsCradle: any) => { 12 | return { testClass, needsCradle } 13 | } 14 | 15 | class TestClass { 16 | dispose() { 17 | /**/ 18 | } 19 | } 20 | class WithDeps { 21 | testClass: any 22 | constructor(testClass: any) { 23 | this.testClass = testClass 24 | } 25 | } 26 | class NeedsCradle { 27 | testClass: any 28 | constructor(cradle: any) { 29 | this.testClass = cradle.testClass 30 | } 31 | } 32 | class MultipleDeps { 33 | testClass: any 34 | needsCradle: any 35 | constructor(testClass: any, needsCradle: any) { 36 | this.testClass = testClass 37 | this.needsCradle = needsCradle 38 | } 39 | } 40 | 41 | describe('registrations', () => { 42 | let container: AwilixContainer 43 | beforeEach(function () { 44 | container = createContainer() 45 | }) 46 | 47 | describe('asValue', () => { 48 | it('creates a registration with a resolve method', () => { 49 | expect(typeof asValue(42).resolve).toBe('function') 50 | }) 51 | }) 52 | 53 | describe('asFunction', () => { 54 | it('creates a registration with a resolve method', () => { 55 | expect(typeof asFunction(testFn).resolve).toBe('function') 56 | }) 57 | 58 | it('defaults to transient', () => { 59 | const testSpy = jest.fn(testFn) 60 | const reg = asFunction(() => testSpy()) 61 | reg.resolve(container) 62 | reg.resolve(container) 63 | 64 | expect(testSpy).toHaveBeenCalledTimes(2) 65 | }) 66 | 67 | it('manually resolves function dependencies', () => { 68 | container.register({ 69 | testClass: asClass(TestClass).classic(), 70 | }) 71 | const reg = asFunction(depsFn).classic() 72 | const result = reg.resolve(container) 73 | expect(typeof reg.resolve).toBe('function') 74 | expect(result).toBeInstanceOf(TestClass) 75 | }) 76 | 77 | it('manually resolves multiple function dependencies', () => { 78 | container.register({ 79 | testClass: asClass(TestClass, { 80 | injectionMode: InjectionMode.CLASSIC, 81 | }), 82 | needsCradle: asClass(NeedsCradle).proxy(), 83 | }) 84 | const reg = asFunction(multiDeps, { 85 | injectionMode: InjectionMode.CLASSIC, 86 | }) 87 | const result = reg.resolve(container) 88 | expect(typeof reg.resolve).toBe('function') 89 | expect(result.testClass).toBeInstanceOf(TestClass) 90 | expect(result.needsCradle).toBeInstanceOf(NeedsCradle) 91 | }) 92 | 93 | it('supports arrow functions', () => { 94 | const arrowWithParen = (dep: any) => dep 95 | const arrowWithoutParen: (arg: any) => any = (dep) => dep 96 | container.register({ 97 | withParen: asFunction(arrowWithParen).classic(), 98 | withoutParen: asFunction(arrowWithoutParen).classic(), 99 | dep: asValue(42), 100 | }) 101 | 102 | expect(container.resolve('withParen')).toBe(42) 103 | expect(container.resolve('withoutParen')).toBe(42) 104 | }) 105 | 106 | it('throws AwilixTypeError when given null', () => { 107 | const err = throws(() => asFunction(null as any)) 108 | expect(err).toBeInstanceOf(AwilixTypeError) 109 | }) 110 | }) 111 | 112 | describe('asClass', () => { 113 | it('creates a registration with a resolve method', () => { 114 | expect(typeof asClass(TestClass).resolve).toBe('function') 115 | }) 116 | 117 | it('resolves the class by newing it up', () => { 118 | const reg = asClass(TestClass) 119 | const result = reg.resolve(container) 120 | expect(result).toBeInstanceOf(TestClass) 121 | }) 122 | 123 | it('resolves dependencies manually', () => { 124 | container.register({ 125 | testClass: asClass(TestClass), 126 | }) 127 | const withDepsReg = asClass(WithDeps).classic() 128 | const result = withDepsReg.resolve(container) 129 | expect(result).toBeInstanceOf(WithDeps) 130 | expect(result.testClass).toBeInstanceOf(TestClass) 131 | }) 132 | 133 | it('resolves single dependency as cradle', () => { 134 | container.register({ 135 | testClass: asClass(TestClass), 136 | }) 137 | const reg = asClass(NeedsCradle).proxy() 138 | const result = reg.resolve(container) 139 | expect(result).toBeInstanceOf(NeedsCradle) 140 | expect(result.testClass).toBeInstanceOf(TestClass) 141 | }) 142 | 143 | it('resolves multiple dependencies manually', () => { 144 | container.register({ 145 | testClass: asClass(TestClass), 146 | needsCradle: asClass(NeedsCradle), 147 | }) 148 | const reg = asClass(MultipleDeps, { 149 | injectionMode: InjectionMode.CLASSIC, 150 | }) 151 | const result = reg.resolve(container) 152 | expect(result).toBeInstanceOf(MultipleDeps) 153 | expect(result.testClass).toBeInstanceOf(TestClass) 154 | expect(result.needsCradle).toBeInstanceOf(NeedsCradle) 155 | }) 156 | 157 | it('throws an Error when given null', () => { 158 | const err = throws(() => asClass(null!)) 159 | expect(err).toBeInstanceOf(AwilixTypeError) 160 | }) 161 | }) 162 | 163 | describe('asClass and asFunction fluid interface', () => { 164 | it('supports all lifetimes and returns the object itself', () => { 165 | const subjects = [ 166 | asClass(TestClass), 167 | asFunction(() => { 168 | return new TestClass() 169 | }), 170 | ] 171 | 172 | subjects.forEach((x) => { 173 | let retVal = x.setLifetime(Lifetime.SCOPED) 174 | expect(retVal.lifetime).toBe(Lifetime.SCOPED) 175 | 176 | retVal = retVal.transient() 177 | expect(retVal.lifetime).toBe(Lifetime.TRANSIENT) 178 | 179 | retVal = retVal.singleton() 180 | expect(retVal.lifetime).toBe(Lifetime.SINGLETON) 181 | 182 | retVal = retVal.scoped() 183 | expect(retVal.lifetime).toBe(Lifetime.SCOPED) 184 | 185 | const d = (t: TestClass) => { 186 | return t.dispose() 187 | } 188 | retVal = retVal.disposer(d) 189 | expect(retVal.dispose).toBe(d) 190 | }) 191 | }) 192 | 193 | it('supports inject()', () => { 194 | const subjects = [ 195 | asClass(TestClass), 196 | asFunction(() => { 197 | /**/ 198 | }), 199 | ] 200 | 201 | subjects.forEach((x) => { 202 | const retVal = x.inject(() => ({ value: 42 })) 203 | expect(retVal.injector).toBeDefined() 204 | }) 205 | }) 206 | }) 207 | 208 | describe('aliasTo', () => { 209 | it('returns the string aliased dependency', () => { 210 | container.register({ val: asValue(123), aliasVal: aliasTo('val') }) 211 | expect(container.resolve('aliasVal')).toBe(123) 212 | }) 213 | 214 | it('returns the symbol aliased dependency', () => { 215 | const symbol = Symbol() 216 | container.register({ [symbol]: asValue(123), aliasVal: aliasTo(symbol) }) 217 | expect(container.resolve('aliasVal')).toBe(123) 218 | }) 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /src/__tests__/rollup.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | const cjs = require('../../lib/awilix') 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import * as es from '../../lib/awilix.module.mjs' 6 | const umd = require('../../lib/awilix.umd') 7 | const browser = require('../../lib/awilix.browser.mjs') 8 | 9 | describe('rollup artifacts', () => { 10 | it('works', () => { 11 | expect(getValue(cjs)).toBe(123) 12 | expect(getValue(es)).toBe(123) 13 | expect(getValue(umd)).toBe(123) 14 | expect(getValue(browser)).toBe(123) 15 | }) 16 | 17 | describe('non-browser builds', () => { 18 | it('support loadModules', async () => { 19 | const answer = 'The answer to "universe" is: 42' 20 | expect(await runLoadModules(cjs).getTheAnswer('universe')).toBe(answer) 21 | expect(await runLoadModules(es).getTheAnswer('universe')).toBe(answer) 22 | }) 23 | }) 24 | 25 | describe('browser build', () => { 26 | it('does not support loadModules', () => { 27 | expect(() => runLoadModules(umd)).toThrow(/browser/) 28 | expect(() => runLoadModules(browser)).toThrow(/browser/) 29 | }) 30 | }) 31 | }) 32 | 33 | function getValue(pkg: any) { 34 | return pkg 35 | .createContainer() 36 | .register({ 37 | value: pkg.asValue(123), 38 | fn: pkg.asFunction(({ value }: any) => value), 39 | }) 40 | .resolve('fn') 41 | } 42 | 43 | function runLoadModules(pkg: any) { 44 | return pkg 45 | .createContainer() 46 | .register({ conn: pkg.asValue({}) }) 47 | .loadModules( 48 | [ 49 | ['services/*.js', pkg.Lifetime.SCOPED], 50 | ['repositories/*.js', { injector: () => ({ timeout: 10 }) }], 51 | ], 52 | { 53 | cwd: path.join(process.cwd(), 'src/__tests__/fixture'), 54 | }, 55 | ) 56 | .resolve('mainService') 57 | } 58 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | flatten, 3 | isClass, 4 | isFunction, 5 | last, 6 | nameValueToObject, 7 | uniq, 8 | } from '../utils' 9 | 10 | describe('flatten', () => { 11 | it('flattens the array', () => { 12 | expect(flatten([[1, 2], [3]])).toEqual([1, 2, 3]) 13 | }) 14 | }) 15 | 16 | describe('isClass', () => { 17 | it('returns true for function that start with a capital letter', () => { 18 | expect( 19 | isClass(function Stuff() { 20 | /**/ 21 | }), 22 | ).toBe(true) 23 | }) 24 | 25 | it('returns false for function that do not start with a capital letter', () => { 26 | expect( 27 | isClass(function stuff() { 28 | /**/ 29 | }), 30 | ).toBe(false) 31 | }) 32 | 33 | it('returns false for other stuff', () => { 34 | expect(isClass('hello' as any)).toBe(false) 35 | expect(isClass(123 as any)).toBe(false) 36 | }) 37 | 38 | it('returns true for classes', () => { 39 | expect(isClass(class Stuff {})).toBe(true) 40 | expect(isClass(class {})).toBe(true) 41 | }) 42 | }) 43 | 44 | describe('isFunction', () => { 45 | it('returns true when the value is a function', () => { 46 | expect( 47 | isFunction(() => { 48 | /**/ 49 | }), 50 | ).toBe(true) 51 | expect( 52 | isFunction(function () { 53 | /**/ 54 | }), 55 | ).toBe(true) 56 | expect(isFunction(class {})).toBe(true) 57 | }) 58 | 59 | it('returns false when the value is not a function', () => { 60 | expect(isFunction(true)).toBe(false) 61 | expect(isFunction(false)).toBe(false) 62 | expect(isFunction('')).toBe(false) 63 | expect(isFunction('string')).toBe(false) 64 | expect(isFunction(123)).toBe(false) 65 | }) 66 | }) 67 | 68 | describe('last', () => { 69 | it('returns the last element', () => { 70 | expect(last([1, 2, 3])).toBe(3) 71 | }) 72 | }) 73 | 74 | describe('nameValueToObject', () => { 75 | it('converts 2 params to 1', () => { 76 | const result = nameValueToObject('hello', 'world') 77 | expect(typeof result).toBe('object') 78 | expect(result.hello).toBe('world') 79 | }) 80 | 81 | it('uses the object if passed', () => { 82 | const input = { hello: 'world' } 83 | const result = nameValueToObject(input) 84 | 85 | expect(typeof result).toBe('object') 86 | expect(result.hello).toBe('world') 87 | }) 88 | }) 89 | 90 | describe('uniq', () => { 91 | it('returns unique items', () => { 92 | expect(uniq(['hello', 'world', 'i', 'say', 'hello', 'world'])).toEqual([ 93 | 'hello', 94 | 'world', 95 | 'i', 96 | 'say', 97 | ]) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/awilix.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type AwilixContainer, 3 | type ContainerOptions, 4 | type CacheEntry, 5 | type ClassOrFunctionReturning, 6 | type FunctionReturning, 7 | type NameAndRegistrationPair, 8 | type RegistrationHash, 9 | type ResolveOptions, 10 | createContainer, 11 | } from './container' 12 | export { 13 | AwilixError, 14 | AwilixRegistrationError, 15 | AwilixResolutionError, 16 | AwilixTypeError, 17 | } from './errors' 18 | export { InjectionMode, type InjectionModeType } from './injection-mode' 19 | export { Lifetime, type LifetimeType } from './lifetime' 20 | export { 21 | type GlobWithOptions, 22 | type ListModulesOptions, 23 | type ModuleDescriptor, 24 | listModules, 25 | } from './list-modules' 26 | export { 27 | type BuildResolverOptions, 28 | type Disposer, 29 | type InjectorFunction, 30 | type Resolver, 31 | type ResolverOptions, 32 | type BuildResolver, 33 | type Constructor, 34 | type DisposableResolver, 35 | type DisposableResolverOptions, 36 | RESOLVER, 37 | aliasTo, 38 | asClass, 39 | asFunction, 40 | asValue, 41 | createBuildResolver, 42 | createDisposableResolver, 43 | } from './resolvers' 44 | export { isClass, isFunction } from './utils' 45 | -------------------------------------------------------------------------------- /src/container.ts: -------------------------------------------------------------------------------- 1 | import * as util from 'util' 2 | import { 3 | AwilixRegistrationError, 4 | AwilixResolutionError, 5 | AwilixTypeError, 6 | } from './errors' 7 | import { InjectionMode, InjectionModeType } from './injection-mode' 8 | import { Lifetime, LifetimeType, isLifetimeLonger } from './lifetime' 9 | import { GlobWithOptions, listModules } from './list-modules' 10 | import { importModule } from './load-module-native.js' 11 | import { 12 | LoadModulesOptions, 13 | LoadModulesResult, 14 | loadModules as realLoadModules, 15 | } from './load-modules' 16 | import { 17 | BuildResolverOptions, 18 | Constructor, 19 | DisposableResolver, 20 | Resolver, 21 | asClass, 22 | asFunction, 23 | } from './resolvers' 24 | import { isClass, last, nameValueToObject } from './utils' 25 | 26 | /** 27 | * The container returned from createContainer has some methods and properties. 28 | * @interface AwilixContainer 29 | */ 30 | export interface AwilixContainer { 31 | /** 32 | * Options the container was configured with. 33 | */ 34 | options: ContainerOptions 35 | /** 36 | * The proxy injected when using `PROXY` injection mode. 37 | * Can be used as-is. 38 | */ 39 | readonly cradle: Cradle 40 | /** 41 | * Getter for the rolled up registrations that merges the container family tree. 42 | */ 43 | readonly registrations: RegistrationHash 44 | /** 45 | * Resolved modules cache. 46 | */ 47 | readonly cache: Map 48 | /** 49 | * Creates a scoped container with this one as the parent. 50 | */ 51 | createScope(): AwilixContainer 52 | /** 53 | * Used by `util.inspect`. 54 | */ 55 | inspect(depth: number, opts?: any): string 56 | /** 57 | * Binds `lib/loadModules` to this container, and provides 58 | * real implementations of it's dependencies. 59 | * 60 | * Additionally, any modules using the `dependsOn` API 61 | * will be resolved. 62 | * 63 | * @see src/load-modules.ts documentation. 64 | */ 65 | loadModules( 66 | globPatterns: Array, 67 | options?: LoadModulesOptions, 68 | ): ESM extends false ? this : Promise 69 | 70 | /** 71 | * Adds a single registration that using a pre-constructed resolver. 72 | */ 73 | register(name: string | symbol, registration: Resolver): this 74 | /** 75 | * Pairs resolvers to registration names and registers them. 76 | */ 77 | register(nameAndRegistrationPair: NameAndRegistrationPair): this 78 | /** 79 | * Resolves the registration with the given name. 80 | * 81 | * @param {string} name 82 | * The name of the registration to resolve. 83 | * 84 | * @return {*} 85 | * Whatever was resolved. 86 | */ 87 | resolve( 88 | name: K, 89 | resolveOptions?: ResolveOptions, 90 | ): Cradle[K] 91 | /** 92 | * Resolves the registration with the given name. 93 | * 94 | * @param {string} name 95 | * The name of the registration to resolve. 96 | * 97 | * @return {*} 98 | * Whatever was resolved. 99 | */ 100 | resolve(name: string | symbol, resolveOptions?: ResolveOptions): T 101 | /** 102 | * Checks if the registration with the given name exists. 103 | * 104 | * @param {string | symbol} name 105 | * The name of the registration to resolve. 106 | * 107 | * @return {boolean} 108 | * Whether or not the registration exists. 109 | */ 110 | hasRegistration(name: string | symbol): boolean 111 | /** 112 | * Recursively gets a registration by name if it exists in the 113 | * current container or any of its' parents. 114 | * 115 | * @param name {string | symbol} The registration name. 116 | */ 117 | getRegistration(name: K): Resolver | null 118 | /** 119 | * Recursively gets a registration by name if it exists in the 120 | * current container or any of its' parents. 121 | * 122 | * @param name {string | symbol} The registration name. 123 | */ 124 | getRegistration(name: string | symbol): Resolver | null 125 | /** 126 | * Given a resolver, class or function, builds it up and returns it. 127 | * Does not cache it, this means that any lifetime configured in case of passing 128 | * a resolver will not be used. 129 | * 130 | * @param {Resolver|Class|Function} targetOrResolver 131 | * @param {ResolverOptions} opts 132 | */ 133 | build( 134 | targetOrResolver: ClassOrFunctionReturning | Resolver, 135 | opts?: BuildResolverOptions, 136 | ): T 137 | /** 138 | * Disposes this container and it's children, calling the disposer 139 | * on all disposable registrations and clearing the cache. 140 | * Only applies to registrations with `SCOPED` or `SINGLETON` lifetime. 141 | */ 142 | dispose(): Promise 143 | } 144 | 145 | /** 146 | * Optional resolve options. 147 | */ 148 | export interface ResolveOptions { 149 | /** 150 | * If `true` and `resolve` cannot find the requested dependency, 151 | * returns `undefined` rather than throwing an error. 152 | */ 153 | allowUnregistered?: boolean 154 | } 155 | 156 | /** 157 | * Cache entry. 158 | */ 159 | export interface CacheEntry { 160 | /** 161 | * The resolver that resolved the value. 162 | */ 163 | resolver: Resolver 164 | /** 165 | * The resolved value. 166 | */ 167 | value: T 168 | } 169 | 170 | /** 171 | * Register a Registration 172 | * @interface NameAndRegistrationPair 173 | */ 174 | export type NameAndRegistrationPair = { 175 | [U in keyof T]?: Resolver 176 | } 177 | 178 | /** 179 | * Function that returns T. 180 | */ 181 | export type FunctionReturning = (...args: Array) => T 182 | 183 | /** 184 | * A class or function returning T. 185 | */ 186 | export type ClassOrFunctionReturning = FunctionReturning | Constructor 187 | 188 | /** 189 | * The options for the createContainer function. 190 | */ 191 | export interface ContainerOptions { 192 | require?: (id: string) => any 193 | injectionMode?: InjectionModeType 194 | strict?: boolean 195 | } 196 | 197 | /** 198 | * Contains a hash of registrations where the name is the key. 199 | */ 200 | export type RegistrationHash = Record> 201 | 202 | export type ResolutionStack = Array<{ 203 | name: string | symbol 204 | lifetime: LifetimeType 205 | }> 206 | 207 | /** 208 | * Family tree symbol. 209 | */ 210 | const FAMILY_TREE = Symbol('familyTree') 211 | 212 | /** 213 | * Roll Up Registrations symbol. 214 | */ 215 | const ROLL_UP_REGISTRATIONS = Symbol('rollUpRegistrations') 216 | 217 | /** 218 | * The string representation when calling toString. 219 | */ 220 | const CRADLE_STRING_TAG = 'AwilixContainerCradle' 221 | 222 | /** 223 | * Creates an Awilix container instance. 224 | * 225 | * @param {Function} options.require The require function to use. Defaults to require. 226 | * 227 | * @param {string} options.injectionMode The mode used by the container to resolve dependencies. 228 | * Defaults to 'Proxy'. 229 | * 230 | * @param {boolean} options.strict True if the container should run in strict mode with additional 231 | * validation for resolver configuration correctness. Defaults to false. 232 | * 233 | * @return {AwilixContainer} The container. 234 | */ 235 | export function createContainer( 236 | options: ContainerOptions = {}, 237 | ): AwilixContainer { 238 | return createContainerInternal(options) 239 | } 240 | 241 | function createContainerInternal< 242 | T extends object = any, 243 | U extends object = any, 244 | >( 245 | options: ContainerOptions, 246 | parentContainer?: AwilixContainer, 247 | parentResolutionStack?: ResolutionStack, 248 | ): AwilixContainer { 249 | options = { 250 | injectionMode: InjectionMode.PROXY, 251 | strict: false, 252 | ...options, 253 | } 254 | 255 | /** 256 | * Tracks the names and lifetimes of the modules being resolved. Used to detect circular 257 | * dependencies and, in strict mode, lifetime leakage issues. 258 | */ 259 | const resolutionStack: ResolutionStack = parentResolutionStack ?? [] 260 | 261 | // Internal registration store for this container. 262 | const registrations: RegistrationHash = {} 263 | 264 | /** 265 | * The `Proxy` that is passed to functions so they can resolve their dependencies without 266 | * knowing where they come from. I call it the "cradle" because 267 | * it is where registered things come to life at resolution-time. 268 | */ 269 | const cradle = new Proxy( 270 | { 271 | [util.inspect.custom]: toStringRepresentationFn, 272 | }, 273 | { 274 | /** 275 | * The `get` handler is invoked whenever a get-call for `container.cradle.*` is made. 276 | * 277 | * @param {object} _target 278 | * The proxy target. Irrelevant. 279 | * 280 | * @param {string} name 281 | * The property name. 282 | * 283 | * @return {*} 284 | * Whatever the resolve call returns. 285 | */ 286 | get: (_target: object, name: string): any => resolve(name), 287 | 288 | /** 289 | * Setting things on the cradle throws an error. 290 | * 291 | * @param {object} target 292 | * @param {string} name 293 | */ 294 | set: (_target, name: string) => { 295 | throw new Error( 296 | `Attempted setting property "${ 297 | name as any 298 | }" on container cradle - this is not allowed.`, 299 | ) 300 | }, 301 | 302 | /** 303 | * Used for `Object.keys`. 304 | */ 305 | ownKeys() { 306 | return Array.from(cradle as any) 307 | }, 308 | 309 | /** 310 | * Used for `Object.keys`. 311 | */ 312 | getOwnPropertyDescriptor(target, key) { 313 | const regs = rollUpRegistrations() 314 | if (Object.getOwnPropertyDescriptor(regs, key)) { 315 | return { 316 | enumerable: true, 317 | configurable: true, 318 | } 319 | } 320 | 321 | return undefined 322 | }, 323 | }, 324 | ) as T 325 | 326 | // The container being exposed. 327 | const container = { 328 | options, 329 | cradle, 330 | inspect, 331 | cache: new Map(), 332 | loadModules, 333 | createScope, 334 | register: register as any, 335 | build, 336 | resolve, 337 | hasRegistration, 338 | dispose, 339 | getRegistration, 340 | [util.inspect.custom]: inspect, 341 | [ROLL_UP_REGISTRATIONS!]: rollUpRegistrations, 342 | get registrations() { 343 | return rollUpRegistrations() 344 | }, 345 | } 346 | 347 | // Track the family tree. 348 | const familyTree: Array = parentContainer 349 | ? [container].concat((parentContainer as any)[FAMILY_TREE]) 350 | : [container] 351 | 352 | // Save it so we can access it from a scoped container. 353 | ;(container as any)[FAMILY_TREE] = familyTree 354 | 355 | // We need a reference to the root container, 356 | // so we can retrieve and store singletons. 357 | const rootContainer = last(familyTree) 358 | 359 | return container 360 | 361 | /** 362 | * Used by util.inspect (which is used by console.log). 363 | */ 364 | function inspect(): string { 365 | return `[AwilixContainer (${ 366 | parentContainer ? 'scoped, ' : '' 367 | }registrations: ${Object.keys(container.registrations).length})]` 368 | } 369 | 370 | /** 371 | * Rolls up registrations from the family tree. 372 | * 373 | * This can get pretty expensive. Only used when 374 | * iterating the cradle proxy, which is not something 375 | * that should be done in day-to-day use, mostly for debugging. 376 | * 377 | * @param {boolean} bustCache 378 | * Forces a recomputation. 379 | * 380 | * @return {object} 381 | * The merged registrations object. 382 | */ 383 | function rollUpRegistrations(): RegistrationHash { 384 | return { 385 | ...(parentContainer && (parentContainer as any)[ROLL_UP_REGISTRATIONS]()), 386 | ...registrations, 387 | } 388 | } 389 | 390 | /** 391 | * Used for providing an iterator to the cradle. 392 | */ 393 | function* cradleIterator() { 394 | const registrations = rollUpRegistrations() 395 | for (const registrationName in registrations) { 396 | yield registrationName 397 | } 398 | } 399 | 400 | /** 401 | * Creates a scoped container. 402 | * 403 | * @return {object} 404 | * The scoped container. 405 | */ 406 | function createScope

(): AwilixContainer

{ 407 | return createContainerInternal( 408 | options, 409 | container as AwilixContainer, 410 | resolutionStack, 411 | ) 412 | } 413 | 414 | /** 415 | * Adds a registration for a resolver. 416 | */ 417 | function register(arg1: any, arg2: any): AwilixContainer { 418 | const obj = nameValueToObject(arg1, arg2) 419 | const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)] 420 | 421 | for (const key of keys) { 422 | const resolver = obj[key as any] as Resolver 423 | // If strict mode is enabled, check to ensure we are not registering a singleton on a non-root 424 | // container. 425 | if (options.strict && resolver.lifetime === Lifetime.SINGLETON) { 426 | if (parentContainer) { 427 | throw new AwilixRegistrationError( 428 | key, 429 | 'Cannot register a singleton on a scoped container.', 430 | ) 431 | } 432 | } 433 | 434 | registrations[key as any] = resolver 435 | } 436 | 437 | return container 438 | } 439 | 440 | /** 441 | * Returned to `util.inspect` and Symbol.toStringTag when attempting to resolve 442 | * a custom inspector function on the cradle. 443 | */ 444 | function toStringRepresentationFn() { 445 | return Object.prototype.toString.call(cradle) 446 | } 447 | 448 | /** 449 | * Recursively gets a registration by name if it exists in the 450 | * current container or any of its' parents. 451 | * 452 | * @param name {string | symbol} The registration name. 453 | */ 454 | function getRegistration(name: string | symbol) { 455 | const resolver = registrations[name] 456 | if (resolver) { 457 | return resolver 458 | } 459 | 460 | if (parentContainer) { 461 | return parentContainer.getRegistration(name) 462 | } 463 | 464 | return null 465 | } 466 | 467 | /** 468 | * Resolves the registration with the given name. 469 | * 470 | * @param {string | symbol} name 471 | * The name of the registration to resolve. 472 | * 473 | * @param {ResolveOptions} resolveOpts 474 | * The resolve options. 475 | * 476 | * @return {any} 477 | * Whatever was resolved. 478 | */ 479 | function resolve(name: string | symbol, resolveOpts?: ResolveOptions): any { 480 | resolveOpts = resolveOpts || {} 481 | 482 | try { 483 | // Grab the registration by name. 484 | const resolver = getRegistration(name) 485 | if (resolutionStack.some(({ name: parentName }) => parentName === name)) { 486 | throw new AwilixResolutionError( 487 | name, 488 | resolutionStack, 489 | 'Cyclic dependencies detected.', 490 | ) 491 | } 492 | 493 | // Used in JSON.stringify. 494 | if (name === 'toJSON') { 495 | return toStringRepresentationFn 496 | } 497 | 498 | // Used in console.log. 499 | if (name === 'constructor') { 500 | return createContainer 501 | } 502 | 503 | if (!resolver) { 504 | // Checks for some edge cases. 505 | switch (name) { 506 | // The following checks ensure that console.log on the cradle does not 507 | // throw an error (issue #7). 508 | case util.inspect.custom: 509 | case 'inspect': 510 | case 'toString': 511 | return toStringRepresentationFn 512 | case Symbol.toStringTag: 513 | return CRADLE_STRING_TAG 514 | // Edge case: Promise unwrapping will look for a "then" property and attempt to call it. 515 | // Return undefined so that we won't cause a resolution error. (issue #109) 516 | case 'then': 517 | return undefined 518 | // When using `Array.from` or spreading the cradle, this will 519 | // return the registration names. 520 | case Symbol.iterator: 521 | return cradleIterator 522 | } 523 | 524 | if (resolveOpts.allowUnregistered) { 525 | return undefined 526 | } 527 | 528 | throw new AwilixResolutionError(name, resolutionStack) 529 | } 530 | 531 | const lifetime = resolver.lifetime || Lifetime.TRANSIENT 532 | 533 | // if we are running in strict mode, this resolver is not explicitly marked leak-safe, and any 534 | // of the parents have a shorter lifetime than the one requested, throw an error. 535 | if (options.strict && !resolver.isLeakSafe) { 536 | const maybeLongerLifetimeParentIndex = resolutionStack.findIndex( 537 | ({ lifetime: parentLifetime }) => 538 | isLifetimeLonger(parentLifetime, lifetime), 539 | ) 540 | if (maybeLongerLifetimeParentIndex > -1) { 541 | throw new AwilixResolutionError( 542 | name, 543 | resolutionStack, 544 | `Dependency '${name.toString()}' has a shorter lifetime than its ancestor: '${resolutionStack[ 545 | maybeLongerLifetimeParentIndex 546 | ].name.toString()}'`, 547 | ) 548 | } 549 | } 550 | 551 | // Pushes the currently-resolving module information onto the stack 552 | resolutionStack.push({ name, lifetime }) 553 | 554 | // Do the thing 555 | let cached: CacheEntry | undefined 556 | let resolved 557 | switch (lifetime) { 558 | case Lifetime.TRANSIENT: 559 | // Transient lifetime means resolve every time. 560 | resolved = resolver.resolve(container) 561 | break 562 | case Lifetime.SINGLETON: 563 | // Singleton lifetime means cache at all times, regardless of scope. 564 | cached = rootContainer.cache.get(name) 565 | if (!cached) { 566 | // if we are running in strict mode, perform singleton resolution using the root 567 | // container only. 568 | resolved = resolver.resolve( 569 | options.strict ? rootContainer : container, 570 | ) 571 | rootContainer.cache.set(name, { resolver, value: resolved }) 572 | } else { 573 | resolved = cached.value 574 | } 575 | break 576 | case Lifetime.SCOPED: 577 | // Scoped lifetime means that the container 578 | // that resolves the registration also caches it. 579 | // If this container cache does not have it, 580 | // resolve and cache it rather than using the parent 581 | // container's cache. 582 | cached = container.cache.get(name) 583 | if (cached !== undefined) { 584 | // We found one! 585 | resolved = cached.value 586 | break 587 | } 588 | 589 | // If we still have not found one, we need to resolve and cache it. 590 | resolved = resolver.resolve(container) 591 | container.cache.set(name, { resolver, value: resolved }) 592 | break 593 | default: 594 | throw new AwilixResolutionError( 595 | name, 596 | resolutionStack, 597 | `Unknown lifetime "${resolver.lifetime}"`, 598 | ) 599 | } 600 | // Pop it from the stack again, ready for the next resolution 601 | resolutionStack.pop() 602 | return resolved 603 | } catch (err) { 604 | // When we get an error we need to reset the stack. Mutate the existing array rather than 605 | // updating the reference to ensure all parent containers' stacks are also updated. 606 | resolutionStack.length = 0 607 | throw err 608 | } 609 | } 610 | 611 | /** 612 | * Checks if the registration with the given name exists. 613 | * 614 | * @param {string | symbol} name 615 | * The name of the registration to resolve. 616 | * 617 | * @return {boolean} 618 | * Whether or not the registration exists. 619 | */ 620 | function hasRegistration(name: string | symbol): boolean { 621 | return !!getRegistration(name) 622 | } 623 | 624 | /** 625 | * Given a registration, class or function, builds it up and returns it. 626 | * Does not cache it, this means that any lifetime configured in case of passing 627 | * a registration will not be used. 628 | * 629 | * @param {Resolver|Constructor|Function} targetOrResolver 630 | * @param {ResolverOptions} opts 631 | */ 632 | function build( 633 | targetOrResolver: Resolver | ClassOrFunctionReturning, 634 | opts?: BuildResolverOptions, 635 | ): T { 636 | if (targetOrResolver && (targetOrResolver as Resolver).resolve) { 637 | return (targetOrResolver as Resolver).resolve(container) 638 | } 639 | 640 | const funcName = 'build' 641 | const paramName = 'targetOrResolver' 642 | AwilixTypeError.assert( 643 | targetOrResolver, 644 | funcName, 645 | paramName, 646 | 'a registration, function or class', 647 | targetOrResolver, 648 | ) 649 | AwilixTypeError.assert( 650 | typeof targetOrResolver === 'function', 651 | funcName, 652 | paramName, 653 | 'a function or class', 654 | targetOrResolver, 655 | ) 656 | 657 | const resolver = isClass(targetOrResolver as any) 658 | ? asClass(targetOrResolver as Constructor, opts) 659 | : asFunction(targetOrResolver as FunctionReturning, opts) 660 | return resolver.resolve(container) 661 | } 662 | 663 | function loadModules( 664 | globPatterns: Array, 665 | opts: LoadModulesOptions, 666 | ): ESM extends false ? AwilixContainer : Promise 667 | /** 668 | * Binds `lib/loadModules` to this container, and provides 669 | * real implementations of it's dependencies. 670 | * 671 | * Additionally, any modules using the `dependsOn` API 672 | * will be resolved. 673 | * 674 | * @see lib/loadModules.js documentation. 675 | */ 676 | function loadModules( 677 | globPatterns: Array, 678 | opts: LoadModulesOptions, 679 | ): Promise | AwilixContainer { 680 | const _loadModulesDeps = { 681 | require: 682 | options!.require || 683 | function (uri) { 684 | // eslint-disable-next-line @typescript-eslint/no-require-imports 685 | return require(uri) 686 | }, 687 | listModules, 688 | container, 689 | } 690 | if (opts?.esModules) { 691 | _loadModulesDeps.require = importModule 692 | return ( 693 | realLoadModules( 694 | _loadModulesDeps, 695 | globPatterns, 696 | opts, 697 | ) as Promise 698 | ).then(() => container) 699 | } else { 700 | realLoadModules(_loadModulesDeps, globPatterns, opts) 701 | return container 702 | } 703 | } 704 | 705 | /** 706 | * Disposes this container and it's children, calling the disposer 707 | * on all disposable registrations and clearing the cache. 708 | */ 709 | function dispose(): Promise { 710 | const entries = Array.from(container.cache.entries()) 711 | container.cache.clear() 712 | return Promise.all( 713 | entries.map(([, entry]) => { 714 | const { resolver, value } = entry 715 | const disposable = resolver as DisposableResolver 716 | if (disposable.dispose) { 717 | return Promise.resolve().then(() => disposable.dispose!(value)) 718 | } 719 | return Promise.resolve() 720 | }), 721 | ).then(() => undefined) 722 | } 723 | } 724 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionStack } from './container' 2 | 3 | /** 4 | * Newline. 5 | */ 6 | const EOL = '\n' 7 | 8 | /** 9 | * An extendable error class. 10 | * @author https://github.com/bjyoungblood/es6-error/ 11 | */ 12 | export class ExtendableError extends Error { 13 | /** 14 | * Constructor for the error. 15 | * 16 | * @param {String} message 17 | * The error message. 18 | */ 19 | constructor(message: string) { 20 | super(message) 21 | 22 | // extending Error is weird and does not propagate `message` 23 | Object.defineProperty(this, 'message', { 24 | enumerable: false, 25 | value: message, 26 | }) 27 | 28 | Object.defineProperty(this, 'name', { 29 | enumerable: false, 30 | value: this.constructor.name, 31 | }) 32 | 33 | // Not all browsers have this function. 34 | /* istanbul ignore else */ 35 | if ('captureStackTrace' in Error) { 36 | Error.captureStackTrace(this, this.constructor) 37 | } else { 38 | Object.defineProperty(this, 'stack', { 39 | enumerable: false, 40 | value: (Error as ErrorConstructor)(message).stack, 41 | writable: true, 42 | configurable: true, 43 | }) 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Base error for all Awilix-specific errors. 50 | */ 51 | export class AwilixError extends ExtendableError {} 52 | 53 | /** 54 | * Error thrown to indicate a type mismatch. 55 | */ 56 | export class AwilixTypeError extends AwilixError { 57 | /** 58 | * Constructor, takes the function name, expected and given 59 | * type to produce an error. 60 | * 61 | * @param {string} funcDescription 62 | * Name of the function being guarded. 63 | * 64 | * @param {string} paramName 65 | * The parameter there was an issue with. 66 | * 67 | * @param {string} expectedType 68 | * Name of the expected type. 69 | * 70 | * @param {string} givenType 71 | * Name of the given type. 72 | */ 73 | constructor( 74 | funcDescription: string, 75 | paramName: string, 76 | expectedType: string, 77 | givenType: any, 78 | ) { 79 | super( 80 | `${funcDescription}: expected ${paramName} to be ${expectedType}, but got ${givenType}.`, 81 | ) 82 | } 83 | 84 | /** 85 | * Asserts the given condition, throws an error otherwise. 86 | * 87 | * @param {*} condition 88 | * The condition to check 89 | * 90 | * @param {string} funcDescription 91 | * Name of the function being guarded. 92 | * 93 | * @param {string} paramName 94 | * The parameter there was an issue with. 95 | * 96 | * @param {string} expectedType 97 | * Name of the expected type. 98 | * 99 | * @param {string} givenType 100 | * Name of the given type. 101 | */ 102 | static assert( 103 | condition: T, 104 | funcDescription: string, 105 | paramName: string, 106 | expectedType: string, 107 | givenType: any, 108 | ) { 109 | if (!condition) { 110 | throw new AwilixTypeError( 111 | funcDescription, 112 | paramName, 113 | expectedType, 114 | givenType, 115 | ) 116 | } 117 | return condition 118 | } 119 | } 120 | 121 | /** 122 | * A nice error class so we can do an instanceOf check. 123 | */ 124 | export class AwilixResolutionError extends AwilixError { 125 | /** 126 | * Constructor, takes the registered modules and unresolved tokens 127 | * to create a message. 128 | * 129 | * @param {string|symbol} name 130 | * The name of the module that could not be resolved. 131 | * 132 | * @param {string[]} resolutionStack 133 | * The current resolution stack 134 | */ 135 | constructor( 136 | name: string | symbol, 137 | resolutionStack: ResolutionStack, 138 | message?: string, 139 | ) { 140 | const stringName = name.toString() 141 | const nameStack = resolutionStack.map(({ name: val }) => val.toString()) 142 | nameStack.push(stringName) 143 | const resolutionPathString = nameStack.join(' -> ') 144 | let msg = `Could not resolve '${stringName}'.` 145 | if (message) { 146 | msg += ` ${message}` 147 | } 148 | 149 | msg += EOL + EOL 150 | msg += `Resolution path: ${resolutionPathString}` 151 | super(msg) 152 | } 153 | } 154 | 155 | /** 156 | * A nice error class so we can do an instanceOf check. 157 | */ 158 | export class AwilixRegistrationError extends AwilixError { 159 | /** 160 | * Constructor, takes the registered modules and unresolved tokens 161 | * to create a message. 162 | * 163 | * @param {string|symbol} name 164 | * The name of the module that could not be registered. 165 | */ 166 | constructor(name: string | symbol, message?: string) { 167 | const stringName = name.toString() 168 | let msg = `Could not register '${stringName}'.` 169 | if (message) { 170 | msg += ` ${message}` 171 | } 172 | super(msg) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/function-tokenizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Token type. 3 | */ 4 | export type TokenType = 5 | | 'ident' 6 | | '(' 7 | | ')' 8 | | ',' 9 | | '=' 10 | | '*' 11 | | 'function' 12 | | 'class' 13 | | 'EOF' 14 | 15 | /** 16 | * Lexer Token. 17 | */ 18 | export interface Token { 19 | type: TokenType 20 | value?: string 21 | } 22 | 23 | /** 24 | * Flags that can be passed to the tokenizer to toggle certain things. 25 | */ 26 | export const enum TokenizerFlags { 27 | None = 0, 28 | /** 29 | * If this is set, the tokenizer will not attempt to be smart about skipping expressions. 30 | */ 31 | Dumb = 1, 32 | } 33 | 34 | /** 35 | * Creates a tokenizer for the specified source. 36 | * 37 | * @param source 38 | */ 39 | export function createTokenizer(source: string) { 40 | const end = source.length 41 | let pos: number = 0 42 | let type: TokenType = 'EOF' 43 | let value: string = '' 44 | let flags = TokenizerFlags.None 45 | // These are used to greedily skip as much as possible. 46 | // Whenever we reach a paren, we increment these. 47 | let parenLeft = 0 48 | let parenRight = 0 49 | 50 | return { 51 | next, 52 | done, 53 | } 54 | 55 | /** 56 | * Advances the tokenizer and returns the next token. 57 | */ 58 | function next(nextFlags = TokenizerFlags.None): Token { 59 | flags = nextFlags 60 | advance() 61 | return createToken() 62 | } 63 | 64 | /** 65 | * Advances the tokenizer state. 66 | */ 67 | function advance() { 68 | value = '' 69 | type = 'EOF' 70 | 71 | while (true) { 72 | if (pos >= end) { 73 | return (type = 'EOF') 74 | } 75 | 76 | const ch = source.charAt(pos) 77 | // Whitespace is irrelevant 78 | if (isWhiteSpace(ch)) { 79 | pos++ 80 | continue 81 | } 82 | 83 | switch (ch) { 84 | case '(': 85 | pos++ 86 | parenLeft++ 87 | return (type = ch) 88 | case ')': 89 | pos++ 90 | parenRight++ 91 | return (type = ch) 92 | case '*': 93 | pos++ 94 | return (type = ch) 95 | case ',': 96 | pos++ 97 | return (type = ch) 98 | case '=': 99 | pos++ 100 | if ((flags & TokenizerFlags.Dumb) === 0) { 101 | // Not in dumb-mode, so attempt to skip. 102 | skipExpression() 103 | } 104 | // We need to know that there's a default value so we can 105 | // skip it if it does not exist when resolving. 106 | return (type = ch) 107 | case '/': { 108 | pos++ 109 | const nextCh = source.charAt(pos) 110 | if (nextCh === '/') { 111 | skipUntil((c) => c === '\n', true) 112 | pos++ 113 | } 114 | if (nextCh === '*') { 115 | skipUntil((c) => { 116 | const closing = source.charAt(pos + 1) 117 | return c === '*' && closing === '/' 118 | }, true) 119 | pos++ 120 | } 121 | break 122 | } 123 | default: 124 | // Scans an identifier. 125 | if (isIdentifierStart(ch)) { 126 | scanIdentifier() 127 | return type 128 | } 129 | 130 | // Elegantly skip over tokens we don't care about. 131 | pos++ 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Scans an identifier, given it's already been proven 138 | * we are ready to do so. 139 | */ 140 | function scanIdentifier() { 141 | const identStart = source.charAt(pos) 142 | const start = ++pos 143 | while (isIdentifierPart(source.charAt(pos))) { 144 | pos++ 145 | } 146 | value = '' + identStart + source.substring(start, pos) 147 | type = value === 'function' || value === 'class' ? value : 'ident' 148 | if (type !== 'ident') { 149 | value = '' 150 | } 151 | return value 152 | } 153 | 154 | /** 155 | * Skips everything until the next comma or the end of the parameter list. 156 | * Checks the parenthesis balance so we correctly skip function calls. 157 | */ 158 | function skipExpression() { 159 | skipUntil((ch) => { 160 | const isAtRoot = parenLeft === parenRight + 1 161 | if (ch === ',' && isAtRoot) { 162 | return true 163 | } 164 | 165 | if (ch === '(') { 166 | parenLeft++ 167 | return false 168 | } 169 | 170 | if (ch === ')') { 171 | parenRight++ 172 | if (isAtRoot) { 173 | return true 174 | } 175 | } 176 | 177 | return false 178 | }) 179 | } 180 | 181 | /** 182 | * Skips strings and whitespace until the predicate is true. 183 | * 184 | * @param callback stops skipping when this returns `true`. 185 | * @param dumb if `true`, does not skip whitespace and strings; 186 | * it only stops once the callback returns `true`. 187 | */ 188 | function skipUntil(callback: (ch: string) => boolean, dumb = false) { 189 | while (pos < source.length) { 190 | const ch = source.charAt(pos) 191 | if (callback(ch)) { 192 | return 193 | } 194 | 195 | if (!dumb) { 196 | if (isWhiteSpace(ch)) { 197 | pos++ 198 | continue 199 | } 200 | 201 | if (isStringQuote(ch)) { 202 | skipString() 203 | continue 204 | } 205 | } 206 | pos++ 207 | } 208 | } 209 | 210 | /** 211 | * Given the current position is at a string quote, skips the entire string. 212 | */ 213 | function skipString() { 214 | const quote = source.charAt(pos) 215 | pos++ 216 | while (pos < source.length) { 217 | const ch = source.charAt(pos) 218 | const prev = source.charAt(pos - 1) 219 | // Checks if the quote was escaped. 220 | if (ch === quote && prev !== '\\') { 221 | pos++ 222 | return 223 | } 224 | 225 | // Template strings are a bit tougher, we want to skip the interpolated values. 226 | if (quote === '`') { 227 | const next = source.charAt(pos + 1) 228 | if (next === '$') { 229 | const afterDollar = source.charAt(pos + 2) 230 | if (afterDollar === '{') { 231 | // This is the start of an interpolation; skip the ${ 232 | pos = pos + 2 233 | // Skip strings and whitespace until we reach the ending }. 234 | // This includes skipping nested interpolated strings. :D 235 | skipUntil((ch) => ch === '}') 236 | } 237 | } 238 | } 239 | 240 | pos++ 241 | } 242 | } 243 | 244 | /** 245 | * Creates a token from the current state. 246 | */ 247 | function createToken(): Token { 248 | if (value) { 249 | return { value, type } 250 | } 251 | return { type } 252 | } 253 | 254 | /** 255 | * Determines if we are done parsing. 256 | */ 257 | function done() { 258 | return type === 'EOF' 259 | } 260 | } 261 | 262 | /** 263 | * Determines if the given character is a whitespace character. 264 | * 265 | * @param {string} ch 266 | * @return {boolean} 267 | */ 268 | function isWhiteSpace(ch: string): boolean { 269 | switch (ch) { 270 | case '\r': 271 | case '\n': 272 | case ' ': 273 | return true 274 | } 275 | return false 276 | } 277 | 278 | /** 279 | * Determines if the specified character is a string quote. 280 | * @param {string} ch 281 | * @return {boolean} 282 | */ 283 | function isStringQuote(ch: string): boolean { 284 | switch (ch) { 285 | case "'": 286 | case '"': 287 | case '`': 288 | return true 289 | } 290 | return false 291 | } 292 | 293 | // NOTE: I've added the `.` character, optionally prefixed by `?` so 294 | // that member expression paths are seen as identifiers. 295 | // This is so we don't get a constructor token for stuff 296 | // like `MyClass.prototype?.constructor()` 297 | const IDENT_START_EXPR = /^[_$a-zA-Z\xA0-\uFFFF]$/ 298 | const IDENT_PART_EXPR = /^[?._$a-zA-Z0-9\xA0-\uFFFF]$/ 299 | 300 | /** 301 | * Determines if the character is a valid JS identifier start character. 302 | */ 303 | function isIdentifierStart(ch: string) { 304 | return IDENT_START_EXPR.test(ch) 305 | } 306 | 307 | /** 308 | * Determines if the character is a valid JS identifier start character. 309 | */ 310 | function isIdentifierPart(ch: string) { 311 | return IDENT_PART_EXPR.test(ch) 312 | } 313 | -------------------------------------------------------------------------------- /src/injection-mode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Injection mode type. 3 | */ 4 | export type InjectionModeType = 'PROXY' | 'CLASSIC' 5 | 6 | /** 7 | * Resolution modes. 8 | */ 9 | export const InjectionMode: Record = { 10 | /** 11 | * The dependencies will be resolved by injecting the cradle proxy. 12 | * 13 | * @type {String} 14 | */ 15 | PROXY: 'PROXY', 16 | 17 | /** 18 | * The dependencies will be resolved by inspecting parameter names of the function/constructor. 19 | * 20 | * @type {String} 21 | */ 22 | CLASSIC: 'CLASSIC', 23 | } 24 | -------------------------------------------------------------------------------- /src/lifetime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lifetime type. 3 | */ 4 | export type LifetimeType = 'SINGLETON' | 'TRANSIENT' | 'SCOPED' 5 | 6 | /** 7 | * Lifetime types. 8 | */ 9 | export const Lifetime: Record = { 10 | /** 11 | * The registration will be resolved once and only once. 12 | * @type {String} 13 | */ 14 | SINGLETON: 'SINGLETON', 15 | 16 | /** 17 | * The registration will be resolved every time (never cached). 18 | * @type {String} 19 | */ 20 | TRANSIENT: 'TRANSIENT', 21 | 22 | /** 23 | * The registration will be resolved once per scope. 24 | * @type {String} 25 | */ 26 | SCOPED: 'SCOPED', 27 | } 28 | 29 | /** 30 | * Returns true if and only if the first lifetime is strictly longer than the second. 31 | */ 32 | export function isLifetimeLonger(a: LifetimeType, b: LifetimeType): boolean { 33 | return ( 34 | (a === Lifetime.SINGLETON && b !== Lifetime.SINGLETON) || 35 | (a === Lifetime.SCOPED && b === Lifetime.TRANSIENT) 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/list-modules.ts: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob' 2 | import * as path from 'path' 3 | import { flatten } from './utils' 4 | import { BuildResolverOptions, ResolverOptions } from './resolvers' 5 | import { LifetimeType } from './awilix' 6 | 7 | /** 8 | * The options when invoking listModules(). 9 | * @interface ListModulesOptions 10 | */ 11 | export interface ListModulesOptions { 12 | cwd?: string 13 | glob?: typeof glob.sync 14 | } 15 | 16 | /** 17 | * An object containing the module name and path (full path to module). 18 | * 19 | * @interface ModuleDescriptor 20 | */ 21 | export interface ModuleDescriptor { 22 | name: string 23 | path: string 24 | opts: any 25 | } 26 | 27 | /** 28 | * A glob pattern with associated registration options. 29 | */ 30 | export type GlobWithOptions = 31 | | [string] 32 | | [string, BuildResolverOptions | LifetimeType] 33 | 34 | // Regex to extract the module name. 35 | const nameExpr = /(.*)\..*/i 36 | 37 | /** 38 | * Internal method for globbing a single pattern. 39 | * 40 | * @param {String} globPattern 41 | * The glob pattern. 42 | * 43 | * @param {String} opts.cwd 44 | * Current working directory, used for resolving filepaths. 45 | * Defaults to `process.cwd()`. 46 | * 47 | * @return {[{name, path, opts}]} 48 | * The module names and paths. 49 | * 50 | * @api private 51 | */ 52 | function _listModules( 53 | globPattern: string | GlobWithOptions, 54 | opts?: ListModulesOptions, 55 | ): Array { 56 | opts = { cwd: process.cwd(), glob: glob.sync, ...opts } 57 | let patternOpts: ResolverOptions | null = null 58 | if (Array.isArray(globPattern)) { 59 | patternOpts = globPattern[1] as ResolverOptions 60 | globPattern = globPattern[0] 61 | } 62 | 63 | // Replace Windows path separators with Posix path 64 | globPattern = globPattern.replace(/\\/g, '/') 65 | 66 | const result = opts.glob!(globPattern, { cwd: opts.cwd }) 67 | const mapped = result.map((p) => ({ 68 | name: nameExpr.exec(path.basename(p))![1], 69 | path: path.resolve(opts!.cwd!, p), 70 | opts: patternOpts, 71 | })) 72 | return mapped 73 | } 74 | 75 | /** 76 | * Returns a list of {name, path} pairs, 77 | * where the name is the module name, and path is the actual 78 | * full path to the module. 79 | * 80 | * @param {String|Array} globPatterns 81 | * The glob pattern as a string or an array of strings. 82 | * 83 | * @param {String} opts.cwd 84 | * Current working directory, used for resolving filepaths. 85 | * Defaults to `process.cwd()`. 86 | * 87 | * @return {[{name, path}]} 88 | * An array of objects with the module names and paths. 89 | */ 90 | export function listModules( 91 | globPatterns: string | Array, 92 | opts?: ListModulesOptions, 93 | ) { 94 | if (Array.isArray(globPatterns)) { 95 | return flatten(globPatterns.map((p) => _listModules(p, opts))) 96 | } 97 | 98 | return _listModules(globPatterns, opts) 99 | } 100 | -------------------------------------------------------------------------------- /src/load-module-native.d.ts: -------------------------------------------------------------------------------- 1 | export function importModule(path: string): Promise 2 | -------------------------------------------------------------------------------- /src/load-module-native.js: -------------------------------------------------------------------------------- 1 | // This is kept in a separate .js file to prevent TypeScript re-writing the import() statement to a require() statement 2 | function importModule(path) { 3 | return import(path) 4 | } 5 | 6 | // eslint-disable-next-line no-undef 7 | module.exports = { importModule } 8 | -------------------------------------------------------------------------------- /src/load-modules.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'camel-case' 2 | import { pathToFileURL } from 'url' 3 | import { AwilixContainer } from './container' 4 | import { Lifetime } from './lifetime' 5 | import { GlobWithOptions, ModuleDescriptor, listModules } from './list-modules' 6 | import { 7 | BuildResolver, 8 | BuildResolverOptions, 9 | RESOLVER, 10 | asClass, 11 | asFunction, 12 | } from './resolvers' 13 | import { isClass, isFunction } from './utils' 14 | 15 | /** 16 | * Metadata of the module as well as the loaded module itself. 17 | * @interface LoadedModuleDescriptor 18 | */ 19 | export interface LoadedModuleDescriptor extends ModuleDescriptor { 20 | value: unknown 21 | } 22 | 23 | /** 24 | * The options when invoking loadModules(). 25 | * @interface LoadModulesOptions 26 | */ 27 | export interface LoadModulesOptions { 28 | cwd?: string 29 | formatName?: NameFormatter | BuiltInNameFormatters 30 | resolverOptions?: BuildResolverOptions 31 | esModules?: ESM 32 | } 33 | 34 | /** 35 | * Name formatting options when using loadModules(). 36 | * @type BuiltInNameFormatters 37 | */ 38 | export type BuiltInNameFormatters = 'camelCase' 39 | 40 | /** 41 | * Takes in the filename of the module being loaded as well as the module descriptor, 42 | * and returns a string which is used to register the module in the container. 43 | * 44 | * `descriptor.name` is the same as `name`. 45 | * 46 | * @type {NameFormatter} 47 | */ 48 | export type NameFormatter = ( 49 | name: string, 50 | descriptor: LoadedModuleDescriptor, 51 | ) => string 52 | 53 | /** 54 | * Dependencies for `loadModules` 55 | */ 56 | export interface LoadModulesDeps { 57 | listModules: typeof listModules 58 | container: AwilixContainer 59 | require(path: string): any | Promise 60 | } 61 | 62 | const nameFormatters: Record = { 63 | camelCase: (s) => camelCase(s), 64 | } 65 | 66 | /** 67 | * The list of loaded modules 68 | */ 69 | export interface LoadModulesResult { 70 | loadedModules: Array 71 | } 72 | 73 | export function loadModules( 74 | dependencies: LoadModulesDeps, 75 | globPatterns: string | Array, 76 | opts?: LoadModulesOptions, 77 | ): ESM extends true ? Promise : LoadModulesResult 78 | /** 79 | * Given an array of glob strings, will call `require` 80 | * on them, and call their default exported function with the 81 | * container as the first parameter. 82 | * 83 | * @param {AwilixContainer} dependencies.container 84 | * The container to install loaded modules in. 85 | * 86 | * @param {Function} dependencies.listModules 87 | * The listModules function to use for listing modules. 88 | * 89 | * @param {Function} dependencies.require 90 | * The require function - it's a dependency because it makes testing easier. 91 | * 92 | * @param {String[]} globPatterns 93 | * The array of globs to use when loading modules. 94 | * 95 | * @param {Object} opts 96 | * Passed to `listModules`, e.g. `{ cwd: '...' }`. 97 | * 98 | * @param {(string, ModuleDescriptor) => string} opts.formatName 99 | * Used to format the name the module is registered with in the container. 100 | * 101 | * @param {boolean} opts.esModules 102 | * Set to `true` to use Node's native ECMAScriptModules modules 103 | * 104 | * @return {Object} 105 | * Returns an object describing the result. 106 | */ 107 | export function loadModules( 108 | dependencies: LoadModulesDeps, 109 | globPatterns: string | Array, 110 | opts?: LoadModulesOptions, 111 | ): Promise | LoadModulesResult { 112 | opts ??= {} 113 | const container = dependencies.container 114 | opts = optsWithDefaults(opts) 115 | const modules = dependencies.listModules(globPatterns, opts) 116 | 117 | if (opts.esModules) { 118 | return loadEsModules(dependencies, container, modules, opts) 119 | } else { 120 | const result = modules.map((m) => { 121 | const loaded = dependencies.require(m.path) 122 | return parseLoadedModule(loaded, m) 123 | }) 124 | return registerModules(result, container, modules, opts) 125 | } 126 | } 127 | 128 | /** 129 | * Loads the modules using native ES6 modules and the async import() 130 | * @param {AwilixContainer} container 131 | * @param {ModuleDescriptor[]} modules 132 | * @param {LoadModulesOptions} opts 133 | */ 134 | async function loadEsModules( 135 | dependencies: LoadModulesDeps, 136 | container: AwilixContainer, 137 | modules: ModuleDescriptor[], 138 | opts: LoadModulesOptions, 139 | ): Promise { 140 | const importPromises = [] 141 | for (const m of modules) { 142 | const fileUrl = pathToFileURL(m.path).toString() 143 | importPromises.push(dependencies.require(fileUrl)) 144 | } 145 | const imports = await Promise.all(importPromises) 146 | const result = [] 147 | for (let i = 0; i < modules.length; i++) { 148 | result.push(parseLoadedModule(imports[i], modules[i])) 149 | } 150 | return registerModules(result, container, modules, opts) 151 | } 152 | 153 | /** 154 | * Parses the module which has been required 155 | * 156 | * @param {any} loaded 157 | * @param {ModuleDescriptor} m 158 | */ 159 | function parseLoadedModule( 160 | loaded: any, 161 | m: ModuleDescriptor, 162 | ): Array { 163 | const items: Array = [] 164 | // Meh, it happens. 165 | if (!loaded) { 166 | return items 167 | } 168 | 169 | if (isFunction(loaded)) { 170 | // for module.exports = ... 171 | items.push({ 172 | name: m.name, 173 | path: m.path, 174 | value: loaded, 175 | opts: m.opts, 176 | }) 177 | 178 | return items 179 | } 180 | 181 | if (loaded.default && isFunction(loaded.default)) { 182 | // ES6 default export 183 | items.push({ 184 | name: m.name, 185 | path: m.path, 186 | value: loaded.default, 187 | opts: m.opts, 188 | }) 189 | } 190 | 191 | // loop through non-default exports, but require the RESOLVER property set for 192 | // it to be a valid service module export. 193 | for (const key of Object.keys(loaded)) { 194 | if (key === 'default') { 195 | // default case handled separately due to its different name (file name) 196 | continue 197 | } 198 | 199 | if (isFunction(loaded[key]) && RESOLVER in loaded[key]) { 200 | items.push({ 201 | name: key, 202 | path: m.path, 203 | value: loaded[key], 204 | opts: m.opts, 205 | }) 206 | } 207 | } 208 | 209 | return items 210 | } 211 | 212 | /** 213 | * Registers the modules 214 | * 215 | * @param {ModuleDescriptorVal[][]} modulesToRegister 216 | * @param {AwilixContainer} container 217 | * @param {ModuleDescriptor[]} modules 218 | * @param {LoadModulesOptions} opts 219 | */ 220 | function registerModules( 221 | modulesToRegister: LoadedModuleDescriptor[][], 222 | container: AwilixContainer, 223 | modules: ModuleDescriptor[], 224 | opts: LoadModulesOptions, 225 | ): LoadModulesResult { 226 | modulesToRegister 227 | .reduce((acc, cur) => acc.concat(cur), []) 228 | .filter((x) => x) 229 | .forEach(registerDescriptor.bind(null, container, opts)) 230 | return { 231 | loadedModules: modules, 232 | } 233 | } 234 | 235 | /** 236 | * Returns a new options object with defaults applied. 237 | */ 238 | function optsWithDefaults( 239 | opts: Partial> | undefined, 240 | ): LoadModulesOptions { 241 | return { 242 | // Does a somewhat-deep merge on the registration options. 243 | resolverOptions: { 244 | lifetime: Lifetime.TRANSIENT, 245 | ...(opts && opts.resolverOptions), 246 | }, 247 | ...opts, 248 | } 249 | } 250 | 251 | /** 252 | * Given a module descriptor, reads it and registers it's value with the container. 253 | * 254 | * @param {AwilixContainer} container 255 | * @param {LoadModulesOptions} opts 256 | * @param {ModuleDescriptor} moduleDescriptor 257 | */ 258 | function registerDescriptor( 259 | container: AwilixContainer, 260 | opts: LoadModulesOptions, 261 | moduleDescriptor: LoadedModuleDescriptor & { value: any }, 262 | ) { 263 | const inlineConfig = moduleDescriptor.value[RESOLVER] 264 | let name = inlineConfig && inlineConfig.name 265 | if (!name) { 266 | name = moduleDescriptor.name 267 | let formatter = opts.formatName 268 | if (formatter) { 269 | if (typeof formatter === 'string') { 270 | formatter = nameFormatters[formatter] 271 | } 272 | 273 | if (formatter) { 274 | name = formatter(name, moduleDescriptor) 275 | } 276 | } 277 | } 278 | 279 | let moduleDescriptorOpts = moduleDescriptor.opts 280 | 281 | if (typeof moduleDescriptorOpts === 'string') { 282 | moduleDescriptorOpts = { lifetime: moduleDescriptorOpts } 283 | } 284 | 285 | const regOpts: BuildResolver = { 286 | ...opts.resolverOptions, 287 | ...moduleDescriptorOpts, 288 | ...inlineConfig, 289 | } 290 | 291 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 292 | const reg: Function = regOpts.register 293 | ? regOpts.register 294 | : isClass(moduleDescriptor.value) 295 | ? asClass 296 | : asFunction 297 | 298 | container.register(name, reg(moduleDescriptor.value, regOpts)) 299 | } 300 | -------------------------------------------------------------------------------- /src/param-parser.ts: -------------------------------------------------------------------------------- 1 | import { createTokenizer, Token, TokenizerFlags } from './function-tokenizer' 2 | 3 | /** 4 | * A parameter for a function. 5 | */ 6 | export interface Parameter { 7 | /** 8 | * Parameter name. 9 | */ 10 | name: string 11 | /** 12 | * True if the parameter is optional. 13 | */ 14 | optional: boolean 15 | } 16 | 17 | /* 18 | * Parses the parameter list of a function string, including ES6 class constructors. 19 | * 20 | * @param {string} source 21 | * The source of a function to extract the parameter list from 22 | * 23 | * @return {Array | null} 24 | * Returns an array of parameters, or `null` if no 25 | * constructor was found for a class. 26 | */ 27 | export function parseParameterList(source: string): Array | null { 28 | const { next: _next, done } = createTokenizer(source) 29 | const params: Array = [] 30 | 31 | let t: Token = null! 32 | nextToken() 33 | while (!done()) { 34 | switch (t.type) { 35 | case 'class': { 36 | const foundConstructor = advanceToConstructor() 37 | // If we didn't find a constructor token, then we know that there 38 | // are no dependencies in the defined class. 39 | if (!foundConstructor) { 40 | return null 41 | } 42 | 43 | break 44 | } 45 | case 'function': { 46 | const next = nextToken() 47 | if (next.type === 'ident' || next.type === '*') { 48 | // This is the function name or a generator star. Skip it. 49 | nextToken() 50 | } 51 | break 52 | } 53 | case '(': 54 | // Start parsing parameter names. 55 | parseParams() 56 | break 57 | case ')': 58 | // We're now out of the parameter list. 59 | return params 60 | 61 | // When we're encountering an identifier token 62 | // at this level, it could be because it's an arrow function 63 | // with a single parameter, e.g. `foo => ...`. 64 | // This path won't be hit if we've already identified the `(` token. 65 | case 'ident': { 66 | // Likely a paren-less arrow function 67 | // which can have no default args. 68 | const param = { name: t.value!, optional: false } 69 | if (t.value === 'async') { 70 | // Given it's the very first token, we can assume it's an async function, 71 | // so skip the async keyword if the next token is not an equals sign, in which 72 | // case it is a single-arg arrow func. 73 | const next = nextToken() 74 | if (next && next.type !== '=') { 75 | break 76 | } 77 | } 78 | params.push(param) 79 | return params 80 | } 81 | /* istanbul ignore next */ 82 | default: 83 | throw unexpected() 84 | } 85 | } 86 | 87 | return params 88 | 89 | /** 90 | * After having been placed within the parameter list of 91 | * a function, parses the parameters. 92 | */ 93 | function parseParams() { 94 | // Current token is a left-paren 95 | let param: Parameter = { name: '', optional: false } 96 | while (!done()) { 97 | nextToken() 98 | switch (t.type) { 99 | case 'ident': 100 | param.name = t.value! 101 | break 102 | case '=': 103 | param.optional = true 104 | break 105 | case ',': 106 | params.push(param) 107 | param = { name: '', optional: false } 108 | break 109 | case ')': 110 | if (param.name) { 111 | params.push(param) 112 | } 113 | return 114 | /* istanbul ignore next */ 115 | default: 116 | throw unexpected() 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Advances until we reach the constructor identifier followed by 123 | * a `(` token. 124 | * 125 | * @returns `true` if a constructor was found, `false` otherwise. 126 | */ 127 | function advanceToConstructor() { 128 | while (!done()) { 129 | if (isConstructorToken()) { 130 | // Consume the token 131 | nextToken(TokenizerFlags.Dumb) 132 | 133 | // If the current token now isn't a `(`, then it wasn't the actual 134 | // constructor. 135 | if (t.type !== '(') { 136 | continue 137 | } 138 | 139 | return true 140 | } 141 | nextToken(TokenizerFlags.Dumb) 142 | } 143 | 144 | return false 145 | } 146 | 147 | /** 148 | * Determines if the current token represents a constructor, and the next token after it is a paren 149 | * @return {boolean} 150 | */ 151 | function isConstructorToken(): boolean { 152 | return t.type === 'ident' && t.value === 'constructor' 153 | } 154 | 155 | /** 156 | * Advances the tokenizer and stores the previous token in history 157 | */ 158 | function nextToken(flags = TokenizerFlags.None) { 159 | t = _next(flags) 160 | return t 161 | } 162 | 163 | /** 164 | * Returns an error describing an unexpected token. 165 | */ 166 | /* istanbul ignore next */ 167 | function unexpected() { 168 | return new SyntaxError( 169 | `Parsing parameter list, did not expect ${t.type} token${ 170 | t.value ? ` (${t.value})` : '' 171 | }`, 172 | ) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { AwilixContainer, FunctionReturning, ResolveOptions } from './container' 2 | import { AwilixTypeError } from './errors' 3 | import { InjectionMode, InjectionModeType } from './injection-mode' 4 | import { Lifetime, LifetimeType } from './lifetime' 5 | import { Parameter, parseParameterList } from './param-parser' 6 | import { isFunction, uniq } from './utils' 7 | 8 | // We parse the signature of any `Function`, so we want to allow `Function` types. 9 | /* eslint-disable @typescript-eslint/no-unsafe-function-type */ 10 | 11 | /** 12 | * RESOLVER symbol can be used by modules loaded by 13 | * `loadModules` to configure their lifetime, injection mode, etc. 14 | */ 15 | export const RESOLVER = Symbol('Awilix Resolver Config') 16 | 17 | /** 18 | * Gets passed the container and is expected to return an object 19 | * whose properties are accessible at construction time for the 20 | * configured resolver. 21 | * 22 | * @type {Function} 23 | */ 24 | export type InjectorFunction = ( 25 | container: AwilixContainer, 26 | ) => object 27 | 28 | /** 29 | * A resolver object returned by asClass(), asFunction() or asValue(). 30 | */ 31 | export interface Resolver extends ResolverOptions { 32 | resolve(container: AwilixContainer): T 33 | } 34 | 35 | /** 36 | * A resolver object created by asClass() or asFunction(). 37 | */ 38 | export interface BuildResolver extends Resolver, BuildResolverOptions { 39 | injectionMode?: InjectionModeType 40 | injector?: InjectorFunction 41 | setLifetime(lifetime: LifetimeType): this 42 | setInjectionMode(mode: InjectionModeType): this 43 | singleton(): this 44 | scoped(): this 45 | transient(): this 46 | proxy(): this 47 | classic(): this 48 | inject(injector: InjectorFunction): this 49 | } 50 | 51 | /** 52 | * Options for disposable resolvers. 53 | */ 54 | export interface DisposableResolverOptions extends ResolverOptions { 55 | dispose?: Disposer 56 | } 57 | 58 | /** 59 | * Disposable resolver. 60 | */ 61 | export interface DisposableResolver 62 | extends Resolver, 63 | DisposableResolverOptions { 64 | disposer(dispose: Disposer): this 65 | } 66 | 67 | /** 68 | * Disposer function type. 69 | */ 70 | export type Disposer = (value: T) => any | Promise 71 | 72 | /** 73 | * The options when registering a class, function or value. 74 | * @type RegistrationOptions 75 | */ 76 | export interface ResolverOptions { 77 | /** 78 | * Only used for inline configuration with `loadModules`. 79 | */ 80 | name?: string 81 | /** 82 | * Lifetime setting. 83 | */ 84 | lifetime?: LifetimeType 85 | /** 86 | * Registration function to use. Only used for inline configuration with `loadModules`. 87 | */ 88 | register?: (...args: any[]) => Resolver 89 | /** 90 | * True if this resolver should be excluded from lifetime leak checking. Used by resolvers that 91 | * wish to uphold the anti-leakage contract themselves. Defaults to false. 92 | */ 93 | isLeakSafe?: boolean 94 | } 95 | 96 | /** 97 | * Builder resolver options. 98 | */ 99 | export interface BuildResolverOptions 100 | extends ResolverOptions, 101 | DisposableResolverOptions { 102 | /** 103 | * Resolution mode. 104 | */ 105 | injectionMode?: InjectionModeType 106 | /** 107 | * Injector function to provide additional parameters. 108 | */ 109 | injector?: InjectorFunction 110 | } 111 | 112 | /** 113 | * A class constructor. For example: 114 | * 115 | * class MyClass {} 116 | * 117 | * container.registerClass('myClass', MyClass) 118 | * ^^^^^^^ 119 | */ 120 | export type Constructor = { new (...args: any[]): T } 121 | 122 | /** 123 | * Creates a simple value resolver where the given value will always be resolved. The value is 124 | * marked as leak-safe since in strict mode, the value will only be resolved when it is not leaking 125 | * upwards from a child scope to a parent singleton. 126 | * 127 | * @param {string} name The name to register the value as. 128 | * 129 | * @param {*} value The value to resolve. 130 | * 131 | * @return {object} The resolver. 132 | */ 133 | export function asValue(value: T): Resolver { 134 | return { 135 | resolve: () => value, 136 | isLeakSafe: true, 137 | } 138 | } 139 | 140 | /** 141 | * Creates a factory resolver, where the given factory function 142 | * will be invoked with `new` when requested. 143 | * 144 | * @param {string} name 145 | * The name to register the value as. 146 | * 147 | * @param {Function} fn 148 | * The function to register. 149 | * 150 | * @param {object} opts 151 | * Additional options for the resolver. 152 | * 153 | * @return {object} 154 | * The resolver. 155 | */ 156 | export function asFunction( 157 | fn: FunctionReturning, 158 | opts?: BuildResolverOptions, 159 | ): BuildResolver & DisposableResolver { 160 | if (!isFunction(fn)) { 161 | throw new AwilixTypeError('asFunction', 'fn', 'function', fn) 162 | } 163 | 164 | const defaults = { 165 | lifetime: Lifetime.TRANSIENT, 166 | } 167 | 168 | opts = makeOptions(defaults, opts, (fn as any)[RESOLVER]) 169 | 170 | const resolve = generateResolve(fn) 171 | const result = { 172 | resolve, 173 | ...opts, 174 | } 175 | 176 | return createDisposableResolver(createBuildResolver(result)) 177 | } 178 | 179 | /** 180 | * Like a factory resolver, but for classes that require `new`. 181 | * 182 | * @param {string} name 183 | * The name to register the value as. 184 | * 185 | * @param {Class} Type 186 | * The function to register. 187 | * 188 | * @param {object} opts 189 | * Additional options for the resolver. 190 | * 191 | * @return {object} 192 | * The resolver. 193 | */ 194 | export function asClass( 195 | Type: Constructor, 196 | opts?: BuildResolverOptions, 197 | ): BuildResolver & DisposableResolver { 198 | if (!isFunction(Type)) { 199 | throw new AwilixTypeError('asClass', 'Type', 'class', Type) 200 | } 201 | 202 | const defaults = { 203 | lifetime: Lifetime.TRANSIENT, 204 | } 205 | 206 | opts = makeOptions(defaults, opts, (Type as any)[RESOLVER]) 207 | 208 | // A function to handle object construction for us, as to make the generateResolve more reusable 209 | const newClass = function newClass(...args: unknown[]) { 210 | return Reflect.construct(Type, args) 211 | } 212 | 213 | const resolve = generateResolve(newClass, Type) 214 | return createDisposableResolver( 215 | createBuildResolver({ 216 | ...opts, 217 | resolve, 218 | }), 219 | ) 220 | } 221 | 222 | /** 223 | * Resolves to the specified registration. Marked as leak-safe since the alias target is what should 224 | * be checked for lifetime leaks. 225 | */ 226 | export function aliasTo( 227 | name: Parameters[0], 228 | ): Resolver { 229 | return { 230 | resolve(container) { 231 | return container.resolve(name) 232 | }, 233 | isLeakSafe: true, 234 | } 235 | } 236 | 237 | /** 238 | * Given an options object, creates a fluid interface 239 | * to manage it. 240 | * 241 | * @param {*} obj 242 | * The object to return. 243 | * 244 | * @return {object} 245 | * The interface. 246 | */ 247 | export function createBuildResolver>( 248 | obj: B, 249 | ): BuildResolver & B { 250 | function setLifetime(this: any, value: LifetimeType) { 251 | return createBuildResolver({ 252 | ...this, 253 | lifetime: value, 254 | }) 255 | } 256 | 257 | function setInjectionMode(this: any, value: InjectionModeType) { 258 | return createBuildResolver({ 259 | ...this, 260 | injectionMode: value, 261 | }) 262 | } 263 | 264 | function inject(this: any, injector: InjectorFunction) { 265 | return createBuildResolver({ 266 | ...this, 267 | injector, 268 | }) 269 | } 270 | 271 | return updateResolver(obj, { 272 | setLifetime, 273 | inject, 274 | transient: partial(setLifetime, Lifetime.TRANSIENT), 275 | scoped: partial(setLifetime, Lifetime.SCOPED), 276 | singleton: partial(setLifetime, Lifetime.SINGLETON), 277 | setInjectionMode, 278 | proxy: partial(setInjectionMode, InjectionMode.PROXY), 279 | classic: partial(setInjectionMode, InjectionMode.CLASSIC), 280 | }) 281 | } 282 | 283 | /** 284 | * Given a resolver, returns an object with methods to manage the disposer 285 | * function. 286 | * @param obj 287 | */ 288 | export function createDisposableResolver>( 289 | obj: B, 290 | ): DisposableResolver & B { 291 | function disposer(this: any, dispose: Disposer) { 292 | return createDisposableResolver({ 293 | ...this, 294 | dispose, 295 | }) 296 | } 297 | 298 | return updateResolver(obj, { 299 | disposer, 300 | }) 301 | } 302 | 303 | /** 304 | * Partially apply arguments to the given function. 305 | */ 306 | function partial(fn: (arg1: T1) => R, arg1: T1): () => R { 307 | return function partiallyApplied(this: any): R { 308 | return fn.call(this, arg1) 309 | } 310 | } 311 | 312 | /** 313 | * Makes an options object based on defaults. 314 | * 315 | * @param {object} defaults 316 | * Default options. 317 | * 318 | * @param {...} rest 319 | * The input to check and possibly assign to the resulting object 320 | * 321 | * @return {object} 322 | */ 323 | function makeOptions(defaults: T, ...rest: Array): T & O { 324 | return Object.assign({}, defaults, ...rest) as T & O 325 | } 326 | 327 | /** 328 | * Creates a new resolver with props merged from both. 329 | * 330 | * @param source 331 | * @param target 332 | */ 333 | function updateResolver, B>( 334 | source: A, 335 | target: B, 336 | ): Resolver & A & B { 337 | const result = { 338 | ...(source as any), 339 | ...(target as any), 340 | } 341 | return result 342 | } 343 | 344 | /** 345 | * Returns a wrapped `resolve` function that provides values 346 | * from the injector and defers to `container.resolve`. 347 | * 348 | * @param {AwilixContainer} container 349 | * @param {Object} locals 350 | * @return {Function} 351 | */ 352 | function wrapWithLocals( 353 | container: AwilixContainer, 354 | locals: any, 355 | ) { 356 | return function wrappedResolve(name: string, resolveOpts: ResolveOptions) { 357 | if (name in locals) { 358 | return locals[name] 359 | } 360 | 361 | return container.resolve(name, resolveOpts) 362 | } 363 | } 364 | 365 | /** 366 | * Returns a new Proxy that checks the result from `injector` 367 | * for values before delegating to the actual container. 368 | * 369 | * @param {Object} cradle 370 | * @param {Function} injector 371 | * @return {Proxy} 372 | */ 373 | function createInjectorProxy( 374 | container: AwilixContainer, 375 | injector: InjectorFunction, 376 | ) { 377 | const locals = injector(container) as any 378 | const allKeys = uniq([ 379 | ...Reflect.ownKeys(container.cradle), 380 | ...Reflect.ownKeys(locals), 381 | ]) 382 | // TODO: Lots of duplication here from the container proxy. 383 | // Need to refactor. 384 | const proxy = new Proxy( 385 | {}, 386 | { 387 | /** 388 | * Resolves the value by first checking the locals, then the container. 389 | */ 390 | get(target: any, name: string | symbol) { 391 | if (name === Symbol.iterator) { 392 | return function* iterateRegistrationsAndLocals() { 393 | for (const prop in container.cradle) { 394 | yield prop 395 | } 396 | for (const prop in locals) { 397 | yield prop 398 | } 399 | } 400 | } 401 | if (name in locals) { 402 | return locals[name] 403 | } 404 | return container.resolve(name as string) 405 | }, 406 | 407 | /** 408 | * Used for `Object.keys`. 409 | */ 410 | ownKeys() { 411 | return allKeys 412 | }, 413 | 414 | /** 415 | * Used for `Object.keys`. 416 | */ 417 | getOwnPropertyDescriptor(target: any, key: string) { 418 | if (allKeys.indexOf(key) > -1) { 419 | return { 420 | enumerable: true, 421 | configurable: true, 422 | } 423 | } 424 | 425 | return undefined 426 | }, 427 | }, 428 | ) 429 | 430 | return proxy 431 | } 432 | 433 | /** 434 | * Returns a resolve function used to construct the dependency graph 435 | * 436 | * @this {Registration} 437 | * The `this` context is a resolver. 438 | * 439 | * @param {Function} fn 440 | * The function to construct 441 | * 442 | * @param {Function} dependencyParseTarget 443 | * The function to parse for the dependencies of the construction target 444 | * 445 | * @param {boolean} isFunction 446 | * Is the resolution target an actual function or a mask for a constructor? 447 | * 448 | * @return {Function} 449 | * The function used for dependency resolution 450 | */ 451 | function generateResolve(fn: Function, dependencyParseTarget?: Function) { 452 | // If the function used for dependency parsing is falsy, use the supplied function 453 | if (!dependencyParseTarget) { 454 | dependencyParseTarget = fn 455 | } 456 | 457 | // Parse out the dependencies 458 | // NOTE: we do this regardless of whether PROXY is used or not, 459 | // because if this fails, we want it to fail early (at startup) rather 460 | // than at resolution time. 461 | const dependencies = parseDependencies(dependencyParseTarget) 462 | 463 | // Use a regular function instead of an arrow function to facilitate binding to the resolver. 464 | return function resolve( 465 | this: BuildResolver, 466 | container: AwilixContainer, 467 | ) { 468 | // Because the container holds a global reolutionMode we need to determine it in the proper order of precedence: 469 | // resolver -> container -> default value 470 | const injectionMode = 471 | this.injectionMode || 472 | container.options.injectionMode || 473 | InjectionMode.PROXY 474 | 475 | if (injectionMode !== InjectionMode.CLASSIC) { 476 | // If we have a custom injector, we need to wrap the cradle. 477 | const cradle = this.injector 478 | ? createInjectorProxy(container, this.injector) 479 | : container.cradle 480 | 481 | // Return the target injected with the cradle 482 | return fn(cradle) 483 | } 484 | 485 | // We have dependencies so we need to resolve them manually 486 | if (dependencies.length > 0) { 487 | const resolve = this.injector 488 | ? wrapWithLocals(container, this.injector(container)) 489 | : container.resolve 490 | 491 | const children = dependencies.map((p) => 492 | resolve(p.name, { allowUnregistered: p.optional }), 493 | ) 494 | return fn(...children) 495 | } 496 | 497 | return fn() 498 | } 499 | } 500 | 501 | /** 502 | * Parses the dependencies from the given function. 503 | * If it's a class that extends another class, and it does 504 | * not have a defined constructor, attempt to parse it's super constructor. 505 | */ 506 | function parseDependencies(fn: Function): Array { 507 | const result = parseParameterList(fn.toString()) 508 | if (!result) { 509 | // No defined constructor for a class, check if there is a parent 510 | // we can parse. 511 | const parent = Object.getPrototypeOf(fn) 512 | if (typeof parent === 'function' && parent !== Function.prototype) { 513 | // Try to parse the parent 514 | return parseDependencies(parent) 515 | } 516 | return [] 517 | } 518 | 519 | return result 520 | } 521 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { createTokenizer } from './function-tokenizer' 2 | import { Constructor } from './resolvers' 3 | 4 | /** 5 | * Quick flatten utility to flatten a 2-dimensional array. 6 | * 7 | * @param {Array>} array 8 | * The array to flatten. 9 | * 10 | * @return {Array} 11 | * The flattened array. 12 | */ 13 | export function flatten(array: Array>): Array { 14 | const result: Array = [] 15 | array.forEach((arr) => { 16 | arr.forEach((item) => { 17 | result.push(item) 18 | }) 19 | }) 20 | 21 | return result 22 | } 23 | 24 | /** 25 | * Creates a { name: value } object if the input isn't already in that format. 26 | * 27 | * @param {string|object} name 28 | * Either a string or an object. 29 | * 30 | * @param {*} value 31 | * The value, only used if name is not an object. 32 | * 33 | * @return {object} 34 | */ 35 | export function nameValueToObject( 36 | name: string | symbol | object, 37 | value?: any, 38 | ): Record { 39 | const obj = name 40 | if (typeof obj === 'string' || typeof obj === 'symbol') { 41 | return { [name as any]: value } 42 | } 43 | 44 | return obj 45 | } 46 | 47 | /** 48 | * Returns the last item in the array. 49 | * 50 | * @param {*[]} arr 51 | * The array. 52 | * 53 | * @return {*} 54 | * The last element. 55 | */ 56 | export function last(arr: Array): T { 57 | return arr[arr.length - 1] 58 | } 59 | 60 | /** 61 | * Determines if the given function is a class. 62 | * 63 | * @param {Function} fn 64 | * @return {boolean} 65 | */ 66 | export function isClass( 67 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 68 | fn: Function | Constructor, 69 | ): boolean { 70 | if (typeof fn !== 'function') { 71 | return false 72 | } 73 | 74 | // Should only need 2 tokens. 75 | const tokenizer = createTokenizer(fn.toString()) 76 | const first = tokenizer.next() 77 | if (first.type === 'class') { 78 | return true 79 | } 80 | 81 | const second = tokenizer.next() 82 | if (first.type === 'function' && second.value) { 83 | if (second.value[0] === second.value[0].toUpperCase()) { 84 | return true 85 | } 86 | } 87 | 88 | return false 89 | } 90 | 91 | /** 92 | * Determines if the given value is a function. 93 | * 94 | * @param {unknown} val 95 | * Any value to check if it's a function. 96 | * 97 | * @return {boolean} 98 | * true if the value is a function, false otherwise. 99 | */ 100 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 101 | export function isFunction(val: unknown): val is Function { 102 | return typeof val === 'function' 103 | } 104 | 105 | /** 106 | * Returns the unique items in the array. 107 | * 108 | * @param {Array} 109 | * The array to remove dupes from. 110 | * 111 | * @return {Array} 112 | * The deduped array. 113 | */ 114 | export function uniq(arr: Array): Array { 115 | return Array.from(new Set(arr)) 116 | } 117 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["__tests__", "**/__tests__/*", "node_modules/*", "examples"], 4 | "compilerOptions": { 5 | "forceConsistentCasingInFileNames": false, 6 | "baseUrl": "src" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "lib"], 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./lib", 6 | "strict": true, 7 | "strictNullChecks": true, 8 | "experimentalDecorators": true, 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true, 11 | "target": "ES2021", 12 | "esModuleInterop": true, 13 | "lib": ["ES2021"], 14 | "module": "commonjs", 15 | "moduleResolution": "node", 16 | "importHelpers": false, 17 | "sourceMap": true, 18 | "declaration": true, 19 | "skipLibCheck": true, 20 | "forceConsistentCasingInFileNames": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------