├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── brandi.code-workspace ├── docs ├── brandi-react │ ├── container-provider.md │ ├── create-injection-hooks.md │ ├── overview.md │ ├── tagged.md │ └── use-injection.md ├── examples │ └── basic-examples.md ├── getting-started │ ├── installation.md │ └── overview.md └── reference │ ├── api-reference.md │ ├── binding-scopes.md │ ├── binding-types.md │ ├── conditional-bindings.md │ ├── container.md │ ├── dependency-modules.md │ ├── hierarchical-containers.md │ ├── optional-dependencies.md │ └── pointers-and-registrators.md ├── jest.config.base.js ├── jest.config.js ├── package-lock.json ├── package.json ├── packages ├── brandi-react │ ├── .eslintignore │ ├── .prettierignore │ ├── LICENSE │ ├── README.md │ ├── build.config.js │ ├── jest.config.js │ ├── package.json │ ├── spec │ │ ├── __snapshots__ │ │ │ └── container.spec.tsx.snap │ │ ├── conditions.spec.tsx │ │ ├── container.spec.tsx │ │ └── injection.spec.tsx │ ├── src │ │ ├── conditions │ │ │ ├── ConditionsContext.ts │ │ │ ├── ConditionsProvider.tsx │ │ │ ├── index.ts │ │ │ ├── tagged.tsx │ │ │ └── useConditions.ts │ │ ├── container │ │ │ ├── ContainerContext.ts │ │ │ ├── ContainerProvider.tsx │ │ │ ├── index.ts │ │ │ └── useContainer.ts │ │ ├── index.ts │ │ └── injection │ │ │ ├── createInjectionHooks.ts │ │ │ ├── index.ts │ │ │ └── useInjection.ts │ └── tsconfig.typings.json └── brandi │ ├── .eslintignore │ ├── .prettierignore │ ├── LICENSE │ ├── README.md │ ├── build.config.js │ ├── jest.config.js │ ├── package.json │ ├── spec │ ├── TokenTypeMap.spec.ts │ ├── __snapshots__ │ │ ├── conditional-bindings.spec.ts.snap │ │ ├── container.spec.ts.snap │ │ └── toInstance.spec.ts.snap │ ├── conditional-bindings.spec.ts │ ├── container.spec.ts │ ├── dependency-modules.spec.ts │ ├── injected.spec.ts │ ├── optional-dependencies.spec.ts │ ├── tagged.spec.ts │ ├── toConstant.spec.ts │ ├── toFactory.spec.ts │ ├── toInstance.spec.ts │ └── utils.ts │ ├── src │ ├── container │ │ ├── BindingsVault.ts │ │ ├── Container.ts │ │ ├── DependencyModule.ts │ │ ├── ResolutionCache.ts │ │ ├── bindings │ │ │ ├── Binding.ts │ │ │ ├── ConstantBinding.ts │ │ │ ├── FactoryBinding.ts │ │ │ ├── InstanceBinding.ts │ │ │ └── index.ts │ │ ├── createContainer.ts │ │ ├── createDependencyModule.ts │ │ ├── index.ts │ │ └── syntax │ │ │ ├── BindOrUseSyntax.ts │ │ │ ├── FromSyntax.ts │ │ │ ├── ScopeSyntax.ts │ │ │ ├── TypeSyntax.ts │ │ │ ├── WhenSyntax.ts │ │ │ └── index.ts │ ├── index.ts │ ├── pointers │ │ ├── index.ts │ │ ├── tag.ts │ │ └── token.ts │ ├── registrators │ │ ├── index.ts │ │ ├── injected.ts │ │ └── tagged.ts │ ├── registries │ │ ├── callableRegistry.ts │ │ ├── index.ts │ │ ├── injectsRegistry.ts │ │ └── tagsRegistry.ts │ └── types.ts │ └── tsconfig.typings.json ├── tsconfig.json └── website ├── .eslintignore ├── .gitignore ├── .prettierignore ├── README.md ├── babel.config.js ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src ├── css │ └── custom.css ├── js │ └── codeTheme.js └── pages │ ├── index.module.css │ └── index.tsx ├── static ├── .nojekyll └── images │ ├── brandi.svg │ ├── cubes.svg │ ├── favicon.ico │ ├── lightning.svg │ ├── ok.svg │ └── weapon.svg └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/build 3 | **/lib 4 | **/coverage 5 | **/.docusaurus 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | extends: [ 5 | 'airbnb-typescript', 6 | 'airbnb/hooks', 7 | 'prettier', 8 | 'prettier/react', 9 | 'prettier/@typescript-eslint', 10 | ], 11 | parserOptions: { 12 | project: path.join(__dirname, 'tsconfig.json'), 13 | }, 14 | rules: { 15 | 'max-classes-per-file': 'off', 16 | 'no-console': ['error', { allow: ['warn', 'error'] }], 17 | 'sort-imports': [ 18 | 'error', 19 | { 20 | ignoreCase: false, 21 | ignoreDeclarationSort: false, 22 | ignoreMemberSort: false, 23 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 24 | allowSeparatedGroups: true, 25 | }, 26 | ], 27 | 'no-underscore-dangle': 'off', 28 | 'import/prefer-default-export': 'off', 29 | 'react/prop-types': 'off', 30 | 'react/jsx-props-no-spreading': 'off', 31 | '@typescript-eslint/explicit-member-accessibility': [ 32 | 'error', 33 | { 34 | accessibility: 'explicit', 35 | overrides: { 36 | constructors: 'no-public', 37 | }, 38 | }, 39 | ], 40 | }, 41 | overrides: [ 42 | { 43 | files: ['**/*.spec.ts', '**/*.spec.tsx'], 44 | env: { 'jest/globals': true }, 45 | plugins: ['jest'], 46 | extends: ['plugin:jest/all'], 47 | rules: { 48 | 'import/no-extraneous-dependencies': 'off', 49 | 'jest/prefer-expect-assertions': [ 50 | 'error', 51 | { onlyFunctionsWithAsyncKeyword: true }, 52 | ], 53 | 'jest/no-hooks': [ 54 | 'error', 55 | { 56 | allow: ['beforeAll', 'afterEach'], 57 | }, 58 | ], 59 | 'jest/lowercase-name': ['error', { ignore: ['describe'] }], 60 | }, 61 | }, 62 | ], 63 | }; 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/build 3 | **/lib 4 | **/coverage 5 | **/.docusaurus 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "quoteProps": "consistent", 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright Vladimir Lewandowski (https://vovaspace.com/) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING 13 | FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE 15 | OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🥃 Brandi 2 | 3 | **Brandi** is a dependency injection container powered by TypeScript. 4 | 5 | - **Framework agnostic.** Can work with any UI or server framework. 6 | - **Lightweight and Effective.** It is tiny and designed for maximum performance. 7 | - **Strongly typed.** TypeScript support out of box. 8 | - **Decorators free.** Does not require additional parameters in `tsconfig.json` and `Reflect` polyfill. 9 | 10 | ## Packages 11 | 12 | The Brandi packages are available for use with a module bundler or in a Node application. 13 | 14 | The Brandi source code is written in TypeScript but we precompile both CommonJS and ESModule builds to **ES2018**. 15 | 16 | Additionally, we provide builds precompiled to **ESNext** by `esnext`, `esnext:main` and `esnext:module` fields. 17 | 18 | **TypeScript** type definitions are **included** in the library and do not need to be installed additionally. 19 | 20 | ### Brandi 21 | 22 | The core library. 23 | 24 | [![License](https://img.shields.io/npm/l/brandi.svg)](https://github.com/vovaspace/brandi/blob/main/packages/brandi/LICENSE) 25 | [![NPM Version](https://img.shields.io/npm/v/brandi.svg)](https://www.npmjs.com/package/brandi) 26 | [![Minzipped Size](https://img.shields.io/bundlephobia/minzip/brandi.svg)](https://bundlephobia.com/result?p=brandi) 27 | 28 | ```bash 29 | # NPM 30 | npm install brandi 31 | ``` 32 | 33 | ```bash 34 | # Yarn 35 | yarn add brandi 36 | ``` 37 | 38 | ### Brandi-React 39 | 40 | The React bindings layer. It lets your React components get dependencies from Brandi containers. 41 | 42 | [![License](https://img.shields.io/npm/l/brandi-react.svg)](https://github.com/vovaspace/brandi/blob/main/packages/brandi-react/LICENSE) 43 | [![NPM Version](https://img.shields.io/npm/v/brandi-react.svg)](https://www.npmjs.com/package/brandi-react) 44 | [![Minzipped Size](https://img.shields.io/bundlephobia/minzip/brandi-react.svg)](https://bundlephobia.com/result?p=brandi-react) 45 | 46 | Brandi-React requires **React 16.8 or later**. 47 | You'll also need to [install](https://brandi.js.org/getting-started/installation) Brandi. 48 | 49 | ```bash 50 | # NPM 51 | npm install brandi-react 52 | ``` 53 | 54 | ```bash 55 | # Yarn 56 | yarn add brandi-react 57 | ``` 58 | 59 | ## No Dependencies 60 | 61 | Brandi has no dependencies, but requires the following globals in order to work: 62 | 63 | - `Symbol` 64 | - `WeakMap` 65 | 66 | ## Production 67 | 68 | By default, Brandi will be in development mode. The development mode includes warnings about common mistakes 69 | and `capture()/restore()` `Container` methods. 70 | 71 | Don't forget to set `process.env.NODE_ENV` to `production` when deploying your application. 72 | 73 | ## Documentation 74 | 75 | You can find the Brandi documentation on the [website](https://brandi.js.org). 76 | 77 | The documentation is divided into several sections: 78 | 79 | - Getting Started 80 | - [Overview](https://brandi.js.org/getting-started) 81 | - [Installation](https://brandi.js.org/getting-started/installation) 82 | - Reference 83 | - [API Reference](https://brandi.js.org/reference) 84 | - [Pointers and Registrators](https://brandi.js.org/reference/pointers-and-registrators) 85 | - [Container](https://brandi.js.org/reference/container) 86 | - [Binding Types](https://brandi.js.org/reference/binding-types) 87 | - [Binding Scopes](https://brandi.js.org/reference/binding-scopes) 88 | - [Optional Dependencies](https://brandi.js.org/reference/optional-dependencies) 89 | - [Dependency Modules](https://brandi.js.org/reference/dependency-modules) 90 | - [Hierarchical Containers](https://brandi.js.org/reference/hierarchical-containers) 91 | - [Conditional Bindings](https://brandi.js.org/reference/conditional-bindings) 92 | - Brandi-React 93 | - [Overview](https://brandi.js.org/brandi-react) 94 | - [`ContainerProvider`](https://brandi.js.org/brandi-react/container-provider) 95 | - [`useInjection`](https://brandi.js.org/brandi-react/use-injection) 96 | - [`createInjectionHooks`](https://brandi.js.org/brandi-react/create-injection-hooks) 97 | - [`tagged`](https://brandi.js.org/brandi-react/tagged) 98 | - Examples 99 | - [Basic Examples](https://brandi.js.org/examples) 100 | 101 | ## Examples 102 | 103 | Here are just basic examples. 104 | 105 | 106 | 107 | ### Getting Instance 108 | 109 | **Binding types** and **scopes** are detailed in [Binding Types](https://brandi.js.org/reference/binding-types) 110 | and [Binding Scopes](https://brandi.js.org/reference/binding-scopes) sections of the documentation. 111 | 112 | 113 | ```typescript 114 | import { Container, token } from 'brandi'; 115 | 116 | class ApiService {} 117 | 118 | const TOKENS = { 119 | /* ↓ Creates a typed token. */ 120 | apiService: token('apiService'), 121 | }; 122 | 123 | const container = new Container(); 124 | 125 | container 126 | .bind(TOKENS.apiService) 127 | .toInstance(ApiService) /* ← Binds the token to an instance */ 128 | .inTransientScope(); /* ← in transient scope. */ 129 | 130 | /* ↓ Gets the instance from the container. */ 131 | const apiService = container.get(TOKENS.apiService); 132 | 133 | expect(apiService).toBeInstanceOf(ApiService); 134 | ``` 135 | 136 | 137 | ### Snapshoting 138 | 139 | 140 | ```typescript 141 | import { Container, token } from 'brandi'; 142 | 143 | const TOKENS = { 144 | apiKey: token('API Key'), 145 | }; 146 | 147 | const container = new Container(); 148 | 149 | container 150 | .bind(TOKENS.apiKey) 151 | .toConstant('#key9428'); /* ← Binds the token to some string. */ 152 | 153 | /* ↓ Captures (snapshots) the current container state. */ 154 | container.capture(); 155 | 156 | container 157 | .bind(TOKENS.apiKey) 158 | .toConstant('#testKey'); /* ← Binds the same token to another value. */ 159 | /* For example, this can be used in testing. */ 160 | 161 | const testKey = container.get(TOKENS.apiKey); 162 | 163 | /* ↓ Restores the captured container state. */ 164 | container.restore(); 165 | 166 | const originalKey = container.get(TOKENS.apiKey); 167 | 168 | expect(testKey).toBe('#testKey'); 169 | expect(originalKey).toBe('#key9428'); 170 | ``` 171 | 172 | 173 | Other `Container` methods are detailed 174 | in [Container](https://brandi.js.org/reference/container) section of the documentation. 175 | 176 | ### Hierarchical Containers 177 | 178 | Hierarchical containers are detailed 179 | in [Hierarchical Containers](https://brandi.js.org/reference/hierarchical-containers) section of the documentation. 180 | 181 | ```typescript 182 | import { Container, token } from 'brandi'; 183 | 184 | class ApiService {} 185 | 186 | const TOKENS = { 187 | apiService: token('apiService'), 188 | }; 189 | 190 | const parentContainer = new Container(); 191 | 192 | parentContainer 193 | .bind(TOKENS.apiService) 194 | .toInstance(ApiService) 195 | .inTransientScope(); 196 | 197 | /* ↓ Creates a container with the parent. */ 198 | const childContainer = new Container().extend(parentContainer); 199 | 200 | /** ↓ That container can't satisfy the getting request, 201 | * it passes it along to its parent container. 202 | * The intsance will be gotten from the parent container. 203 | */ 204 | const apiService = childContainer.get(TOKENS.apiService); 205 | 206 | expect(apiService).toBeInstanceOf(ApiService); 207 | ``` 208 | -------------------------------------------------------------------------------- /brandi.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "editor.tabSize": 2, 9 | "editor.formatOnSave": true, 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/brandi-react/container-provider.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: container-provider 3 | title: ContainerProvider 4 | sidebar_label: ContainerProvider 5 | --- 6 | 7 | The `ContainerProvider` component makes the Brandi container available to any nested components that need to use injections. 8 | 9 | The [`useInjection`](./use-injection.md) hook can then access the provided container via React's Context. 10 | 11 | ## Props 12 | 13 | - `container`: [`Container`](../reference/container.md#container) — the provider will not pass the orginal container, 14 | but its clone received from the [`Container.clone()`](../reference/container.md#clone) method 15 | (This can be useful together with using the [container scope](../reference/binding-scopes.md#incontainerscope)). 16 | This behavior is implemented so that the provider can safely [extends](../reference/container#extendcontainer) the container 17 | with a container received from the upstream provider. 18 | In this way, we can implement a [hierarchical DI system](../reference/hierarchical-containers.md) 19 | based on a hierarchy of React components. 20 | - `[isolated]`: `boolean` — as mentioned above, the provider extends the container 21 | with a container received from the upstream provider. 22 | `isolated` prop disables this behavior. 23 | - `children`: `ReactNode`. 24 | 25 | ## Examples 26 | 27 | ### Providing Root Container 28 | 29 | ```tsx 30 | import { createContainer } from 'brandi'; 31 | import { ContainerProvider } from 'brandi-react'; 32 | import React from 'react'; 33 | import ReactDOM from 'react-dom'; 34 | 35 | import { TOKENS } from './tokens'; 36 | import { ApiService } from './ApiService'; 37 | import { App } from './App'; 38 | 39 | const container = createContainer(); 40 | 41 | container.bind(TOKENS.apiService).toInstance(ApiService).inTransientScope(); 42 | 43 | ReactDOM.render( 44 | 45 | 46 | , 47 | document.getElementById('root'), 48 | ); 49 | ``` 50 | 51 | ### Providing a Module's Own Container 52 | 53 | The container of this module will automatically access the root container from the previous example. 54 | Thus the module components can request a dependency by `TOKENS.apiService` token and get it from the parent container. 55 | 56 | ```tsx 57 | import { createContainer } from 'brandi'; 58 | import { ContainerProvider } from 'brandi-react'; 59 | import { FunctionComponent } from 'react'; 60 | 61 | import { TOKENS } from '../tokens'; 62 | 63 | import { UserService } from './UserService'; 64 | import { UserComponent } from './UserComponent'; 65 | 66 | const container = createContainer(); 67 | 68 | container.bind(TOKENS.userService).toInstance(UserService).inTransientScope(); 69 | 70 | export const UserModule: FunctionComponent = () => ( 71 | 72 | 73 | , 74 | ); 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/brandi-react/create-injection-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: create-injection-hooks 3 | title: createInjectionHooks 4 | sidebar_label: createInjectionHooks 5 | --- 6 | 7 | The `createInjectionHooks(...tokens)` creates hooks for getting dependencies more easily. 8 | 9 | ## Arguments 10 | 11 | 1. `...tokens`: [`TokenValue[]`](../reference/pointers-and-registrators.md#tokentdescription). 12 | 13 | ## Returns 14 | 15 | `(() => TokenType)[]` — an array of hooks for getting dependencies. 16 | 17 | ## Example 18 | 19 | 20 | ```typescript title="hooks.ts" 21 | import { createInjectionHooks } from 'brandi-react'; 22 | 23 | import { TOKENS } from './tokens'; 24 | 25 | const [ 26 | useApiService, 27 | useUserService, 28 | useLogger, 29 | ] = createInjectionHooks( 30 | TOKENS.apiService, 31 | TOKENS.userService, 32 | TOKENS.logger.optional, 33 | ); 34 | 35 | export { useApiService, useUserService, useLogger }; 36 | ``` 37 | 38 | 39 | ```tsx title="UserComponent.tsx" 40 | import { FunctionComponent } from 'react'; 41 | 42 | import { useUserService } from './hooks'; 43 | 44 | export const UserComponent: FunctionComponent = () => { 45 | const userService = useUserService(); 46 | 47 | /* ... */ 48 | 49 | return (/* ... */); 50 | } 51 | ``` 52 | 53 | This `UserComponent` is the same as `UserComponent` 54 | in [the example](./use-injection.md#example) in `useInjection` section. 55 | 56 | For more information about `TOKENS.logger.optional` syntax, 57 | see the [Optional Dependencies](../reference/optional-dependencies.md) documentation section. 58 | -------------------------------------------------------------------------------- /docs/brandi-react/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Brandi-React 4 | sidebar_label: Overview 5 | slug: /brandi-react 6 | --- 7 | 8 | **Brandi-React** is the React bindings layer for Brandi. 9 | 10 | It lets your React components get dependencies from Brandi containers. 11 | 12 | ## Installation 13 | 14 | Brandi-React requires **React 16.8 or later**. 15 | You'll also need to [install](../getting-started/installation.md) Brandi. 16 | 17 | ```bash 18 | # NPM 19 | npm install brandi-react 20 | ``` 21 | 22 | ```bash 23 | # Yarn 24 | yarn add brandi-react 25 | ``` 26 | 27 | The Brandi-React source code is written in TypeScript but we precompile both CommonJS and ESModule builds to **ES2018**. 28 | 29 | Additionally, we provide builds precompiled to **ESNext** by `esnext`, `esnext:main` and `esnext:module` fields. 30 | 31 | **TypeScript** type definitions are **included** in the library and do not need to be installed additionally. 32 | 33 | ## API Reference 34 | 35 | - [`ContainerProvider`](./container-provider.md) — 36 | makes the Brandi container available to any nested components that need to use injections. 37 | - [`useInjection(token)`](./use-injection.md) — allows you to get a dependency from a container. 38 | - [`createInjectionHooks(...tokens)`](./create-injection-hooks.md) — creates hooks for getting dependencies more easily. 39 | - [`tagged(...tags)(Component, [options])`](./tagged.md) — attaches tags to the component and all nested components. 40 | -------------------------------------------------------------------------------- /docs/brandi-react/tagged.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: tagged 3 | title: tagged 4 | sidebar_label: tagged 5 | --- 6 | 7 | The `tagged(...tags)(Component, [options])` HoC attaches tags to the component and its child components. 8 | 9 | Conditional Bindings are detailed 10 | in [Conditional Bindings](../reference/conditional-bindings.md) section of the documentation. 11 | 12 | ## Arguments 13 | 14 | ### tagged(...tags) 15 | 16 | 1. `...tags`: [`Tag[]`](../reference/pointers-and-registrators.md#tagdescription) — tags to be attached to the component. 17 | 18 | ### tagged(...tags)(Component, [isolated]) 19 | 20 | 1. `Component`: `React.ComponentType` — component to be wrapped. 21 | 2. `[options]`: `TaggedOptions` 22 | - `isolated`: `boolean` — by default, the wrapped component and its child components inherit tags 23 | from from the upstream tagged components. You can use `isolated` option to disable this behavior. 24 | 25 | ## Returns 26 | 27 | `React.ComponentType` — the wrapped component. 28 | 29 | ## Example 30 | 31 | ```typescript title="tags.ts" 32 | import { tag } from 'brandi'; 33 | import { tagged } from 'brandi-react'; 34 | 35 | export const TAGS = { 36 | offline: tag('offline'), 37 | }; 38 | 39 | export const withOfflineTag = tagged(TAGS.offline); 40 | ``` 41 | 42 | 43 | ```tsx title="index.tsx" 44 | import { createContainer } from 'brandi'; 45 | import { ContainerProvider } from 'brandi-react'; 46 | import React from 'react'; 47 | import ReactDOM from 'react-dom'; 48 | 49 | import { TOKENS } from './tokens'; 50 | import { TAGS } from './tags'; 51 | import { ApiService, OfflineApiService } from './ApiService'; 52 | import { App } from './App'; 53 | 54 | const container = createContainer(); 55 | 56 | container 57 | .bind(TOKENS.apiService) 58 | .toInstance(ApiService) 59 | .inTransientScope(); 60 | 61 | container 62 | .when(TAGS.offline) 63 | .bind(TOKENS.apiService) 64 | .toInstance(OfflineApiService) 65 | .inTransientScope(); 66 | 67 | ReactDOM.render( 68 | 69 | 70 | , 71 | document.getElementById('root'), 72 | ); 73 | ``` 74 | 75 | 76 | ```tsx title="UserComponent.tsx" 77 | import { useInjection } from 'brandi-react'; 78 | import { FunctionComponent } from 'react'; 79 | 80 | import { TOKENS } from './tokens'; 81 | 82 | export const UserComponent: FunctionComponent = () => { 83 | /* ↓ Will be `ApiService`. */ 84 | const apiService = useInjection(TOKENS.apiService); 85 | 86 | /* ... */ 87 | 88 | return (/* ... */); 89 | } 90 | ``` 91 | 92 | ```tsx title="SettingsComponent.tsx" 93 | import { useInjection } from 'brandi-react'; 94 | 95 | import { TOKENS } from './tokens'; 96 | import { withOfflineTag } from './tags'; 97 | 98 | /* ↓ Tags the component. */ 99 | export const SettingsComponent = withOfflineTag(() => { 100 | /* ↓ Will be `OfflineApiService`. */ 101 | const apiService = useInjection(TOKENS.apiService); 102 | 103 | /* ... */ 104 | 105 | return (/* ... */); 106 | }); 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/brandi-react/use-injection.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: use-injection 3 | title: useInjection 4 | sidebar_label: useInjection 5 | --- 6 | 7 | The `useInjection(token)` hook allows you to get a dependency 8 | from a container provided through [`ContainerProvider`](./container-provider.md). 9 | 10 | ## Arguments 11 | 12 | 1. `token`: [`TokenValue`](../reference/pointers-and-registrators.md#tokentdescription). 13 | 14 | ## Returns 15 | 16 | `TokenType` — a dependency bound to the token. 17 | 18 | ## Example 19 | 20 | ```tsx 21 | import { useInjection } from 'brandi-react'; 22 | import { FunctionComponent } from 'react'; 23 | 24 | import { TOKENS } from '../tokens'; 25 | 26 | export const UserComponent: FunctionComponent = () => { 27 | const userService = useInjection(TOKENS.userService); 28 | const logger = useInjection(TOKENS.logger.optional); 29 | 30 | /* ... */ 31 | 32 | return (/* ... */); 33 | } 34 | ``` 35 | 36 | The binding of `TOKENS.userService` was shown 37 | in [the example](./container-provider.md#providing-a-modules-own-container) in `ContainerProvider` section. 38 | 39 | For more information about `TOKENS.logger.optional` syntax, 40 | see the [Optional Dependencies](../reference/optional-dependencies.md) documentation section. 41 | -------------------------------------------------------------------------------- /docs/examples/basic-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: basic-examples 3 | title: Basic Examples 4 | sidebar_label: Basic Examples 5 | slug: /examples 6 | --- 7 | 8 | Here are just basic examples. 9 | 10 | ## Getting Instance 11 | 12 | **Binding types** and **scopes** are detailed in [Binding Types](../reference/binding-types.md) 13 | and [Binding Scopes](../reference/binding-scopes.md) sections of the documentation. 14 | 15 | 16 | ```typescript 17 | import { Container, token } from 'brandi'; 18 | 19 | class ApiService {} 20 | 21 | const TOKENS = { 22 | /* ↓ Creates a typed token. */ 23 | apiService: token('apiService'), 24 | }; 25 | 26 | const container = new Container(); 27 | 28 | container 29 | .bind(TOKENS.apiService) 30 | .toInstance(ApiService) /* ← Binds the token to an instance */ 31 | .inTransientScope(); /* ← in transient scope. */ 32 | 33 | /* ↓ Gets the instance from the container. */ 34 | const apiService = container.get(TOKENS.apiService); 35 | 36 | expect(apiService).toBeInstanceOf(ApiService); 37 | ``` 38 | 39 | 40 | ## Snapshoting 41 | 42 | 43 | ```typescript 44 | import { Container, token } from 'brandi'; 45 | 46 | const TOKENS = { 47 | apiKey: token('API Key'), 48 | }; 49 | 50 | const container = new Container(); 51 | 52 | container 53 | .bind(TOKENS.apiKey) 54 | .toConstant('#key9428'); /* ← Binds the token to some string. */ 55 | 56 | /* ↓ Captures (snapshots) the current container state. */ 57 | container.capture(); 58 | 59 | container 60 | .bind(TOKENS.apiKey) 61 | .toConstant('#testKey'); /* ← Binds the same token to another value. */ 62 | /* For example, this can be used in testing. */ 63 | 64 | const testKey = container.get(TOKENS.apiKey); 65 | 66 | /* ↓ Restores the captured container state. */ 67 | container.restore(); 68 | 69 | const originalKey = container.get(TOKENS.apiKey); 70 | 71 | expect(testKey).toBe('#testKey'); 72 | expect(originalKey).toBe('#key9428'); 73 | ``` 74 | 75 | 76 | Other `Container` methods are detailed 77 | in [Container](../reference/container.md) section of the documentation. 78 | 79 | ## Hierarchical Containers 80 | 81 | Hierarchical containers are detailed 82 | in [Hierarchical Containers](../reference/hierarchical-containers.md) section of the documentation. 83 | 84 | ```typescript 85 | import { Container, token } from 'brandi'; 86 | 87 | class ApiService {} 88 | 89 | const TOKENS = { 90 | apiService: token('apiService'), 91 | }; 92 | 93 | const parentContainer = new Container(); 94 | 95 | parentContainer 96 | .bind(TOKENS.apiService) 97 | .toInstance(ApiService) 98 | .inTransientScope(); 99 | 100 | /* ↓ Creates a container with the parent. */ 101 | const childContainer = new Container().extend(parentContainer); 102 | 103 | /** ↓ That container can't satisfy the getting request, 104 | * it passes it along to its parent container. 105 | * The intsance will be gotten from the parent container. 106 | */ 107 | const apiService = childContainer.get(TOKENS.apiService); 108 | 109 | expect(apiService).toBeInstanceOf(ApiService); 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | sidebar_label: Installation 5 | --- 6 | 7 | Brandi is available as a package for use with a module bundler or in a Node application. 8 | 9 | ```bash 10 | # NPM 11 | npm install brandi 12 | ``` 13 | 14 | ```bash 15 | # Yarn 16 | yarn add brandi 17 | ``` 18 | 19 | The Brandi source code is written in TypeScript but we precompile both CommonJS and ESModule builds to **ES2018**. 20 | 21 | Additionally, we provide builds precompiled to **ESNext** by `esnext`, `esnext:main` and `esnext:module` fields. 22 | 23 | ## TypeScript 24 | 25 | TypeScript type definitions are **included** in the library and do not need to be installed additionally. 26 | 27 | ## No Dependencies 28 | 29 | Brandi has no dependencies, but requires the following globals in order to work: 30 | 31 | - `Symbol` 32 | - `WeakMap` 33 | 34 | ## Production 35 | 36 | By default, Brandi will be in development mode. The development mode includes warnings about common mistakes 37 | and `capture()/restore()` `Container` methods. 38 | 39 | Don't forget to set `process.env.NODE_ENV` to `production` when deploying your application. 40 | 41 | ## Brandi-React 42 | 43 | [**Brandi-React**](../brandi-react/overview.md) is the React bindings layer for Brandi. 44 | 45 | It lets your React components get dependencies from Brandi containers. 46 | 47 | ```bash 48 | # NPM 49 | npm install brandi-react 50 | ``` 51 | 52 | ```bash 53 | # Yarn 54 | yarn add brandi-react 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/getting-started/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | sidebar_label: Overview 5 | slug: /getting-started 6 | --- 7 | 8 | **Brandi** is a dependency injection container powered by TypeScript. 9 | 10 | - **Framework agnostic.** Can work with any UI or server framework. 11 | - **Lightweight and Effective.** It is tiny and designed for maximum performance. 12 | - **Strongly typed.** TypeScript support out of box. 13 | - **Decorators free.** Does not require additional parameters in `tsconfig.json` and `Reflect` polyfill. 14 | 15 | ## About Dependency Injection 16 | 17 | [Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) is a design pattern used to implement Inversion of Control (IoC). 18 | Using DI, we move the creation and binding of the dependent objects outside of the class that depends on them. 19 | 20 | **DI Container** is a thing that knows how to create and configure an instance of a class and its dependent objects. 21 | 22 | ## Constructor Injection 23 | 24 | Brandi performs a [Constructor Injection](https://en.wikipedia.org/wiki/Dependency_injection#Constructor_injection) and we have no plans to add other DI types. 25 | -------------------------------------------------------------------------------- /docs/reference/api-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: api-reference 3 | title: API Reference 4 | sidebar_label: API Reference 5 | slug: /reference 6 | --- 7 | 8 | Brandi is designed to be as simple and clear as possible. 9 | 10 | This section documents the complete Brandi API. 11 | 12 | ## Container 13 | 14 | - [`Container`](./container.md) 15 | - [`createContainer()`](./container.md#createcontainer) 16 | 17 | ### `Container` Methods 18 | 19 | - [`bind(token)`](./container.md#bindtoken) 20 | - [`toConstant(value)`](./binding-types.md#toconstantvalue) 21 | - [`toFactory(creator, [initializer])`](./binding-types.md#tofactorycreator-initializer) 22 | - [`toInstance(creator)`](./binding-types.md#toinstancecreator) 23 | - [`inContainerScope()`](./binding-scopes.md#incontainerscope) 24 | - [`inResolutionScope()`](./binding-scopes.md#inresolutionscope) 25 | - [`inSingletonScope()`](./binding-scopes.md#insingletonscope) 26 | - [`inTransientScope()`](./binding-scopes.md#intransientscope) 27 | - [`capture()`](./container.md#capture) 28 | - [`clone()`](./container.md#clone) 29 | - [`extend(container)`](./container.md#extendcontainer) 30 | - [`get(token)`](./container.md#gettoken) 31 | - [`restore()`](./container.md#restore) 32 | - [`use(...tokens)`](./container.md#usetokensfrommodule) 33 | - [`from(module)`](./container.md#usetokensfrommodule) 34 | - [`when(condition)`](./container.md#whencondition) 35 | 36 | ## Dependency Modules 37 | 38 | - [`DependencyModule`](./dependency-modules.md) 39 | - [`createDependencyModule()`](./dependency-modules.md#createdependencymodule) 40 | 41 | ## Pointers 42 | 43 | - [`token(description)`](./pointers-and-registrators.md#tokentdescription) 44 | - [`tag(description)`](./pointers-and-registrators.md#tagdescription) 45 | 46 | ## Registrators 47 | 48 | - [`injected(target, ...tokens)`](./pointers-and-registrators.md#injectedtarget-tokens) 49 | - [`tagged(target, ...tags)`](./pointers-and-registrators.md#taggedtarget-tags) 50 | -------------------------------------------------------------------------------- /docs/reference/binding-scopes.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: binding-scopes 3 | title: Binding Scopes 4 | sidebar_label: Binding Scopes 5 | --- 6 | 7 | This section documents controlling the scope of the dependencies. 8 | 9 | ## `inSingletonScope()` 10 | 11 | Each getting will return the same instance. 12 | 13 | #### Example 14 | 15 | ```typescript 16 | container.bind(TOKENS.apiService).toInstance(ApiService).inSingletonScope(); 17 | 18 | const apiServiceFirst = container.get(TOKENS.apiService); 19 | const apiServiceSecond = container.get(TOKENS.apiService); 20 | 21 | expect(apiServiceFirst).toBe(apiServiceSecond); 22 | ``` 23 | 24 | --- 25 | 26 | ## `inTransientScope()` 27 | 28 | New instance will be created with each getting. 29 | 30 | #### Example 31 | 32 | ```typescript 33 | container.bind(TOKENS.apiService).toInstance(ApiService).inTransientScope(); 34 | 35 | const apiServiceFirst = container.get(TOKENS.apiService); 36 | const apiServiceSecond = container.get(TOKENS.apiService); 37 | 38 | expect(apiServiceFirst).not.toBe(apiServiceSecond); 39 | ``` 40 | 41 | --- 42 | 43 | ## `inContainerScope()` 44 | 45 | The container will return the same instance with each getting. 46 | This is similar to being a singleton, 47 | however if the container has a [child container](./hierarchical-containers.md) or a [clone](./container#clone), 48 | that child container or clone will get an instance unique to it. 49 | 50 | #### Example 51 | 52 | ```typescript 53 | const parentContainer = new Container(); 54 | 55 | parentContainer 56 | .bind(TOKENS.apiService) 57 | .toInstance(ApiService) 58 | .inTransientScope(); 59 | 60 | const childContainer = new Container(parentContainer); 61 | 62 | /* Getting instances from the parent container. */ 63 | const apiServiceParentFirst = parentContainer.get(TOKENS.apiService); 64 | const apiServiceParentSecond = parentContainer.get(TOKENS.apiService); 65 | 66 | /* Getting instances from the child container */ 67 | const apiServiceChildFirst = childContainer.get(TOKENS.apiService); 68 | const apiServiceChildSecond = childContainer.get(TOKENS.apiService); 69 | 70 | /* The instances gotten from the same container are the same. */ 71 | expect(apiServiceParentFirst).toBe(apiServiceParentSecond); 72 | expect(apiServiceChildFirst).toBe(apiServiceChildFirst); 73 | 74 | /* The instances gotten from different containers are different. */ 75 | expect(apiServiceParentFirst).not.toBe(apiServiceChildFirst); 76 | expect(apiServiceParentSecond).not.toBe(apiServiceChildSecond); 77 | ``` 78 | 79 | --- 80 | 81 | ## `inResolutionScope()` 82 | 83 | The same instance will be got for each getting of this dependency during a single resolution chain. 84 | 85 | #### Example 86 | 87 | ```typescript 88 | class EmailService {} 89 | 90 | class SettingsService { 91 | constructor(public emailService: EmailService) {} 92 | } 93 | 94 | class UserService { 95 | constructor( 96 | public settingsService: SettingsService, 97 | public emailService: EmailService, 98 | ) {} 99 | } 100 | 101 | const TOKENS = { 102 | emailService: token('emailService'), 103 | settingsService: token('settingsService'), 104 | userService: token('userService'), 105 | }; 106 | 107 | injected(SettingsService, TOKENS.emailService); 108 | injected(UserService, TOKENS.settingsService, TOKENS.emailService); 109 | 110 | const container = new Container(); 111 | 112 | container 113 | .bind(TOKENS.emailService) 114 | .toInstance(EmailService) 115 | .inResolutionScope(); /* ← `EmailService` in resolution scope. */ 116 | 117 | container 118 | .bind(TOKENS.settingsService) 119 | .toInstance(SettingsService) 120 | .inTransientScope(); 121 | 122 | container.bind(TOKENS.userService).toInstance(UserService).inTransientScope(); 123 | 124 | const userService = container.get(TOKENS.userService); 125 | 126 | /* `EmailService` instances are the same for this resolution chain. */ 127 | expect(userService.emailService).toBe(userService.settingsService.emailService); 128 | ``` 129 | -------------------------------------------------------------------------------- /docs/reference/binding-types.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: binding-types 3 | title: Binding Types 4 | sidebar_label: Binding Types 5 | --- 6 | 7 | ## `toConstant(value)` 8 | 9 | Binds the token to the constant value. 10 | 11 | ### Arguments 12 | 13 | 1. `value`: `TokenType` — the value that will be bound to the token. 14 | 15 | ### Example 16 | 17 | ```typescript 18 | import { Container, token } from 'brandi'; 19 | 20 | const TOKENS = { 21 | apiKey: token('API Key'), 22 | }; 23 | 24 | const container = new Container(); 25 | container.bind(TOKENS.apiKey).toConstant('#key9428'); 26 | 27 | const key = container.get(TOKENS.apiKey); 28 | 29 | expect(key).toBe('#key9428'); 30 | ``` 31 | 32 | --- 33 | 34 | ## `toInstance(creator)` 35 | 36 | Binds the token to an instance in one of the [scopes](./binding-scopes.md). 37 | 38 | ### Arguments 39 | 40 | 1. `creator`: `(new (...args: any[]) => TokenType) | ((...args: any[]) => TokenType)` — 41 | the instance creator that will be bound to the token. 42 | 43 | ### Returns 44 | 45 | [Binding Scope](./binding-scopes.md) syntax: 46 | 47 | - [`inSingletonScope()`](./binding-scopes.md#insingletonscope) 48 | - [`inTransientScope()`](./binding-scopes.md#intransientscope) 49 | - [`inContainerScope()`](./binding-scopes.md#incontainerscope) 50 | - [`inResolutionScope()`](./binding-scopes.md#inresolutionscope) 51 | 52 | ### Examples 53 | 54 | #### Class Instance 55 | 56 | ```typescript 57 | import { Container, token } from 'brandi'; 58 | 59 | class ApiService { 60 | /* ... */ 61 | } 62 | 63 | const TOKENS = { 64 | apiService: token('apiService'), 65 | }; 66 | 67 | const container = new Container(); 68 | container.bind(TOKENS.apiService).toInstance(ApiService).inTransientScope(); 69 | 70 | const apiService = container.get(TOKENS.apiService); 71 | 72 | expect(apiService).toBeInstanceOf(ApiService); 73 | ``` 74 | 75 | #### Function Call Result 76 | 77 | ```typescript 78 | import { Container, token } from 'brandi'; 79 | 80 | interface ApiService { 81 | /* ... */ 82 | } 83 | 84 | const createApiService = (): ApiService => { 85 | /* ... */ 86 | }; 87 | 88 | const TOKENS = { 89 | apiService: token('apiService'), 90 | }; 91 | 92 | const container = new Container(); 93 | container 94 | .bind(TOKENS.apiService) 95 | .toInstance(createApiService) 96 | .inTransientScope(); 97 | 98 | const apiService = container.get(TOKENS.apiService); 99 | 100 | expect(apiService).toStrictEqual({ 101 | /* ... */ 102 | }); 103 | ``` 104 | 105 | ### Injetions 106 | 107 | If the constructor or function has arguments you need to register dependencies 108 | by [`injected`](./pointers-and-registrators.md#injectedtarget-tokens) registrator. 109 | 110 | ```typescript 111 | import { injected } from 'brandi'; 112 | 113 | import { TOKENS } from './tokens'; 114 | import type { HttpClient } from './HttpClient'; 115 | 116 | export class ApiService { 117 | constructor(private http: HttpClient) {} 118 | } 119 | 120 | injected(ApiService, TOKENS.httpClient); 121 | ``` 122 | 123 | --- 124 | 125 | ## `toFactory(creator, [initializer])` 126 | 127 | Binds the token to the factory. 128 | 129 | ### Arguments 130 | 131 | 1. `creator` — the instance creator which the factory will use; 132 | 2. `[initializer]` — optional function called after the instance is created. 133 | 134 | ### Examples 135 | 136 | #### Simple Factory 137 | 138 | ```typescript 139 | import { Container, Factory, token } from 'brandi'; 140 | 141 | class ApiService { 142 | /* ... */ 143 | } 144 | 145 | const TOKENS = { 146 | apiServiceFactory: token>('Factory'), 147 | }; 148 | 149 | const container = new Container(); 150 | 151 | /* ↓ Binds the factory. */ 152 | container.bind(TOKENS.apiServiceFactory).toFactory(ApiService); 153 | 154 | const apiServiceFactory = container.get(TOKENS.apiServiceFactory); 155 | const apiService = apiServiceFactory(); 156 | 157 | expect(apiService).toBeInstanceOf(ApiService); 158 | ``` 159 | 160 | #### Factory With Initializer 161 | 162 | ```typescript 163 | import { Container, Factory, token } from 'brandi'; 164 | 165 | class ApiService { 166 | init() { 167 | /* ... */ 168 | } 169 | 170 | /* ... */ 171 | } 172 | 173 | const TOKENS = { 174 | apiServiceFactory: token>('Factory'), 175 | }; 176 | 177 | const container = new Container(); 178 | 179 | container 180 | .bind(TOKENS.apiServiceFactory) 181 | /* ↓ Binds the factory with the initializer. */ 182 | .toFactory(ApiService, (instance) => instance.init()); 183 | 184 | const apiServiceFactory = container.get(TOKENS.apiServiceFactory); 185 | 186 | /* ↓ The initializer will be called after the instance is created. */ 187 | const apiService = apiServiceFactory(); 188 | 189 | expect(apiService).toBeInstanceOf(ApiService); 190 | ``` 191 | 192 | #### Factory With Arguments 193 | 194 | ```typescript 195 | import { Container, Factory, token } from 'brandi'; 196 | 197 | class ApiService { 198 | public key: string; 199 | 200 | public setKey(key: string) { 201 | this.key = key; 202 | } 203 | 204 | /* ... */ 205 | } 206 | 207 | const TOKENS = { 208 | /* ↓ `Factory` generic with second argument. */ 209 | apiServiceFactory: token>( 210 | 'Factory', 211 | ), 212 | }; 213 | 214 | const container = new Container(); 215 | 216 | container 217 | .bind(TOKENS.apiServiceFactory) 218 | /* ↓ Binds the factory with `apiKey` argument. */ 219 | .toFactory(ApiService, (instance, apiKey) => instance.setKey(apiKey)); 220 | 221 | const apiServiceFactory = container.get(TOKENS.apiServiceFactory); 222 | const apiService = apiServiceFactory('#key9124'); 223 | 224 | expect(apiService).toBeInstanceOf(ApiService); 225 | expect(apiService.key).toBe('#key9124'); 226 | ``` 227 | 228 | #### Functional Factory 229 | 230 | ```typescript 231 | import { Container, Factory, token } from 'brandi'; 232 | 233 | interface ApiService { 234 | /* ... */ 235 | } 236 | 237 | const createApiService = (): ApiService => { 238 | /* ... */ 239 | }; 240 | 241 | const TOKENS = { 242 | apiServiceFactory: token>('Factory'), 243 | }; 244 | 245 | const container = new Container(); 246 | 247 | container.bind(TOKENS.apiServiceFactory).toFactory(createApiService); 248 | 249 | const apiServiceFactory = container.get(TOKENS.apiServiceFactory); 250 | const apiService = apiServiceFactory(); 251 | 252 | expect(apiService).toStrictEqual({ 253 | /* ... */ 254 | }); 255 | ``` 256 | 257 | #### Instance Caching Factory 258 | 259 | ```typescript 260 | import { Container, Factory, token } from 'brandi'; 261 | 262 | class ApiService { 263 | /* ... */ 264 | } 265 | 266 | const TOKENS = { 267 | apiService: token('apiService'), 268 | apiServiceFactory: token>('Factory'), 269 | }; 270 | 271 | const container = new Container(); 272 | 273 | container 274 | .bind(TOKENS.apiService) 275 | .toInstance(ApiService) 276 | .inSingletonScope() /* ← Binds the token to `ApiService` instance in singleton scope. */; 277 | 278 | container 279 | .bind(TOKENS.apiServiceFactory) 280 | .toFactory(() => container.get(TOKENS.apiService)); 281 | 282 | const apiServiceFactory = container.get(TOKENS.apiService); 283 | 284 | const firstApiService = apiServiceFactory(); 285 | const secondApiService = apiServiceFactory(); 286 | 287 | expect(firstApiService).toBe(secondApiService); 288 | ``` 289 | 290 | #### Factory With Async Creator 291 | 292 | ```typescript 293 | import { AsyncFactory, Container, token } from 'brandi'; 294 | 295 | interface ApiService { 296 | /* ... */ 297 | } 298 | 299 | /* ↓ Async creator. */ 300 | const createApiService = async (): Promise => { 301 | /* ... */ 302 | }; 303 | 304 | const TOKENS = { 305 | /* ↓ Token with `AsyncFactory` type. */ 306 | apiServiceFactory: token>( 307 | 'AsyncFactory', 308 | ), 309 | }; 310 | 311 | const container = new Container(); 312 | 313 | container.bind(TOKENS.apiServiceFactory).toFactory(createApiService); 314 | 315 | const apiServiceFactory = container.get(TOKENS.apiServiceFactory); 316 | 317 | /** 318 | * ↓ Will wait for the creation resolution 319 | * and then call the initializer, if there is one. 320 | */ 321 | const apiService = await apiServiceFactory(); 322 | 323 | expect(apiService).toStrictEqual({ 324 | /* ... */ 325 | }); 326 | ``` 327 | 328 | #### Factory With Async Initializer 329 | 330 | ```typescript 331 | import { AsyncFactory, Container, token } from 'brandi'; 332 | 333 | class ApiService { 334 | init(): Promise { 335 | /* ... */ 336 | } 337 | 338 | /* ... */ 339 | } 340 | 341 | const TOKENS = { 342 | /* ↓ Token with `AsyncFactory` type. */ 343 | apiServiceFactory: token>( 344 | 'AsyncFactory', 345 | ), 346 | }; 347 | 348 | const container = new Container(); 349 | 350 | container 351 | .bind(TOKENS.apiServiceFactory) 352 | /* ↓ Returns a `Promise`. */ 353 | .toFactory(createApiService, (instance) => instance.init()); 354 | 355 | const apiServiceFactory = container.get(TOKENS.apiServiceFactory); 356 | 357 | /* ↓ Will wait for the initialization resolution. */ 358 | const apiService = await apiServiceFactory(); 359 | 360 | expect(apiService).toBeInstanceOf(ApiService); 361 | ``` 362 | -------------------------------------------------------------------------------- /docs/reference/conditional-bindings.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: conditional-bindings 3 | title: Conditional Bindings 4 | sidebar_label: Conditional Bindings 5 | --- 6 | 7 | Brandi allows you to use **tags** and **targets** to control which implementation of the abstraction will be injected to a target. 8 | 9 | Creating tokens: 10 | 11 | ```typescript title="tokens.ts" 12 | import { token } from 'brandi'; 13 | 14 | import type { Cacher } from './Cacher'; 15 | import type { UserService } from './UserService'; 16 | import type { SettingsService } from './SettingsService'; 17 | import type { AdminService } from './AdminService'; 18 | 19 | export const TOKENS = { 20 | cacher: token('cacher'), 21 | userService: token('userService'), 22 | settingsService: token('settingsService'), 23 | adminService: token('adminService'), 24 | }; 25 | ``` 26 | 27 | Creating tags: 28 | 29 | ```typescript title="tags.ts" 30 | import { tag } from 'brandi'; 31 | 32 | export const TAGS = { 33 | offline: tag('offline'), 34 | }; 35 | ``` 36 | 37 | Creating two types of cacher with a common interface: 38 | 39 | ```typescript title="Cacher.ts" 40 | export interface Cacher { 41 | /* ... */ 42 | } 43 | 44 | export class OnlineCacher implements Cacher { 45 | /* ... */ 46 | } 47 | 48 | export class LocalCacher implements Cacher { 49 | /* ... */ 50 | } 51 | ``` 52 | 53 | Creating some services with a dependency on the `Cacher`: 54 | 55 | ```typescript title="UserService.ts" 56 | import { injected } from 'brandi'; 57 | 58 | import { TOKENS } from './tokens'; 59 | import { Cacher } from './Cacher'; 60 | 61 | export class UserService { 62 | constructor(private cacher: Cacher) {} 63 | 64 | /* ... */ 65 | } 66 | 67 | injected(UserService, TOKENS.cacher); 68 | ``` 69 | 70 | ```typescript title="SettingsService.ts" 71 | import { injected } from 'brandi'; 72 | 73 | import { TOKENS } from './tokens'; 74 | import { Cacher } from './Cacher'; 75 | 76 | export class SettingsService { 77 | constructor(private cacher: Cacher) {} 78 | 79 | /* ... */ 80 | } 81 | 82 | injected(SettingsService, TOKENS.cacher); 83 | ``` 84 | 85 | ```typescript title="AdminService.ts" 86 | import { injected } from 'brandi'; 87 | 88 | import { TOKENS } from './tokens'; 89 | import { Cacher } from './Cacher'; 90 | 91 | export class AdminService { 92 | constructor(private cacher: Cacher) {} 93 | 94 | /* ... */ 95 | } 96 | 97 | injected(AdminService, TOKENS.cacher); 98 | ``` 99 | 100 | Configuring the container: 101 | 102 | 103 | ```typescript title="container.ts" 104 | import { Container, tagged } from 'brandi'; 105 | 106 | import { TOKENS } from './tokens'; 107 | import { TAGS } from './tags'; 108 | import { OnlineCacher, LocalCacher } from './Cacher'; 109 | import { UserService } from './UserService'; 110 | import { SettingsService } from './SettingsService'; 111 | import { AdminService } from './AdminService'; 112 | 113 | tagged(SettingsService, TAGS.offline); /* ← Tags `SettingsService`. */ 114 | 115 | export const container = new Container(); 116 | 117 | container 118 | .bind(TOKENS.cacher) /* ← Binds to `OnlineCacher` in common cases. */ 119 | .toInstance(OnlineCacher) 120 | .inTransientScope(); 121 | 122 | container 123 | .when(TAGS.offline) /* ← Binds to `LocalCacher` when target tagget by `offline` tag. */ 124 | .bind(TOKENS.cacher) 125 | .toInstance(LocalCacher) 126 | .inTransientScope(); 127 | 128 | container 129 | .when(AdminService) /* ← Binds to `LocalCacher` when target is `AdminService`. */ 130 | .bind(TOKENS.cacher) 131 | .toInstance(LocalCacher) 132 | .inTransientScope(); 133 | 134 | container.bind(TOKENS.userService).toInstance(UserService).inTransientScope(); 135 | container.bind(TOKENS.settingsService).toInstance(SettingsService).inTransientScope(); 136 | container.bind(TOKENS.adminService).toInstance(AdminService).inTransientScope(); 137 | ``` 138 | 139 | 140 | Dependencies are injected into services based on the tag: 141 | 142 | ```typescript title="index.ts" 143 | import { TOKENS } from './tokens'; 144 | import { OnlineCacher, LocalCacher } from './Cacher'; 145 | import { container } from './container'; 146 | 147 | const userService = container.get(TOKENS.userService); 148 | const settingsService = container.get(TOKENS.settingsService); 149 | const adminService = container.get(TOKENS.adminService); 150 | 151 | expect(userService.cacher).toBeInstanceOf(OnlineCacher); 152 | expect(settingsService.cacher).toBeInstanceOf(LocalCacher); 153 | expect(adminService.cacher).toBeInstanceOf(LocalCacher); 154 | ``` 155 | -------------------------------------------------------------------------------- /docs/reference/container.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: container 3 | title: Container 4 | sidebar_label: Container 5 | --- 6 | 7 | `Container` is a Brandi class whose instances store information about dependencies, 8 | resolve dependencies, and inject dependencies into targets. 9 | 10 | ## `Container` Methods 11 | 12 | ### `bind(token)` 13 | 14 | Binds the token to an implementation. 15 | 16 | #### Arguments 17 | 18 | 1. `token`: [`Token`](./pointers-and-registrators.md#tokentdescription) — a token to be bound. 19 | 20 | #### Returns 21 | 22 | [Binding Type](./binding-types.md) syntax: 23 | 24 | - [`toConstant(value)`](./binding-types.md#toconstantvalue) 25 | - [`toInstance(creator)`](./binding-types.md#toinstancecreator) 26 | - [`toFactory(creator, [initializer])`](./binding-types.md#tofactorycreator-initializer) 27 | 28 | --- 29 | 30 | ### `use(...tokens).from(module)` 31 | 32 | Uses bindings from a [dependency module](./dependency-modules.md). 33 | 34 | #### Arguments 35 | 36 | ##### `use(...tokens)` 37 | 38 | 1. `...tokens`: `Token[]` — tokens to be used from a dependency module. 39 | 40 | ##### `use(...tokens).from(module)` 41 | 42 | 1. `module`: [`DependencyModule`](./dependency-modules.md) — a dependency module. 43 | 44 | --- 45 | 46 | ### `when(condition)` 47 | 48 | Creates a [conditional binding](./conditional-bindings.md). 49 | 50 | #### Arguments 51 | 52 | 1. `condition`: [`Tag`](./pointers-and-registrators.md#tagdescription) | `UnknownCreator` — a condition. 53 | 54 | #### Returns 55 | 56 | `bind` or `use` syntax: 57 | 58 | - [`bind(token)`](#bindtoken) 59 | - [`use(...tokens)`](#usetokensfrommodule) 60 | 61 | --- 62 | 63 | ### `extend(container)` 64 | 65 | Sets the parent container. For more information, see [Hierarchical Containers](./hierarchical-containers.md) section. 66 | 67 | #### Arguments 68 | 69 | 1. `container`: `Container | null` — a `Container` or `null` that will be set as the parent container. 70 | 71 | #### Returns 72 | 73 | `this` — the container. 74 | 75 | --- 76 | 77 | ### `get(token)` 78 | 79 | Gets a dependency bound to the token. 80 | 81 | #### Arguments 82 | 83 | 1. `token`: [`TokenValue`](./pointers-and-registrators.md#tokentdescription) — token for which a dependence will be got. 84 | 85 | #### Returns 86 | 87 | `TokenType` — a dependency bound to the token. 88 | 89 | --- 90 | 91 | ### `clone()` 92 | 93 | Returns an unlinked clone of the container. 94 | 95 | #### Returns 96 | 97 | `Container` — new container. 98 | 99 | #### Example 100 | 101 | ```typescript 102 | import { Container } from 'brandi'; 103 | 104 | const originalContainer = new Container(); 105 | const containerClone = originalContainer.clone(); 106 | 107 | expect(containerClone).toBeInstanceOf(Container); 108 | expect(containerClone).not.toBe(originalContainer); 109 | ``` 110 | 111 | --- 112 | 113 | ### `capture()` 114 | 115 | Captures (snapshots) the current container state. 116 | 117 | :::note 118 | 119 | The `capture()` method works only in development mode (`process.env.NODE_ENV !== 'production'`). 120 | 121 | `Container.capture` is `undefined` in production mode. 122 | 123 | ::: 124 | 125 | ### `restore()` 126 | 127 | Restores the [captured](#capture) container state. 128 | 129 | :::note 130 | 131 | The `restore()` method works only in development mode (`process.env.NODE_ENV !== 'production'`). 132 | 133 | `Container.restore` is `undefined` in production mode. 134 | 135 | ::: 136 | 137 | #### Example 138 | 139 | ```typescript 140 | import { Container, token } from 'brandi'; 141 | 142 | const TOKENS = { 143 | apiKey: token('API Key'), 144 | }; 145 | 146 | const container = new Container(); 147 | container.bind(TOKENS.apiKey).toConstant('#key9428'); 148 | 149 | container.capture(); 150 | 151 | container.bind(TOKENS.apiKey).toConstant('#testKey'); 152 | 153 | const testKey = container.get(TOKENS.apiKey); 154 | 155 | container.restore(); 156 | 157 | const originalKey = container.get(TOKENS.apiKey); 158 | 159 | expect(testKey).toBe('#testKey'); 160 | expect(originalKey).toBe('#key9428'); 161 | ``` 162 | 163 | --- 164 | 165 | ## `createContainer()` 166 | 167 | `createContainer()` — is alias for `new Container()`. 168 | -------------------------------------------------------------------------------- /docs/reference/dependency-modules.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: dependency-modules 3 | title: Dependency Modules 4 | sidebar_label: Dependency Modules 5 | --- 6 | 7 | By using Brandi, you can organize bindings in dependency modules. 8 | In this section we will see how to create, organize and use your modules. 9 | 10 | `DependencyModule` is a logical space to help you organize your bindings. 11 | It is similar to [`Container`](./container.md), but it can only store dependencies, but not resolve or inject them. 12 | 13 | ## `DependencyModule` Methods 14 | 15 | `DependencyModule` repeats the following [`Container`](./container.md) methods: 16 | 17 | - [`bind(token)`](./container.md#bindtoken) 18 | - [`use(...tokens).from(module)`](./container.md#usetokensfrommodule) 19 | - [`when(condition)`](./container.md#whencondition) 20 | 21 | ## Using Dependency Modules 22 | 23 | ### Simple Example 24 | 25 | Let's take an simple example, with using a binding from a module by the token: 26 | 27 | ```typescript 28 | import { Container, DependencyModule, token } from 'brandi'; 29 | 30 | const TOKENS = { 31 | apiKey: token('apiKey'), 32 | }; 33 | 34 | const apiModule = new DependencyModule(); 35 | apiModule.bind(TOKENS.apiKey).toConstant('#key9428'); 36 | 37 | const container = new Container(); 38 | container.use(TOKENS.apiKey).from(apiModule); 39 | 40 | const key = container.get(TOKENS.apiKey); 41 | 42 | expect(key).toBe('#key9428'); 43 | ``` 44 | 45 | Container will resolve the `apiKey` dependency from the module. 46 | 47 | ### More Complex Example 48 | 49 | In this example, we have the `ApiService` that depends on a `apiKey`, API dependency module, and the `App` that depends on an `ApiService`. 50 | 51 | Let's start with the token declaration: 52 | 53 | ```typescript title="tokens.ts" 54 | import { token } from 'brandi'; 55 | 56 | import type { ApiService } from './api/ApiService'; 57 | import type { App } from './App'; 58 | 59 | export const TOKENS = { 60 | apiKey: token('apiKey'), 61 | apiService: token('apiService'), 62 | app: token('app'), 63 | }; 64 | ``` 65 | 66 | Then we will create the `ApiService` with a dependency on a `apiKey`: 67 | 68 | ```typescript title="api/ApiService.ts" 69 | import { injected } from 'brandi'; 70 | 71 | import { TOKENS } from '../tokens'; 72 | 73 | export class ApiService { 74 | constructor(private apiKey: string) {} 75 | 76 | /* ... */ 77 | } 78 | 79 | injected(ApiService, TOKENS.apiKey); 80 | ``` 81 | 82 | Then we will create the dependency module to which we will bind all the dependencies necessary for the API module: 83 | 84 | ```typescript title="api/module.ts" 85 | import { DependencyModule } from 'brandi'; 86 | 87 | import { TOKENS } from '../tokens'; 88 | 89 | import { ApiService } from './ApiService'; 90 | 91 | export const apiModule = new DependencyModule(); 92 | 93 | apiModule.bind(TOKENS.apiKey).toConstant('#key9428'); 94 | apiModule.bind(TOKENS.apiService).toInstance(ApiService).inTransientScope(); 95 | ``` 96 | 97 | Creating our `App` that depends on `ApiService`: 98 | 99 | ```typescript title="App.ts" 100 | import { injected } from 'brandi'; 101 | 102 | import { TOKENS } from './tokens'; 103 | import type { ApiService } from './api/ApiService'; 104 | 105 | export class App { 106 | constructor(private apiService: ApiService) {} 107 | 108 | /* ... */ 109 | } 110 | 111 | injected(App, TOKENS.apiService); 112 | ``` 113 | 114 | And finally configure the container: 115 | 116 | ```typescript title="container.ts" 117 | import { Container } from 'brandi'; 118 | 119 | import { TOKENS } from './tokens'; 120 | import { apiModule } from './api/module'; 121 | import { App } from './App'; 122 | 123 | export const container = new Container(); 124 | 125 | /** 126 | * ↓ We only use the `apiService` token that the `App` directly depends on. 127 | * The `apiKey` token binding will be resolved from the `apiModule` automatically 128 | * and it does not need to be bound additionally. 129 | */ 130 | container.use(TOKENS.apiService).from(apiModule); 131 | 132 | container.bind(TOKENS.app).toInstance(App).inSingletonScope(); 133 | ``` 134 | 135 | Let's run: 136 | 137 | ```typescript title="index.ts" 138 | import { TOKENS } from './tokens'; 139 | import { container } from './container'; 140 | 141 | const app = container.get(TOKENS.app); 142 | 143 | app.run(); 144 | ``` 145 | 146 | :::note 147 | 148 | Some of your dependency modules may use bindings from other modules. 149 | If there are bindings of the same token in the module chain, the highest-level binding in the hierarchy will be used. 150 | 151 | ::: 152 | 153 | ## `createDependencyModule()` 154 | 155 | `createDependencyModule()` — is alias for `new DependencyModule()`. 156 | -------------------------------------------------------------------------------- /docs/reference/hierarchical-containers.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: hierarchical-containers 3 | title: Hierarchical Containers 4 | sidebar_label: Hierarchical Containers 5 | --- 6 | 7 | Brandi allows you to build hierarchical DI systems. 8 | 9 | In a hierarchical DI system, a container can have a parent container and multiple containers can be used in one application. 10 | The containers form a hierarchical structure. 11 | 12 | When you get a dependency from a container, the container tries to satisfy that dependency with it's own bindings. 13 | If the container lacks bindings, it passes the request up to its parent container. 14 | If that container can't satisfy the request, it passes it along to its parent container. 15 | The requests keep bubbling up until we find an container that can handle the request or run out of container ancestors. 16 | 17 | ```typescript 18 | import { Container, token } from 'brandi'; 19 | 20 | class ApiService {} 21 | 22 | const TOKENS = { 23 | apiService: token('apiService'), 24 | }; 25 | 26 | const parentContainer = new Container(); 27 | 28 | parentContainer 29 | .bind(TOKENS.apiService) 30 | .toInstance(ApiService) 31 | .inTransientScope(); 32 | 33 | const childContainer = new Container().extend(parentContainer); 34 | 35 | const apiService = childContainer.get(TOKENS.apiService); 36 | 37 | expect(apiService).toBeInstanceOf(ApiService); 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/reference/optional-dependencies.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: optional-dependencies 3 | title: Optional Dependencies 4 | sidebar_label: Optional Dependencies 5 | --- 6 | 7 | You can declare an optional dependency using the standard TypeScript syntax: 8 | 9 | ```typescript title="ApiService.ts" 10 | import { injected } from 'brandi'; 11 | import { TOKENS } from './tokens'; 12 | import { Logger } from './Logger'; 13 | 14 | export class ApiService { 15 | /* ↓ The optional dependency. */ 16 | constructor(private logger?: Logger) {} 17 | } 18 | ``` 19 | 20 | And inject it using the `optional` token field: 21 | 22 | ```typescript title="ApiService.ts" 23 | injected( 24 | ApiService, 25 | TOKENS.logger.optional /* ← The optional dependency token. */, 26 | ); 27 | ``` 28 | 29 | When using `optional` tokens, the Brani container will not throw an error 30 | if the dependency is not bound in the container, but will return undefined. 31 | -------------------------------------------------------------------------------- /docs/reference/pointers-and-registrators.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: pointers-and-registrators 3 | title: Pointers and Registrators 4 | sidebar_label: Pointers and Registrators 5 | --- 6 | 7 | ## Pointers 8 | 9 | A pointer in Brandi terminology is a unique value that is used for relating entities. 10 | 11 | ### `token(description)` 12 | 13 | Creates a unique token with the type. 14 | 15 | #### Arguments 16 | 17 | 1. `description`: `string` — a description of the token to be used in logs and error messages. 18 | 19 | #### Returns 20 | 21 | `Token` — a unique token with the type. 22 | 23 | #### Type Safety 24 | 25 | The token mechanism in Brandi provides type safety when binding and getting dependencies. 26 | 27 | ```typescript 28 | import { Container, token } from 'brandi'; 29 | 30 | const TOKENS = { 31 | apiKey: token('API Key') /* ← The token with `string` type. */, 32 | }; 33 | 34 | const container = new Container(); 35 | 36 | /* ↓ Binding the `string` type value. It's OK. */ 37 | container.bind(TOKENS.apiKey).toConstant('#key9428'); 38 | 39 | /** 40 | * ↓ Trying to bind the `string` type token to the `number` type value: 41 | * TS Error: `Argument of type 'number' is not assignable 42 | * to parameter of type 'string'. ts(2345)`. 43 | */ 44 | container.bind(TOKENS.apiKey).toConstant(9428); 45 | 46 | const key = container.get(TOKENS.apiKey); 47 | 48 | type Key = typeof key; /* ← The type is derived from the token. `type Key = string;`. */ 49 | 50 | /* ↓ TS Error: `Type 'string' is not assignable to type 'number'. ts(2322)` */ 51 | const numKey: number = container.get(TOKENS.apiKey); 52 | ``` 53 | 54 | --- 55 | 56 | ### `tag(description)` 57 | 58 | Creates a unique tag. 59 | 60 | #### Arguments 61 | 62 | 1. `description`: `string` — a description of the tag to be used in logs and error messages. 63 | 64 | #### Returns 65 | 66 | `Tag` — a unique tag. 67 | 68 | --- 69 | 70 | ## Registrators 71 | 72 | ### `injected(target, ...tokens)` 73 | 74 | Registers target injections. 75 | 76 | #### Arguments 77 | 78 | 1. `target` — constructor or function whose dependencies will be injected. 79 | 2. `...tokens`: `TokenValue[]` — dependency tokens. 80 | 81 | #### Returns 82 | 83 | `target` — the first argument. 84 | 85 | #### Example 86 | 87 | ```typescript title="ApiService.ts" 88 | import { injected } from 'brandi'; 89 | import { TOKENS } from './tokens'; 90 | import { Logger } from './Logger'; 91 | 92 | export class ApiService { 93 | constructor(private apiKey: string, private logger?: Logger) {} 94 | } 95 | 96 | injected(ApiService, TOKENS.apiKey, TOKENS.logger.optional); 97 | ``` 98 | 99 | #### Type Safety 100 | 101 | Injections in Brandi are strictly typed. 102 | 103 | ```typescript title="tokens.ts" 104 | import { token } from 'brandi'; 105 | 106 | export const TOKENS = { 107 | strKey: token('String API Key'), 108 | numKey: token('Number API Key'), 109 | }; 110 | ``` 111 | 112 | 113 | ```typescript title="ApiService.ts" 114 | import { injected } from 'brandi'; 115 | import { TOKENS } from './tokens'; 116 | import { Logger } from './Logger'; 117 | 118 | export class ApiService { 119 | constructor( 120 | private apiKey: string /* ← The `string` type dependency. */, 121 | private logger?: Logger /* ← The optional dependency. */, 122 | ) {} 123 | } 124 | 125 | injected( 126 | ApiService, 127 | TOKENS.strKey /* ← Injecting the `string` type dependency. It's OK. */, 128 | TOKENS.logger.optional /* ← Injecting the optional dependency. It's OK. */, 129 | ); 130 | 131 | injected( 132 | ApiService, 133 | 134 | TOKENS.numKey, /* ← Injecting the `number` type dependency. 135 | * TS Error: `Type 'number' is not assignable to type 'string'. ts(2345)`. 136 | */ 137 | 138 | TOKENS.logger, /* ← Injecting the required value instead of the optional one. 139 | * TS Error: `Argument of type 'Token' 140 | * is not assignable to parameter of type 'OptionalToken'`. 141 | */ 142 | ); 143 | ``` 144 | 145 | 146 | --- 147 | 148 | ### `tagged(target, ...tags)` 149 | 150 | Tags target. 151 | For more information about tags, see the [Conditional Bindings](./conditional-bindings.md) documentation section. 152 | 153 | #### Arguments 154 | 155 | 1. `target` — constructor or function that will be tagged. 156 | 2. `...tags`: `Tag[]` — tags. 157 | 158 | #### Returns 159 | 160 | `target` — the first argument. 161 | 162 | #### Example 163 | 164 | ```typescript title="ApiService.ts" 165 | import { tagged } from 'brandi'; 166 | import { TAGS } from './tags'; 167 | 168 | export class ApiService {} 169 | 170 | tagged(ApiService, TAGS.offline); 171 | ``` 172 | -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testMatch: ['/spec/*.spec.ts(x)?'], 4 | }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ['/packages/*'], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brandi-workspace", 3 | "private": true, 4 | "workspaces": [ 5 | "./packages/*" 6 | ], 7 | "scripts": { 8 | "test": "jest --colors", 9 | "build:brandi": "cd packages/brandi && npm run build", 10 | "build:brandi-react": "cd packages/brandi-react && npm run build", 11 | "build": "npm run build:brandi && npm run build:brandi-react", 12 | "code:lint": "prettier --check . && eslint .", 13 | "code:fix": "prettier --write . && eslint --fix ." 14 | }, 15 | "license": "ISC", 16 | "author": "Vladimir Lewandowski (https://vovaspace.com/)", 17 | "homepage": "https://brandi.js.org", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/vovaspace/brandi.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/vovaspace/brandi/issues" 24 | }, 25 | "devDependencies": { 26 | "@testing-library/react-hooks": "^5.1.1", 27 | "@types/jest": "^26.0.20", 28 | "@types/react": "^17.0.3", 29 | "@typescript-eslint/eslint-plugin": "^4.16.1", 30 | "esbuild": "^0.8.55", 31 | "eslint": "^7.21.0", 32 | "eslint-config-airbnb": "^18.2.1", 33 | "eslint-config-airbnb-typescript": "^12.3.1", 34 | "eslint-config-prettier": "^7.2.0", 35 | "eslint-plugin-import": "^2.22.1", 36 | "eslint-plugin-jest": "^24.1.5", 37 | "eslint-plugin-jsx-a11y": "^6.4.1", 38 | "eslint-plugin-react": "^7.23.1", 39 | "eslint-plugin-react-hooks": "^4.2.0", 40 | "jest": "^26.6.3", 41 | "prettier": "^2.2.1", 42 | "react-test-renderer": "^17.0.2", 43 | "rimraf": "^3.0.2", 44 | "ts-jest": "^26.5.3", 45 | "typescript": "^4.2.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/brandi-react/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /packages/brandi-react/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /packages/brandi-react/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright Vladimir Lewandowski (https://vovaspace.com/) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING 13 | FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE 15 | OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /packages/brandi-react/README.md: -------------------------------------------------------------------------------- 1 | # 🥃 ⚛️ Brandi-React 2 | 3 | **Brandi-React** is the React bindings layer 4 | for [Brandi](https://github.com/vovaspace/brandi/tree/main/packages/brandi) — the dependency injection container. It lets your React components get dependencies from Brandi containers. 5 | 6 | [![License](https://img.shields.io/npm/l/brandi-react.svg)](https://github.com/vovaspace/brandi/blob/main/packages/brandi-react/LICENSE) 7 | [![NPM Version](https://img.shields.io/npm/v/brandi-react.svg)](https://www.npmjs.com/package/brandi-react) 8 | [![Minzipped Size](https://img.shields.io/bundlephobia/minzip/brandi-react.svg)](https://bundlephobia.com/result?p=brandi-react) 9 | 10 | ## Installation 11 | 12 | Brandi-React requires **React 16.8 or later**. 13 | You'll also need to [install](https://brandi.js.org/getting-started/installation) Brandi. 14 | 15 | ```bash 16 | # NPM 17 | npm install brandi-react 18 | ``` 19 | 20 | ```bash 21 | # Yarn 22 | yarn add brandi-react 23 | ``` 24 | 25 | The Brandi-React source code is written in TypeScript but we precompile both CommonJS and ESModule builds to **ES2018**. 26 | 27 | Additionally, we provide builds precompiled to **ESNext** by `esnext`, `esnext:main` and `esnext:module` fields. 28 | 29 | **TypeScript** type definitions are **included** in the library and do not need to be installed additionally. 30 | 31 | ## API Reference 32 | 33 | - [`ContainerProvider`](https://brandi.js.org/brandi-react/container-provider) — 34 | makes the Brandi container available to any nested components that need to use injections. 35 | - [`useInjection(token)`](https://brandi.js.org/brandi-react/use-injection) — 36 | allows you to get a dependency from a container. 37 | - [`createInjectionHooks(...tokens)`](https://brandi.js.org/brandi-react/create-injection-hooks) — 38 | creates hooks for getting dependencies more easily. 39 | - [`tagged(...tags)(Component, [options])`](https://brandi.js.org/brandi-react/tagged) — 40 | attaches tags to the component and all nested components. 41 | 42 | You can find the full Brandi documentation on the [website](https://brandi.js.org). 43 | 44 | ## Example 45 | 46 | ```tsx 47 | // index.ts 48 | 49 | import { createContainer } from 'brandi'; 50 | import { ContainerProvider } from 'brandi-react'; 51 | import React from 'react'; 52 | import ReactDOM from 'react-dom'; 53 | 54 | import { TOKENS } from './tokens'; 55 | import { ApiService } from './ApiService'; 56 | import { App } from './App'; 57 | 58 | const container = createContainer(); 59 | 60 | container.bind(TOKENS.apiService).toInstance(ApiService).inTransientScope(); 61 | 62 | ReactDOM.render( 63 | 64 | 65 | , 66 | document.getElementById('root'), 67 | ); 68 | ``` 69 | 70 | ```tsx 71 | // UserComponent.tsx 72 | 73 | import { useInjection } from 'brandi-react'; 74 | import { FunctionComponent } from 'react'; 75 | 76 | import { TOKENS } from './tokens'; 77 | 78 | export const UserComponent: FunctionComponent = () => { 79 | const apiService = useInjection(TOKENS.apiService); 80 | 81 | /* ... */ 82 | 83 | return (/* ... */); 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /packages/brandi-react/build.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | const esbuild = require('esbuild'); 4 | 5 | const pkg = require('./package.json'); 6 | 7 | const commons = { 8 | entryPoints: ['./src/index.ts'], 9 | bundle: true, 10 | platform: 'node', 11 | sourcemap: true, 12 | external: ['brandi', 'react'], 13 | }; 14 | 15 | esbuild.build({ 16 | ...commons, 17 | outfile: pkg.main, 18 | format: 'cjs', 19 | target: 'es2018', 20 | }); 21 | 22 | esbuild.build({ 23 | ...commons, 24 | outfile: pkg.module, 25 | format: 'esm', 26 | target: 'es2018', 27 | }); 28 | 29 | esbuild.build({ 30 | ...commons, 31 | outfile: pkg.esnext, 32 | format: 'esm', 33 | target: 'esnext', 34 | }); 35 | -------------------------------------------------------------------------------- /packages/brandi-react/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base, 5 | displayName: 'brandi-react', 6 | }; 7 | -------------------------------------------------------------------------------- /packages/brandi-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brandi-react", 3 | "version": "5.0.0", 4 | "description": "React bindings for Brandi — the dependency injection container.", 5 | "main": "./lib/brandi-react.js", 6 | "module": "./lib/brandi-react.mjs", 7 | "esnext": "./lib/brandi-react.next.js", 8 | "esnext:main": "./lib/brandi-react.next.js", 9 | "esnext:module": "./lib/brandi-react.next.js", 10 | "exports": { 11 | "import": "./lib/brandi-react.mjs", 12 | "require": "./lib/brandi-react.js", 13 | "default": "./lib/brandi-react.js", 14 | "types": "./lib/typings/index.d.ts" 15 | }, 16 | "typings": "./lib/typings/index.d.ts", 17 | "sideEffects": false, 18 | "files": [ 19 | "lib" 20 | ], 21 | "scripts": { 22 | "test": "jest --colors", 23 | "build": "npm run clean && npm run build:lib && npm run build:typings", 24 | "build:lib": "node build.config.js", 25 | "build:typings": "tsc -p ./tsconfig.typings.json", 26 | "clean": "rimraf ./lib", 27 | "code:lint": "prettier --check . && eslint .", 28 | "code:fix": "prettier --write . && eslint --fix ." 29 | }, 30 | "license": "ISC", 31 | "author": "Vladimir Lewandowski (https://vovaspace.com/)", 32 | "homepage": "https://brandi.js.org", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/vovaspace/brandi.git", 36 | "directory": "packages/brandi-react" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/vovaspace/brandi/issues" 40 | }, 41 | "keywords": [ 42 | "react", 43 | "di", 44 | "ioc", 45 | "dependency injection", 46 | "dependency inversion", 47 | "inversion of control", 48 | "container" 49 | ], 50 | "peerDependencies": { 51 | "brandi": "^3 || ^4 || ^5", 52 | "react": "^16.8.0 || ^17 || ^18" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/brandi-react/spec/__snapshots__/container.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`container useContainer throws error when a container is not passed through 'ContainerProvider' 1`] = `[Error: Could not get a container from a context. Did you forget to pass the container through 'ContainerProvider'?]`; 4 | -------------------------------------------------------------------------------- /packages/brandi-react/spec/conditions.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { tag } from 'brandi'; 4 | 5 | import { tagged } from '../src'; 6 | import { useConditions } from '../src/conditions'; 7 | 8 | const tags = { 9 | some: tag('some'), 10 | other: tag('other'), 11 | another: tag('another'), 12 | }; 13 | 14 | const TestComponent: React.FunctionComponent = ({ children }) => ( 15 |
{children}
16 | ); 17 | 18 | describe('conditions', () => { 19 | it('passes tags through a tagged component', () => { 20 | const TaggedComponent = tagged(tags.some, tags.other)(TestComponent); 21 | 22 | const wrapper: React.FunctionComponent = ({ children }) => ( 23 | {children} 24 | ); 25 | 26 | const { result } = renderHook(() => useConditions(), { wrapper }); 27 | 28 | expect(result.current).toStrictEqual([tags.some, tags.other]); 29 | }); 30 | 31 | it('passes unique tags throw nested tagged components', () => { 32 | const ParentTaggedComponent = tagged(tags.some, tags.other)(TestComponent); 33 | const ChildTaggedComponent = tagged( 34 | tags.other, 35 | tags.another, 36 | )(TestComponent); 37 | 38 | const wrapper: React.FunctionComponent = ({ children }) => ( 39 | 40 | {children} 41 | 42 | ); 43 | 44 | const { result } = renderHook(() => useConditions(), { wrapper }); 45 | 46 | expect(result.current).toStrictEqual([tags.some, tags.other, tags.another]); 47 | }); 48 | 49 | it('does not pass parent tags throw a isolated tagged component', () => { 50 | const ParentTaggedComponent = tagged(tags.some, tags.other)(TestComponent); 51 | const ChildTaggedComponent = tagged(tags.another)(TestComponent, { 52 | isolated: true, 53 | }); 54 | 55 | const wrapper: React.FunctionComponent = ({ children }) => ( 56 | 57 | {children} 58 | 59 | ); 60 | 61 | const { result } = renderHook(() => useConditions(), { wrapper }); 62 | 63 | expect(result.current).toStrictEqual([tags.another]); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/brandi-react/spec/container.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Container, createContainer, token } from 'brandi'; 2 | import React from 'react'; 3 | import { renderHook } from '@testing-library/react-hooks'; 4 | 5 | import { ContainerProvider } from '../src'; 6 | import { useContainer } from '../src/container'; 7 | 8 | describe('container', () => { 9 | describe('useContainer', () => { 10 | it("throws error when a container is not passed through 'ContainerProvider'", () => { 11 | const { result } = renderHook(() => useContainer()); 12 | 13 | expect(result.error).toBeInstanceOf(Error); 14 | expect(result.error).toMatchSnapshot(); 15 | }); 16 | 17 | it("returns 'null' in not strict mode when a container is not passed through 'ContainerProvider'", () => { 18 | const { result } = renderHook(() => useContainer(false)); 19 | 20 | expect(result.current).toBeNull(); 21 | }); 22 | }); 23 | 24 | describe('ContainerProvider', () => { 25 | it('passes a container clone', () => { 26 | const TOKENS = { 27 | some: token('some'), 28 | }; 29 | 30 | const someValue = 1; 31 | const anotherValue = 2; 32 | 33 | const container = createContainer(); 34 | container.bind(TOKENS.some).toConstant(someValue); 35 | 36 | const wrapper: React.FunctionComponent = ({ children }) => ( 37 | {children} 38 | ); 39 | 40 | const { result } = renderHook(() => useContainer(), { wrapper }); 41 | 42 | container.bind(TOKENS.some).toConstant(anotherValue); 43 | 44 | expect(result.current).not.toBe(container); 45 | expect(result.current).toBeInstanceOf(Container); 46 | expect(result.current.get(TOKENS.some)).toBe(someValue); 47 | }); 48 | 49 | it('binds the parent container from the parent context', () => { 50 | const TOKENS = { 51 | some: token('some'), 52 | another: token('another'), 53 | }; 54 | 55 | const someValue = 1; 56 | const anotherValue = 2; 57 | 58 | const parentContainer = createContainer(); 59 | parentContainer.bind(TOKENS.some).toConstant(someValue); 60 | 61 | const childContainer = createContainer(); 62 | childContainer.bind(TOKENS.another).toConstant(anotherValue); 63 | 64 | const wrapper: React.FunctionComponent = ({ children }) => ( 65 | 66 | 67 | {children} 68 | 69 | 70 | ); 71 | 72 | const { result } = renderHook(() => useContainer(), { wrapper }); 73 | 74 | expect(result.current.get(TOKENS.some)).toBe(someValue); 75 | expect(result.current.get(TOKENS.another)).toBe(anotherValue); 76 | }); 77 | 78 | it("keeps the original parent container with 'isolated' prop", () => { 79 | const TOKENS = { 80 | some: token('some'), 81 | }; 82 | 83 | const someValue = 1; 84 | const anotherValue = 2; 85 | 86 | const parentContainer = createContainer(); 87 | parentContainer.bind(TOKENS.some).toConstant(someValue); 88 | 89 | const anotherParentContainer = createContainer(); 90 | anotherParentContainer.bind(TOKENS.some).toConstant(anotherValue); 91 | 92 | const childContainer = createContainer().extend(parentContainer); 93 | 94 | const wrapper: React.FunctionComponent = ({ children }) => ( 95 | 96 | 97 | {children} 98 | 99 | 100 | ); 101 | 102 | const { result } = renderHook(() => useContainer(), { wrapper }); 103 | 104 | expect(result.current.get(TOKENS.some)).toBe(someValue); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /packages/brandi-react/spec/injection.spec.tsx: -------------------------------------------------------------------------------- 1 | import { createContainer, tag, token } from 'brandi'; 2 | import React from 'react'; 3 | import { renderHook } from '@testing-library/react-hooks'; 4 | 5 | import { 6 | ContainerProvider, 7 | createInjectionHooks, 8 | tagged, 9 | useInjection, 10 | } from '../src'; 11 | 12 | describe('injection', () => { 13 | describe('useInjection', () => { 14 | it('uses a dependency', () => { 15 | const TOKENS = { 16 | some: token('some'), 17 | }; 18 | 19 | const value = 1; 20 | 21 | const container = createContainer(); 22 | container.bind(TOKENS.some).toConstant(value); 23 | 24 | const wrapper: React.FunctionComponent = ({ children }) => ( 25 | {children} 26 | ); 27 | 28 | const { result } = renderHook(() => useInjection(TOKENS.some), { 29 | wrapper, 30 | }); 31 | 32 | expect(result.current).toBe(value); 33 | }); 34 | 35 | it('uses a tagged dependency', () => { 36 | const TOKENS = { 37 | some: token('some'), 38 | }; 39 | 40 | const tags = { 41 | some: tag('some'), 42 | }; 43 | 44 | const value = 1; 45 | const anotherValue = 2; 46 | 47 | const container = createContainer(); 48 | container.bind(TOKENS.some).toConstant(value); 49 | container.when(tags.some).bind(TOKENS.some).toConstant(anotherValue); 50 | 51 | const TaggedComponent = tagged(tags.some)(({ children }) => ( 52 |
{children}
53 | )); 54 | 55 | const wrapper: React.FunctionComponent = ({ children }) => ( 56 | 57 | {children} 58 | 59 | ); 60 | 61 | const { result } = renderHook(() => useInjection(TOKENS.some), { 62 | wrapper, 63 | }); 64 | 65 | expect(result.current).toBe(anotherValue); 66 | }); 67 | 68 | it('uses an optional dependency', () => { 69 | const TOKENS = { 70 | some: token('some'), 71 | }; 72 | 73 | const container = createContainer(); 74 | 75 | const wrapper: React.FunctionComponent = ({ children }) => ( 76 | {children} 77 | ); 78 | 79 | const { result } = renderHook(() => useInjection(TOKENS.some.optional), { 80 | wrapper, 81 | }); 82 | 83 | expect(result.current).toBeUndefined(); 84 | }); 85 | 86 | it('keeps the same transient scoped dependency between renderers', () => { 87 | class Some {} 88 | 89 | const TOKENS = { 90 | some: token('some'), 91 | }; 92 | 93 | const container = createContainer(); 94 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 95 | 96 | const wrapper: React.FunctionComponent = ({ children }) => ( 97 | {children} 98 | ); 99 | 100 | const { result, rerender } = renderHook( 101 | () => useInjection(TOKENS.some.optional), 102 | { 103 | wrapper, 104 | }, 105 | ); 106 | 107 | rerender(); 108 | 109 | expect(result.all[0]).toBeInstanceOf(Some); 110 | expect(result.all[1]).toBeInstanceOf(Some); 111 | expect(result.all[0]).toBe(result.all[1]); 112 | }); 113 | 114 | it('returns individual transient scoped dependencies for separate renders', () => { 115 | class Some {} 116 | 117 | const TOKENS = { 118 | some: token('some'), 119 | }; 120 | 121 | const container = createContainer(); 122 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 123 | 124 | const wrapper: React.FunctionComponent = ({ children }) => ( 125 | {children} 126 | ); 127 | 128 | const { result: firstResult } = renderHook( 129 | () => useInjection(TOKENS.some.optional), 130 | { 131 | wrapper, 132 | }, 133 | ); 134 | 135 | const { result: secondResult } = renderHook( 136 | () => useInjection(TOKENS.some.optional), 137 | { 138 | wrapper, 139 | }, 140 | ); 141 | 142 | expect(firstResult.current).toBeInstanceOf(Some); 143 | expect(secondResult.current).toBeInstanceOf(Some); 144 | expect(firstResult.current).not.toBe(secondResult.current); 145 | }); 146 | }); 147 | 148 | describe('createInjectionHooks', () => { 149 | it('creates injection hooks', () => { 150 | const TOKENS = { 151 | some: token('some'), 152 | another: token('another'), 153 | }; 154 | 155 | const hooks = createInjectionHooks(TOKENS.some, TOKENS.another); 156 | const [useSome, useAnother] = hooks; 157 | 158 | const someValue = 1; 159 | const anotherValue = '2'; 160 | 161 | const container = createContainer(); 162 | container.bind(TOKENS.some).toConstant(someValue); 163 | container.bind(TOKENS.another).toConstant(anotherValue); 164 | 165 | const wrapper: React.FunctionComponent = ({ children }) => ( 166 | {children} 167 | ); 168 | 169 | const { result: someResult } = renderHook(() => useSome(), { wrapper }); 170 | const { result: anotherResult } = renderHook(() => useAnother(), { 171 | wrapper, 172 | }); 173 | 174 | expect(someResult.current).toBe(someValue); 175 | expect(anotherResult.current).toBe(anotherValue); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /packages/brandi-react/src/conditions/ConditionsContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ResolutionCondition } from 'brandi'; 3 | 4 | export const ConditionsContext = React.createContext([]); 5 | -------------------------------------------------------------------------------- /packages/brandi-react/src/conditions/ConditionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ResolutionCondition } from 'brandi'; 3 | 4 | import { ConditionsContext } from './ConditionsContext'; 5 | import { useConditions } from './useConditions'; 6 | 7 | export const ConditionsProvider: React.FunctionComponent<{ 8 | children?: React.ReactNode; 9 | conditions: ResolutionCondition[]; 10 | isolated?: boolean; 11 | }> = ({ children, conditions, isolated = false }) => { 12 | const currentConditions = useConditions(); 13 | const resolvedConditions = React.useMemo( 14 | () => 15 | currentConditions.length > 0 && !isolated 16 | ? [...new Set([...currentConditions, ...conditions])] 17 | : conditions, 18 | [currentConditions, conditions, isolated], 19 | ); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/brandi-react/src/conditions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConditionsContext'; 2 | export * from './ConditionsProvider'; 3 | export * from './tagged'; 4 | export * from './useConditions'; 5 | -------------------------------------------------------------------------------- /packages/brandi-react/src/conditions/tagged.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tag } from 'brandi'; 3 | 4 | import { ConditionsProvider } from './ConditionsProvider'; 5 | 6 | export interface TaggedOptions { 7 | isolated?: boolean; 8 | } 9 | 10 | export const tagged = (...tags: Tag[]) =>

( 11 | Component: React.ComponentType

, 12 | options: TaggedOptions = {}, 13 | ): React.FunctionComponent

=> { 14 | const Wrapper: React.FunctionComponent

= (props) => ( 15 | 16 | 17 | 18 | ); 19 | 20 | Wrapper.displayName = Component.displayName 21 | ? `Tagged(${Component.displayName})` 22 | : 'Tagged'; 23 | 24 | return Wrapper; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/brandi-react/src/conditions/useConditions.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ConditionsContext } from './ConditionsContext'; 4 | 5 | export const useConditions = () => React.useContext(ConditionsContext); 6 | -------------------------------------------------------------------------------- /packages/brandi-react/src/container/ContainerContext.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'brandi'; 2 | import React from 'react'; 3 | 4 | export const ContainerContext = React.createContext(null); 5 | -------------------------------------------------------------------------------- /packages/brandi-react/src/container/ContainerProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from 'brandi'; 2 | import React from 'react'; 3 | 4 | import { ContainerContext } from './ContainerContext'; 5 | import { useContainer } from './useContainer'; 6 | 7 | export const ContainerProvider: React.FunctionComponent<{ 8 | children?: React.ReactNode; 9 | container: Container; 10 | isolated?: boolean; 11 | }> = ({ children, container, isolated = false }) => { 12 | const parentContainer = useContainer(false); 13 | 14 | const extend = !isolated ? parentContainer : null; 15 | 16 | const clonedContainer = React.useMemo(() => { 17 | const cloned = container.clone() 18 | if (extend) cloned.extend(extend) 19 | return cloned 20 | }, [container, extend]); 21 | 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/brandi-react/src/container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContainerContext'; 2 | export * from './ContainerProvider'; 3 | export * from './useContainer'; 4 | -------------------------------------------------------------------------------- /packages/brandi-react/src/container/useContainer.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'brandi'; 2 | import React from 'react'; 3 | 4 | import { ContainerContext } from './ContainerContext'; 5 | 6 | export function useContainer(strict?: true): Container; 7 | export function useContainer(strict?: false): Container | null; 8 | export function useContainer(strict: boolean = true): Container | null { 9 | const container = React.useContext(ContainerContext); 10 | 11 | if (strict && container === null) { 12 | throw new Error( 13 | "Could not get a container from a context. Did you forget to pass the container through 'ContainerProvider'?", 14 | ); 15 | } 16 | 17 | return container; 18 | } 19 | -------------------------------------------------------------------------------- /packages/brandi-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ContainerProvider } from './container'; 2 | export { createInjectionHooks, useInjection } from './injection'; 3 | export { tagged } from './conditions'; 4 | -------------------------------------------------------------------------------- /packages/brandi-react/src/injection/createInjectionHooks.ts: -------------------------------------------------------------------------------- 1 | import { TokenType, TokenValue } from 'brandi'; 2 | 3 | import { useInjection } from './useInjection'; 4 | 5 | type InjectionHooks = { 6 | [K in keyof T]: T[K] extends TokenValue ? () => TokenType : never; 7 | }; 8 | 9 | export const createInjectionHooks = ( 10 | ...tokens: T 11 | ): InjectionHooks => 12 | tokens.map((token) => () => useInjection(token)) as InjectionHooks; 13 | -------------------------------------------------------------------------------- /packages/brandi-react/src/injection/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createInjectionHooks'; 2 | export * from './useInjection'; 3 | -------------------------------------------------------------------------------- /packages/brandi-react/src/injection/useInjection.ts: -------------------------------------------------------------------------------- 1 | import { TokenType, TokenValue } from 'brandi'; 2 | import React from 'react'; 3 | 4 | import { useConditions } from '../conditions'; 5 | import { useContainer } from '../container'; 6 | 7 | export const useInjection = (token: T): TokenType => { 8 | const container = useContainer(); 9 | const conditions = useConditions(); 10 | 11 | return React.useMemo(() => container.get(token, conditions), [ 12 | token, 13 | conditions, 14 | container, 15 | ]); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/brandi-react/tsconfig.typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "removeComments": false, 5 | "declaration": true, 6 | "declarationDir": "./lib/typings", 7 | "emitDeclarationOnly": true 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/brandi/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /packages/brandi/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /packages/brandi/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright Vladimir Lewandowski (https://vovaspace.com/) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING 13 | FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE 15 | OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /packages/brandi/README.md: -------------------------------------------------------------------------------- 1 | # 🥃 Brandi 2 | 3 | **Brandi** is a dependency injection container powered by TypeScript. 4 | 5 | [![License](https://img.shields.io/npm/l/brandi.svg)](https://github.com/vovaspace/brandi/blob/main/packages/brandi/LICENSE) 6 | [![NPM Version](https://img.shields.io/npm/v/brandi.svg)](https://www.npmjs.com/package/brandi) 7 | [![Minzipped Size](https://img.shields.io/bundlephobia/minzip/brandi.svg)](https://bundlephobia.com/result?p=brandi) 8 | 9 | - **Framework agnostic.** Can work with any UI or server framework. 10 | - **Lightweight and Effective.** It is tiny and designed for maximum performance. 11 | - **Strongly typed.** TypeScript support out of box. 12 | - **Decorators free.** Does not require additional parameters in `tsconfig.json` and `Reflect` polyfill. 13 | 14 | ## Installation 15 | 16 | Brandi is available as a package for use with a module bundler or in a Node application. 17 | 18 | ```bash 19 | # NPM 20 | npm install brandi 21 | ``` 22 | 23 | ```bash 24 | # Yarn 25 | yarn add brandi 26 | ``` 27 | 28 | The Brandi source code is written in TypeScript but we precompile both CommonJS and ESModule builds to **ES2018**. 29 | 30 | Additionally, we provide builds precompiled to **ESNext** by `esnext`, `esnext:main` and `esnext:module` fields. 31 | 32 | ### TypeScript 33 | 34 | TypeScript type definitions are **included** in the library and do not need to be installed additionally. 35 | 36 | ### No Dependencies 37 | 38 | Brandi has no dependencies, but requires the following globals in order to work: 39 | 40 | - `Symbol` 41 | - `WeakMap` 42 | 43 | ### Production 44 | 45 | By default, Brandi will be in development mode. The development mode includes warnings about common mistakes 46 | and `capture()/restore()` `Container` methods. 47 | 48 | Don't forget to set `process.env.NODE_ENV` to `production` when deploying your application. 49 | 50 | ## Documentation 51 | 52 | You can find the Brandi documentation on the [website](https://brandi.js.org). 53 | 54 | The documentation is divided into several sections: 55 | 56 | - Getting Started 57 | - [Overview](https://brandi.js.org/getting-started) 58 | - [Installation](https://brandi.js.org/getting-started/installation) 59 | - Reference 60 | - [API Reference](https://brandi.js.org/reference) 61 | - [Pointers and Registrators](https://brandi.js.org/reference/pointers-and-registrators) 62 | - [Container](https://brandi.js.org/reference/container) 63 | - [Binding Types](https://brandi.js.org/reference/binding-types) 64 | - [Binding Scopes](https://brandi.js.org/reference/binding-scopes) 65 | - [Optional Dependencies](https://brandi.js.org/reference/optional-dependencies) 66 | - [Dependency Modules](https://brandi.js.org/reference/dependency-modules) 67 | - [Hierarchical Containers](https://brandi.js.org/reference/hierarchical-containers) 68 | - [Conditional Bindings](https://brandi.js.org/reference/conditional-bindings) 69 | - Brandi-React 70 | - [Overview](https://brandi.js.org/brandi-react) 71 | - [`ContainerProvider`](https://brandi.js.org/brandi-react/container-provider) 72 | - [`useInjection`](https://brandi.js.org/brandi-react/use-injection) 73 | - [`createInjectionHooks`](https://brandi.js.org/brandi-react/create-injection-hooks) 74 | - [`tagged`](https://brandi.js.org/brandi-react/tagged) 75 | - Examples 76 | - [Basic Examples](https://brandi.js.org/examples) 77 | 78 | ## Examples 79 | 80 | Here are just basic examples. 81 | 82 | 83 | 84 | ### Getting Instance 85 | 86 | **Binding types** and **scopes** are detailed in [Binding Types](https://brandi.js.org/reference/binding-types) 87 | and [Binding Scopes](https://brandi.js.org/reference/binding-scopes) sections of the documentation. 88 | 89 | 90 | ```typescript 91 | import { Container, token } from 'brandi'; 92 | 93 | class ApiService {} 94 | 95 | const TOKENS = { 96 | /* ↓ Creates a typed token. */ 97 | apiService: token('apiService'), 98 | }; 99 | 100 | const container = new Container(); 101 | 102 | container 103 | .bind(TOKENS.apiService) 104 | .toInstance(ApiService) /* ← Binds the token to an instance */ 105 | .inTransientScope(); /* ← in transient scope. */ 106 | 107 | /* ↓ Gets the instance from the container. */ 108 | const apiService = container.get(TOKENS.apiService); 109 | 110 | expect(apiService).toBeInstanceOf(ApiService); 111 | ``` 112 | 113 | 114 | ### Snapshoting 115 | 116 | 117 | ```typescript 118 | import { Container, token } from 'brandi'; 119 | 120 | const TOKENS = { 121 | apiKey: token('API Key'), 122 | }; 123 | 124 | const container = new Container(); 125 | 126 | container 127 | .bind(TOKENS.apiKey) 128 | .toConstant('#key9428'); /* ← Binds the token to some string. */ 129 | 130 | /* ↓ Captures (snapshots) the current container state. */ 131 | container.capture(); 132 | 133 | container 134 | .bind(TOKENS.apiKey) 135 | .toConstant('#testKey'); /* ← Binds the same token to another value. */ 136 | /* For example, this can be used in testing. */ 137 | 138 | const testKey = container.get(TOKENS.apiKey); 139 | 140 | /* ↓ Restores the captured container state. */ 141 | container.restore(); 142 | 143 | const originalKey = container.get(TOKENS.apiKey); 144 | 145 | expect(testKey).toBe('#testKey'); 146 | expect(originalKey).toBe('#key9428'); 147 | ``` 148 | 149 | 150 | Other `Container` methods are detailed 151 | in [Container](https://brandi.js.org/reference/container) section of the documentation. 152 | 153 | ### Hierarchical Containers 154 | 155 | Hierarchical containers are detailed 156 | in [Hierarchical Containers](https://brandi.js.org/reference/hierarchical-containers) section of the documentation. 157 | 158 | ```typescript 159 | import { Container, token } from 'brandi'; 160 | 161 | class ApiService {} 162 | 163 | const TOKENS = { 164 | apiService: token('apiService'), 165 | }; 166 | 167 | const parentContainer = new Container(); 168 | 169 | parentContainer 170 | .bind(TOKENS.apiService) 171 | .toInstance(ApiService) 172 | .inTransientScope(); 173 | 174 | /* ↓ Creates a container with the parent. */ 175 | const childContainer = new Container().extend(parentContainer); 176 | 177 | /** ↓ That container can't satisfy the getting request, 178 | * it passes it along to its parent container. 179 | * The intsance will be gotten from the parent container. 180 | */ 181 | const apiService = childContainer.get(TOKENS.apiService); 182 | 183 | expect(apiService).toBeInstanceOf(ApiService); 184 | ``` 185 | -------------------------------------------------------------------------------- /packages/brandi/build.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | const esbuild = require('esbuild'); 4 | 5 | const pkg = require('./package.json'); 6 | 7 | const commons = { 8 | entryPoints: ['./src/index.ts'], 9 | bundle: true, 10 | platform: 'node', 11 | sourcemap: true, 12 | }; 13 | 14 | esbuild.build({ 15 | ...commons, 16 | outfile: pkg.main, 17 | format: 'cjs', 18 | target: 'es2018', 19 | }); 20 | 21 | esbuild.build({ 22 | ...commons, 23 | outfile: pkg.module, 24 | format: 'esm', 25 | target: 'es2018', 26 | }); 27 | 28 | esbuild.build({ 29 | ...commons, 30 | outfile: pkg.esnext, 31 | format: 'esm', 32 | target: 'esnext', 33 | }); 34 | -------------------------------------------------------------------------------- /packages/brandi/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base, 5 | displayName: 'brandi', 6 | }; 7 | -------------------------------------------------------------------------------- /packages/brandi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brandi", 3 | "version": "5.0.0", 4 | "description": "The dependency injection container.", 5 | "main": "./lib/brandi.js", 6 | "module": "./lib/brandi.mjs", 7 | "esnext": "./lib/brandi.next.js", 8 | "esnext:main": "./lib/brandi.next.js", 9 | "esnext:module": "./lib/brandi.next.js", 10 | "exports": { 11 | "import": "./lib/brandi.mjs", 12 | "require": "./lib/brandi.js", 13 | "default": "./lib/brandi.js", 14 | "types": "./lib/typings/index.d.ts" 15 | }, 16 | "typings": "./lib/typings/index.d.ts", 17 | "files": [ 18 | "lib" 19 | ], 20 | "scripts": { 21 | "test": "jest --colors", 22 | "build": "npm run clean && npm run build:lib && npm run build:typings", 23 | "build:lib": "node build.config.js", 24 | "build:typings": "tsc -p ./tsconfig.typings.json", 25 | "clean": "rimraf ./lib", 26 | "code:lint": "prettier --check . && eslint .", 27 | "code:fix": "prettier --write . && eslint --fix ." 28 | }, 29 | "license": "ISC", 30 | "author": "Vladimir Lewandowski (https://vovaspace.com/)", 31 | "homepage": "https://brandi.js.org", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/vovaspace/brandi.git", 35 | "directory": "packages/brandi" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/vovaspace/brandi/issues" 39 | }, 40 | "keywords": [ 41 | "di", 42 | "ioc", 43 | "dependency injection", 44 | "dependency inversion", 45 | "inversion of control", 46 | "container" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /packages/brandi/spec/TokenTypeMap.spec.ts: -------------------------------------------------------------------------------- 1 | import { TokenTypeMap, token } from '../src'; 2 | 3 | import { Equal, Expect } from './utils'; 4 | 5 | describe('TokenTypeMap', () => { 6 | it('returns token type map', () => { 7 | const TOKENS = { 8 | num: token('num'), 9 | str: token('str'), 10 | nested: { 11 | num: token('num'), 12 | str: token('str'), 13 | }, 14 | }; 15 | 16 | type Result = Expect< 17 | Equal< 18 | TokenTypeMap, 19 | { 20 | num: number; 21 | str: string; 22 | nested: { 23 | num: number; 24 | str: string; 25 | }; 26 | } 27 | > 28 | >; 29 | 30 | expect(true).toBe(true); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/brandi/spec/__snapshots__/conditional-bindings.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`conditional bindings Container.get logs a warning when multiple related conditions were passed 1`] = `"Warning: When resolving a binding by 'someValue' token with [Some, tag(some)] conditions, more than one binding was found. In this case, Brandi resolves the binding by the first tag assigned by 'tagged(target, ...tags)' function or, if you explicitly passed conditions through 'Container.get(token, conditions)' method, by the first resolved condition. Try to avoid such implicit logic."`; 4 | 5 | exports[`conditional bindings logs a warning when multiple related tags are on the target 1`] = `"Warning: When resolving a binding by 'someValue' token with [tag(some), tag(another)] conditions, more than one binding was found. In this case, Brandi resolves the binding by the first tag assigned by 'tagged(target, ...tags)' function or, if you explicitly passed conditions through 'Container.get(token, conditions)' method, by the first resolved condition. Try to avoid such implicit logic."`; 6 | -------------------------------------------------------------------------------- /packages/brandi/spec/__snapshots__/container.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`container captures the container bindings to a snapshot 1`] = `"No matching bindings found for 'additional' token."`; 4 | 5 | exports[`container captures the container bindings to a snapshot 2`] = `"No matching bindings found for 'additional' token."`; 6 | 7 | exports[`container clears parent container 1`] = `"No matching bindings found for 'value' token."`; 8 | 9 | exports[`container logs an error when trying to restore a non-captured container state 1`] = `"Error: It looks like a trying to restore a non-captured container state. Did you forget to call 'capture()' method?"`; 10 | 11 | exports[`container returns an unlinked container from 'clone' method 1`] = `"No matching bindings found for 'copied' token."`; 12 | 13 | exports[`container throws error when the token was not bound 1`] = `"No matching bindings found for 'some' token."`; 14 | -------------------------------------------------------------------------------- /packages/brandi/spec/__snapshots__/toInstance.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`toInstance logs a warning when a binding scope setting method was not called 1`] = `"Warning: did you forget to set a scope for 'unknown' token binding? Call 'inTransientScope()', 'inSingletonScope()', 'inContainerScope()' or 'inResolutionScope()'."`; 4 | 5 | exports[`toInstance throws error when trying to construct an instance with required constructor arguments when the creator was not injected 1`] = `"Missing required 'injected' registration of 'Some'"`; 6 | 7 | exports[`toInstance throws error when trying to construct an instance with required constructor arguments when the creator was not injected 2`] = `"Missing required 'injected' registration of 'createSome'"`; 8 | -------------------------------------------------------------------------------- /packages/brandi/spec/conditional-bindings.spec.ts: -------------------------------------------------------------------------------- 1 | import { Container, injected, tag, tagged, token } from '../src'; 2 | 3 | import { setEnv } from './utils'; 4 | 5 | describe('conditional bindings', () => { 6 | it('creates an instance with an injection that depends on the target', () => { 7 | const someValue = 1; 8 | const anotherValue = 2; 9 | 10 | class Some { 11 | constructor(public num: number) {} 12 | } 13 | 14 | class Another { 15 | constructor(public num: number) {} 16 | } 17 | 18 | const TOKENS = { 19 | num: token('num'), 20 | some: token('some'), 21 | another: token('another'), 22 | }; 23 | 24 | injected(Some, TOKENS.num); 25 | injected(Another, TOKENS.num); 26 | 27 | const container = new Container(); 28 | 29 | container.bind(TOKENS.num).toConstant(someValue); 30 | container.when(Another).bind(TOKENS.num).toConstant(anotherValue); 31 | 32 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 33 | container.bind(TOKENS.another).toInstance(Another).inTransientScope(); 34 | 35 | const someInstance = container.get(TOKENS.some); 36 | const anotherInstance = container.get(TOKENS.another); 37 | 38 | expect(someInstance.num).toBe(someValue); 39 | expect(anotherInstance.num).toBe(anotherValue); 40 | }); 41 | 42 | it('creates an instance with an injection that depends on the tag', () => { 43 | const someValue = 1; 44 | const anotherValue = 2; 45 | 46 | class Some { 47 | constructor(public num: number) {} 48 | } 49 | 50 | class Another { 51 | constructor(public num: number) {} 52 | } 53 | 54 | const TOKENS = { 55 | num: token('num'), 56 | some: token('some'), 57 | another: token('another'), 58 | }; 59 | 60 | const TAGS = { 61 | some: tag('some'), 62 | }; 63 | 64 | injected(Some, TOKENS.num); 65 | 66 | injected(Another, TOKENS.num); 67 | tagged(Another, TAGS.some); 68 | 69 | const container = new Container(); 70 | 71 | container.bind(TOKENS.num).toConstant(someValue); 72 | container.when(TAGS.some).bind(TOKENS.num).toConstant(anotherValue); 73 | 74 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 75 | container.bind(TOKENS.another).toInstance(Another).inTransientScope(); 76 | 77 | const someInstance = container.get(TOKENS.some); 78 | const anotherInstance = container.get(TOKENS.another); 79 | 80 | expect(someInstance.num).toBe(someValue); 81 | expect(anotherInstance.num).toBe(anotherValue); 82 | }); 83 | 84 | it('injects a dependency by the target condition when there are conditions for both the target and the tag', () => { 85 | const someValue = 1; 86 | const anotherValue = 2; 87 | 88 | class Some { 89 | constructor(public num: number) {} 90 | } 91 | 92 | class Another { 93 | constructor(public num: number) {} 94 | } 95 | 96 | const TOKENS = { 97 | num: token('num'), 98 | some: token('some'), 99 | another: token('another'), 100 | }; 101 | 102 | const TAGS = { 103 | some: tag('some'), 104 | }; 105 | 106 | injected(Some, TOKENS.num); 107 | tagged(Some, TAGS.some); 108 | 109 | injected(Another, TOKENS.num); 110 | tagged(Another, TAGS.some); 111 | 112 | const container = new Container(); 113 | 114 | container.when(TAGS.some).bind(TOKENS.num).toConstant(someValue); 115 | container.when(Another).bind(TOKENS.num).toConstant(anotherValue); 116 | 117 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 118 | container.bind(TOKENS.another).toInstance(Another).inTransientScope(); 119 | 120 | const someInstance = container.get(TOKENS.some); 121 | const anotherInstance = container.get(TOKENS.another); 122 | 123 | expect(someInstance.num).toBe(someValue); 124 | expect(anotherInstance.num).toBe(anotherValue); 125 | }); 126 | 127 | it('ignores an unused tag on the target', () => { 128 | const someValue = 1; 129 | 130 | class Some { 131 | constructor(public num: number) {} 132 | } 133 | 134 | const TOKENS = { 135 | num: token('num'), 136 | some: token('some'), 137 | }; 138 | 139 | const TAGS = { 140 | unused: tag('unused'), 141 | }; 142 | 143 | injected(Some, TOKENS.num); 144 | 145 | tagged(Some, TAGS.unused); 146 | 147 | const container = new Container(); 148 | container.bind(TOKENS.num).toConstant(someValue); 149 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 150 | 151 | const instance = container.get(TOKENS.some); 152 | 153 | expect(instance.num).toBe(someValue); 154 | }); 155 | 156 | it('injects a dependency by conditions from the parent container', () => { 157 | const defaultValue = 1; 158 | const someValue = 2; 159 | const anotherValue = 3; 160 | 161 | class Some { 162 | constructor(public value: number) {} 163 | } 164 | class Another { 165 | constructor(public value: number) {} 166 | } 167 | 168 | const TOKENS = { 169 | value: token('value'), 170 | someClass: token('someClass'), 171 | anotherClass: token('anotherClass'), 172 | }; 173 | 174 | const TAGS = { 175 | some: tag('some'), 176 | }; 177 | 178 | injected(Some, TOKENS.value); 179 | tagged(Some, TAGS.some); 180 | 181 | injected(Another, TOKENS.value); 182 | 183 | const parentContainer = new Container(); 184 | 185 | parentContainer.bind(TOKENS.value).toConstant(defaultValue); 186 | parentContainer.when(TAGS.some).bind(TOKENS.value).toConstant(someValue); 187 | parentContainer.when(Another).bind(TOKENS.value).toConstant(anotherValue); 188 | 189 | const childContainer = new Container().extend(parentContainer); 190 | childContainer.bind(TOKENS.someClass).toInstance(Some).inTransientScope(); 191 | childContainer 192 | .bind(TOKENS.anotherClass) 193 | .toInstance(Another) 194 | .inTransientScope(); 195 | 196 | const someInstance = childContainer.get(TOKENS.someClass); 197 | const anotherInstance = childContainer.get(TOKENS.anotherClass); 198 | 199 | expect(someInstance.value).toBe(someValue); 200 | expect(anotherInstance.value).toBe(anotherValue); 201 | }); 202 | 203 | it('takes a binding by the tag assigned first with multiple related tags on the target', () => { 204 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 205 | 206 | const someValue = 1; 207 | const anotherValue = 2; 208 | const otherValue = 3; 209 | 210 | class Some { 211 | constructor(public value: number) {} 212 | } 213 | 214 | const TOKENS = { 215 | someValue: token('someValue'), 216 | someClass: token('someClass'), 217 | }; 218 | 219 | const TAGS = { 220 | some: tag('some'), 221 | another: tag('another'), 222 | other: tag('other'), 223 | }; 224 | 225 | injected(Some, TOKENS.someValue); 226 | tagged(Some, TAGS.some, TAGS.another, TAGS.other); 227 | 228 | const container = new Container(); 229 | 230 | container.when(TAGS.other).bind(TOKENS.someValue).toConstant(otherValue); 231 | container.when(TAGS.some).bind(TOKENS.someValue).toConstant(someValue); 232 | container 233 | .when(TAGS.another) 234 | .bind(TOKENS.someValue) 235 | .toConstant(anotherValue); 236 | 237 | container.bind(TOKENS.someClass).toInstance(Some).inTransientScope(); 238 | 239 | const instance = container.get(TOKENS.someClass); 240 | 241 | expect(instance.value).toBe(someValue); 242 | 243 | spy.mockRestore(); 244 | }); 245 | 246 | it('logs a warning when multiple related tags are on the target', () => { 247 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 248 | 249 | class Some { 250 | constructor(public value: number) {} 251 | } 252 | 253 | const TOKENS = { 254 | someValue: token('someValue'), 255 | someClass: token('someClass'), 256 | }; 257 | 258 | const TAGS = { 259 | some: tag('some'), 260 | another: tag('another'), 261 | }; 262 | 263 | injected(Some, TOKENS.someValue); 264 | tagged(Some, TAGS.some, TAGS.another); 265 | 266 | const container = new Container(); 267 | 268 | container.when(TAGS.another).bind(TOKENS.someValue).toConstant(0); 269 | container.when(TAGS.some).bind(TOKENS.someValue).toConstant(0); 270 | 271 | container.bind(TOKENS.someClass).toInstance(Some).inTransientScope(); 272 | 273 | container.get(TOKENS.someClass); 274 | 275 | expect(spy).toHaveBeenCalledTimes(1); 276 | expect(spy.mock.calls[0]?.[0]).toMatchSnapshot(); 277 | 278 | spy.mockRestore(); 279 | }); 280 | 281 | it('does not log a warning when a single related tag are on the target', () => { 282 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 283 | 284 | class Some { 285 | constructor(public value: number) {} 286 | } 287 | 288 | class Another { 289 | constructor(public value: number) {} 290 | } 291 | 292 | const TOKENS = { 293 | someValue: token('someValue'), 294 | someClass: token('someClass'), 295 | anotherClass: token('anotherClass'), 296 | }; 297 | 298 | const TAGS = { 299 | some: tag('some'), 300 | }; 301 | 302 | injected(Some, TOKENS.someValue); 303 | injected(Another, TOKENS.someValue); 304 | 305 | tagged(Another, TAGS.some); 306 | 307 | const container = new Container(); 308 | 309 | container.bind(TOKENS.someValue).toConstant(0); 310 | container.when(TAGS.some).bind(TOKENS.someValue).toConstant(0); 311 | 312 | container.bind(TOKENS.someClass).toInstance(Some).inTransientScope(); 313 | container.bind(TOKENS.anotherClass).toInstance(Another).inTransientScope(); 314 | 315 | container.get(TOKENS.someClass); 316 | container.get(TOKENS.anotherClass); 317 | 318 | expect(spy).toHaveBeenCalledTimes(0); 319 | 320 | spy.mockRestore(); 321 | }); 322 | 323 | it("skips the logging in 'production' env", () => { 324 | const restoreEnv = setEnv('production'); 325 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 326 | 327 | class Some { 328 | constructor(public value: number) {} 329 | } 330 | 331 | const TOKENS = { 332 | someValue: token('someValue'), 333 | someClass: token('someClass'), 334 | }; 335 | 336 | const TAGS = { 337 | some: tag('some'), 338 | another: tag('another'), 339 | }; 340 | 341 | injected(Some, TOKENS.someValue); 342 | tagged(Some, TAGS.some, TAGS.another); 343 | 344 | const container = new Container(); 345 | 346 | container.when(TAGS.another).bind(TOKENS.someValue).toConstant(0); 347 | container.when(TAGS.some).bind(TOKENS.someValue).toConstant(0); 348 | 349 | container.bind(TOKENS.someClass).toInstance(Some).inTransientScope(); 350 | 351 | container.get(TOKENS.someClass); 352 | 353 | expect(spy).toHaveBeenCalledTimes(0); 354 | 355 | restoreEnv(); 356 | spy.mockRestore(); 357 | }); 358 | 359 | describe('Container.get', () => { 360 | it('returns a dependency with a condition', () => { 361 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 362 | 363 | const someValue = 1; 364 | const anotherValue = 2; 365 | const otherValue = 3; 366 | 367 | class Some { 368 | constructor(public value: number) {} 369 | } 370 | 371 | const TOKENS = { 372 | someValue: token('someValue'), 373 | }; 374 | 375 | const TAGS = { 376 | some: tag('some'), 377 | }; 378 | 379 | injected(Some, TOKENS.someValue); 380 | 381 | const container = new Container(); 382 | 383 | container.bind(TOKENS.someValue).toConstant(someValue); 384 | container.when(TAGS.some).bind(TOKENS.someValue).toConstant(anotherValue); 385 | container.when(Some).bind(TOKENS.someValue).toConstant(otherValue); 386 | 387 | expect(container.get(TOKENS.someValue)).toBe(someValue); 388 | expect(container.get(TOKENS.someValue, [])).toBe(someValue); 389 | expect(container.get(TOKENS.someValue, [TAGS.some])).toBe(anotherValue); 390 | expect(container.get(TOKENS.someValue, [Some])).toBe(otherValue); 391 | expect(container.get(TOKENS.someValue, [TAGS.some, Some])).toBe( 392 | anotherValue, 393 | ); 394 | expect(container.get(TOKENS.someValue, [Some, TAGS.some])).toBe( 395 | otherValue, 396 | ); 397 | 398 | spy.mockRestore(); 399 | }); 400 | 401 | it('logs a warning when multiple related conditions were passed', () => { 402 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 403 | 404 | class Some { 405 | constructor(public value: number) {} 406 | } 407 | 408 | const TOKENS = { 409 | someValue: token('someValue'), 410 | }; 411 | 412 | const TAGS = { 413 | some: tag('some'), 414 | }; 415 | 416 | injected(Some, TOKENS.someValue); 417 | 418 | const container = new Container(); 419 | 420 | container.bind(TOKENS.someValue).toConstant(0); 421 | container.when(TAGS.some).bind(TOKENS.someValue).toConstant(0); 422 | container.when(Some).bind(TOKENS.someValue).toConstant(0); 423 | 424 | container.get(TOKENS.someValue, [Some, TAGS.some]); 425 | 426 | expect(spy).toHaveBeenCalledTimes(1); 427 | expect(spy.mock.calls[0]?.[0]).toMatchSnapshot(); 428 | 429 | spy.mockRestore(); 430 | }); 431 | 432 | it('does not log a warning when a single related condition were passed', () => { 433 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 434 | 435 | class Some { 436 | constructor(public value: number) {} 437 | } 438 | 439 | const TOKENS = { 440 | value: token('value'), 441 | }; 442 | 443 | const TAGS = { 444 | some: tag('some'), 445 | }; 446 | 447 | injected(Some, TOKENS.value); 448 | 449 | const container = new Container(); 450 | 451 | container.bind(TOKENS.value).toConstant(0); 452 | container.when(TAGS.some).bind(TOKENS.value).toConstant(0); 453 | container.when(Some).bind(TOKENS.value).toConstant(0); 454 | 455 | container.get(TOKENS.value, [Some]); 456 | container.get(TOKENS.value, [TAGS.some]); 457 | 458 | expect(spy).toHaveBeenCalledTimes(0); 459 | 460 | spy.mockRestore(); 461 | }); 462 | 463 | it("skips the logging in 'production' env", () => { 464 | const restoreEnv = setEnv('production'); 465 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => null); 466 | 467 | class Some { 468 | constructor(public value: number) {} 469 | } 470 | 471 | const TOKENS = { 472 | value: token('value'), 473 | }; 474 | 475 | const TAGS = { 476 | some: tag('some'), 477 | }; 478 | 479 | injected(Some, TOKENS.value); 480 | 481 | const container = new Container(); 482 | 483 | container.bind(TOKENS.value).toConstant(0); 484 | container.when(TAGS.some).bind(TOKENS.value).toConstant(0); 485 | container.when(Some).bind(TOKENS.value).toConstant(0); 486 | 487 | container.get(TOKENS.value, [Some, TAGS.some]); 488 | 489 | expect(spy).toHaveBeenCalledTimes(0); 490 | 491 | restoreEnv(); 492 | spy.mockRestore(); 493 | }); 494 | }); 495 | }); 496 | -------------------------------------------------------------------------------- /packages/brandi/spec/container.spec.ts: -------------------------------------------------------------------------------- 1 | import { Container, DependencyModule, createContainer, token } from '../src'; 2 | 3 | import { setEnv } from './utils'; 4 | 5 | describe('container', () => { 6 | it('throws error when the token was not bound', () => { 7 | const TOKENS = { 8 | some: token('some'), 9 | }; 10 | 11 | const container = new Container(); 12 | 13 | expect(() => container.get(TOKENS.some)).toThrowErrorMatchingSnapshot(); 14 | }); 15 | 16 | it("returns an unlinked container from 'clone' method", () => { 17 | const parentValue = 1; 18 | const someValue = 2; 19 | const anotherValue = 3; 20 | const copiedValue = 4; 21 | 22 | const TOKENS = { 23 | parent: token('parent'), 24 | original: token('original'), 25 | copied: token('copied'), 26 | }; 27 | 28 | const parentContainer = new Container(); 29 | parentContainer.bind(TOKENS.parent).toConstant(parentValue); 30 | 31 | const originalContainer = new Container().extend(parentContainer); 32 | originalContainer.bind(TOKENS.original).toConstant(someValue); 33 | 34 | const copiedContainer = originalContainer.clone(); 35 | copiedContainer.bind(TOKENS.original).toConstant(anotherValue); 36 | copiedContainer.bind(TOKENS.copied).toConstant(copiedValue); 37 | 38 | expect(originalContainer.get(TOKENS.original)).toBe(someValue); 39 | expect(() => 40 | originalContainer.get(TOKENS.copied), 41 | ).toThrowErrorMatchingSnapshot(); 42 | 43 | expect(copiedContainer.get(TOKENS.parent)).toBe(parentValue); 44 | expect(copiedContainer.get(TOKENS.original)).toBe(anotherValue); 45 | expect(copiedContainer.get(TOKENS.copied)).toBe(copiedValue); 46 | }); 47 | 48 | it('returns a dependency from the parent container', () => { 49 | const value = 1; 50 | 51 | const TOKENS = { 52 | value: token('value'), 53 | }; 54 | 55 | const parentContainer = new Container(); 56 | parentContainer.bind(TOKENS.value).toConstant(value); 57 | 58 | const childContainer = new Container().extend(parentContainer); 59 | 60 | expect(childContainer.get(TOKENS.value)).toBe(value); 61 | }); 62 | 63 | it('changes parent container', () => { 64 | const firstValue = 1; 65 | const secondValue = 2; 66 | 67 | const TOKENS = { 68 | value: token('value'), 69 | }; 70 | 71 | const firstContainer = new Container(); 72 | firstContainer.bind(TOKENS.value).toConstant(firstValue); 73 | 74 | const secondContainer = new Container(); 75 | secondContainer.bind(TOKENS.value).toConstant(secondValue); 76 | 77 | const container = new Container().extend(firstContainer); 78 | 79 | expect(container.get(TOKENS.value)).toBe(firstValue); 80 | 81 | container.extend(secondContainer); 82 | 83 | expect(container.get(TOKENS.value)).toBe(secondValue); 84 | }); 85 | 86 | it('clears parent container', () => { 87 | const value = 1; 88 | 89 | const TOKENS = { 90 | value: token('value'), 91 | }; 92 | 93 | const parentContainer = new Container(); 94 | parentContainer.bind(TOKENS.value).toConstant(value); 95 | 96 | const childContainer = new Container().extend(parentContainer); 97 | 98 | expect(childContainer.get(TOKENS.value)).toBe(value); 99 | 100 | childContainer.extend(null); 101 | 102 | expect(() => 103 | childContainer.get(TOKENS.value), 104 | ).toThrowErrorMatchingSnapshot(); 105 | }); 106 | 107 | it("rebinds a parent container's binding in the child container", () => { 108 | const someValue = 1; 109 | const anotherValue = 2; 110 | 111 | const TOKENS = { 112 | value: token('value'), 113 | }; 114 | 115 | const parentContainer = new Container(); 116 | parentContainer.bind(TOKENS.value).toConstant(someValue); 117 | 118 | const childContainer = new Container().extend(parentContainer); 119 | childContainer.bind(TOKENS.value).toConstant(anotherValue); 120 | 121 | expect(parentContainer.get(TOKENS.value)).toBe(someValue); 122 | expect(childContainer.get(TOKENS.value)).toBe(anotherValue); 123 | }); 124 | 125 | it('captures the container bindings to a snapshot', () => { 126 | const someValue = 1; 127 | const anotherValue = 2; 128 | const additionalValue = 3; 129 | 130 | const TOKENS = { 131 | value: token('value'), 132 | additional: token('additional'), 133 | }; 134 | 135 | const container = new Container(); 136 | container.bind(TOKENS.value).toConstant(someValue); 137 | 138 | container.capture!(); 139 | 140 | container.bind(TOKENS.value).toConstant(anotherValue); 141 | container.bind(TOKENS.additional).toConstant(additionalValue); 142 | 143 | expect(container.get(TOKENS.value)).toBe(anotherValue); 144 | expect(container.get(TOKENS.additional)).toBe(additionalValue); 145 | 146 | container.restore!(); 147 | 148 | expect(container.get(TOKENS.value)).toBe(someValue); 149 | expect(() => 150 | container.get(TOKENS.additional), 151 | ).toThrowErrorMatchingSnapshot(); 152 | 153 | container.bind(TOKENS.value).toConstant(anotherValue); 154 | container.bind(TOKENS.additional).toConstant(additionalValue); 155 | 156 | expect(container.get(TOKENS.value)).toBe(anotherValue); 157 | expect(container.get(TOKENS.additional)).toBe(additionalValue); 158 | 159 | container.restore!(); 160 | 161 | expect(container.get(TOKENS.value)).toBe(someValue); 162 | expect(() => 163 | container.get(TOKENS.additional), 164 | ).toThrowErrorMatchingSnapshot(); 165 | }); 166 | 167 | it('captures the container parent to a snapshot', () => { 168 | const firstValue = 1; 169 | const secondValue = 2; 170 | 171 | const TOKENS = { 172 | value: token('value'), 173 | }; 174 | 175 | const firstContainer = new Container(); 176 | firstContainer.bind(TOKENS.value).toConstant(firstValue); 177 | 178 | const secondContainer = new Container(); 179 | secondContainer.bind(TOKENS.value).toConstant(secondValue); 180 | 181 | const container = new Container().extend(firstContainer); 182 | 183 | container.capture!(); 184 | 185 | container.extend(secondContainer); 186 | 187 | expect(container.get(TOKENS.value)).toBe(secondValue); 188 | 189 | container.restore!(); 190 | 191 | expect(container.get(TOKENS.value)).toBe(firstValue); 192 | 193 | container.extend(secondContainer); 194 | 195 | expect(container.get(TOKENS.value)).toBe(secondValue); 196 | 197 | container.restore!(); 198 | 199 | expect(container.get(TOKENS.value)).toBe(firstValue); 200 | }); 201 | 202 | it('captures a singleton scoped binding state to a snapshot', () => { 203 | class Singleton {} 204 | 205 | const TOKENS = { 206 | some: token('some'), 207 | another: token('another'), 208 | }; 209 | 210 | const container = new Container(); 211 | container.bind(TOKENS.some).toInstance(Singleton).inSingletonScope(); 212 | container.bind(TOKENS.another).toInstance(Singleton).inSingletonScope(); 213 | 214 | const firstSomeInstance = container.get(TOKENS.some); 215 | 216 | container.capture!(); 217 | 218 | const secondSomeInstance = container.get(TOKENS.some); 219 | const firstAnotherInstance = container.get(TOKENS.another); 220 | 221 | container.restore!(); 222 | container.capture!(); 223 | 224 | const thirdSomeInstance = container.get(TOKENS.some); 225 | const secondAnotherInstance = container.get(TOKENS.another); 226 | 227 | container.restore!(); 228 | 229 | const fourthSomeInstance = container.get(TOKENS.some); 230 | const thirdAnotherInstance = container.get(TOKENS.another); 231 | 232 | container.restore!(); 233 | 234 | const fifthSomeInstance = container.get(TOKENS.some); 235 | const fourthAnotherInstance = container.get(TOKENS.another); 236 | 237 | expect(firstSomeInstance).toBe(secondSomeInstance); 238 | expect(secondSomeInstance).toBe(thirdSomeInstance); 239 | expect(thirdSomeInstance).toBe(fourthSomeInstance); 240 | expect(fourthSomeInstance).toBe(fifthSomeInstance); 241 | 242 | expect(firstAnotherInstance).not.toBe(secondAnotherInstance); 243 | expect(secondAnotherInstance).not.toBe(thirdAnotherInstance); 244 | expect(thirdAnotherInstance).not.toBe(fourthAnotherInstance); 245 | }); 246 | 247 | it('captures a container scoped binding state to a snapshot', () => { 248 | class Some {} 249 | 250 | const TOKENS = { 251 | some: token('some'), 252 | another: token('another'), 253 | }; 254 | 255 | const container = new Container(); 256 | container.bind(TOKENS.some).toInstance(Some).inContainerScope(); 257 | container.bind(TOKENS.another).toInstance(Some).inContainerScope(); 258 | 259 | const firstSomeInstance = container.get(TOKENS.some); 260 | 261 | container.capture!(); 262 | 263 | const secondSomeInstance = container.get(TOKENS.some); 264 | const firstAnotherInstance = container.get(TOKENS.another); 265 | 266 | container.restore!(); 267 | container.capture!(); 268 | 269 | const thirdSomeInstance = container.get(TOKENS.some); 270 | const secondAnotherInstance = container.get(TOKENS.another); 271 | 272 | container.restore!(); 273 | 274 | const fourthSomeInstance = container.get(TOKENS.some); 275 | const thirdAnotherInstance = container.get(TOKENS.another); 276 | 277 | container.restore!(); 278 | 279 | const fifthSomeInstance = container.get(TOKENS.some); 280 | const fourthAnotherInstance = container.get(TOKENS.another); 281 | 282 | expect(firstSomeInstance).toBe(secondSomeInstance); 283 | expect(secondSomeInstance).not.toBe(thirdSomeInstance); 284 | expect(thirdSomeInstance).not.toBe(fourthSomeInstance); 285 | expect(fourthSomeInstance).not.toBe(fifthSomeInstance); 286 | 287 | expect(firstAnotherInstance).not.toBe(secondAnotherInstance); 288 | expect(secondAnotherInstance).not.toBe(thirdAnotherInstance); 289 | expect(thirdAnotherInstance).not.toBe(fourthAnotherInstance); 290 | }); 291 | 292 | it('captures a dependency module state to a snapshot', () => { 293 | class Some {} 294 | 295 | const TOKENS = { 296 | some: token('some'), 297 | }; 298 | 299 | const dependencyModule = new DependencyModule(); 300 | dependencyModule.bind(TOKENS.some).toInstance(Some).inSingletonScope(); 301 | 302 | const container = new Container(); 303 | container.use(TOKENS.some).from(dependencyModule); 304 | 305 | container.capture!(); 306 | const firstSomeInstance = container.get(TOKENS.some); 307 | 308 | container.restore!(); 309 | container.capture!(); 310 | const secondSomeInstance = container.get(TOKENS.some); 311 | 312 | container.restore!(); 313 | const thirdSomeInstance = container.get(TOKENS.some); 314 | 315 | container.restore!(); 316 | const fourthSomeInstance = container.get(TOKENS.some); 317 | 318 | expect(firstSomeInstance).not.toBe(secondSomeInstance); 319 | expect(secondSomeInstance).not.toBe(thirdSomeInstance); 320 | expect(thirdSomeInstance).not.toBe(fourthSomeInstance); 321 | }); 322 | 323 | it('logs an error when trying to restore a non-captured container state', () => { 324 | const spy = jest.spyOn(console, 'error').mockImplementation(() => null); 325 | 326 | const container = new Container(); 327 | container.restore!(); 328 | 329 | expect(spy).toHaveBeenCalledTimes(1); 330 | expect(spy.mock.calls[0]?.[0]).toMatchSnapshot(); 331 | 332 | spy.mockRestore(); 333 | }); 334 | 335 | it('does not log an error when restoring the captured container state', () => { 336 | const spy = jest.spyOn(console, 'error').mockImplementation(() => null); 337 | 338 | const container = new Container(); 339 | container.capture!(); 340 | container.restore!(); 341 | 342 | expect(spy).toHaveBeenCalledTimes(0); 343 | 344 | spy.mockRestore(); 345 | }); 346 | 347 | it("does not include capturing in 'production' env", () => { 348 | const restoreEnv = setEnv('production'); 349 | 350 | const container = new Container(); 351 | 352 | expect(container.capture).toBeUndefined(); 353 | expect(container.restore).toBeUndefined(); 354 | 355 | restoreEnv(); 356 | }); 357 | 358 | it("creates a container by 'createContainer'", () => { 359 | expect(createContainer()).toBeInstanceOf(Container); 360 | }); 361 | 362 | describe('typings', () => { 363 | it('does not allow to bind an optional token', () => { 364 | expect.assertions(0); 365 | 366 | const TOKENS = { 367 | some: token('some'), 368 | }; 369 | 370 | const container = new Container(); 371 | 372 | // @ts-expect-error: Argument of type 'OptionalToken' is not assignable to parameter of type 'Token'. 373 | container.bind(TOKENS.some.optional); 374 | }); 375 | }); 376 | }); 377 | -------------------------------------------------------------------------------- /packages/brandi/spec/dependency-modules.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Container, 3 | DependencyModule, 4 | createDependencyModule, 5 | injected, 6 | tag, 7 | tagged, 8 | token, 9 | } from '../src'; 10 | 11 | describe('dependency modules', () => { 12 | it('uses a binding from the dependency module', () => { 13 | const value = 1; 14 | 15 | const TOKENS = { 16 | value: token('value'), 17 | }; 18 | 19 | const dependencyModule = new DependencyModule(); 20 | dependencyModule.bind(TOKENS.value).toConstant(value); 21 | 22 | const container = new Container(); 23 | container.use(TOKENS.value).from(dependencyModule); 24 | 25 | expect(container.get(TOKENS.value)).toBe(value); 26 | }); 27 | 28 | it('uses multiple bindings from the dependency module', () => { 29 | const num = 1; 30 | const str = ''; 31 | 32 | const TOKENS = { 33 | num: token('num'), 34 | str: token('str'), 35 | }; 36 | 37 | const dependencyModule = new DependencyModule(); 38 | dependencyModule.bind(TOKENS.num).toConstant(num); 39 | dependencyModule.bind(TOKENS.str).toConstant(str); 40 | 41 | const container = new Container(); 42 | container.use(TOKENS.num, TOKENS.str).from(dependencyModule); 43 | 44 | expect(container.get(TOKENS.num)).toBe(num); 45 | expect(container.get(TOKENS.str)).toBe(str); 46 | }); 47 | 48 | it('uses a binding with dependencies from the dependency module', () => { 49 | const value = 1; 50 | 51 | class Some { 52 | constructor(public num: number) {} 53 | } 54 | 55 | const TOKENS = { 56 | num: token('value'), 57 | some: token('some'), 58 | }; 59 | 60 | injected(Some, TOKENS.num); 61 | 62 | const dependencyModule = new DependencyModule(); 63 | dependencyModule.bind(TOKENS.num).toConstant(value); 64 | dependencyModule.bind(TOKENS.some).toInstance(Some).inTransientScope(); 65 | 66 | const container = new Container(); 67 | container.use(TOKENS.some).from(dependencyModule); 68 | 69 | const instance = container.get(TOKENS.some); 70 | 71 | expect(instance).toBeInstanceOf(Some); 72 | expect(instance.num).toBe(value); 73 | }); 74 | 75 | it("ignores dependency module's binding if there is container's binding", () => { 76 | const moduleValue = 1; 77 | const containerValue = 1; 78 | 79 | class Some { 80 | constructor(public num: number) {} 81 | } 82 | 83 | const TOKENS = { 84 | num: token('value'), 85 | some: token('some'), 86 | }; 87 | 88 | injected(Some, TOKENS.num); 89 | 90 | const dependencyModule = new DependencyModule(); 91 | dependencyModule.bind(TOKENS.num).toConstant(moduleValue); 92 | dependencyModule.bind(TOKENS.some).toInstance(Some).inTransientScope(); 93 | 94 | const container = new Container(); 95 | container.use(TOKENS.some).from(dependencyModule); 96 | container.bind(TOKENS.num).toConstant(containerValue); 97 | 98 | const instance = container.get(TOKENS.some); 99 | 100 | expect(instance).toBeInstanceOf(Some); 101 | expect(instance.num).toBe(containerValue); 102 | }); 103 | 104 | it("ignores dependency module's binding if there is container parent's binding", () => { 105 | const moduleValue = 1; 106 | const containerValue = 1; 107 | 108 | class Some { 109 | constructor(public num: number) {} 110 | } 111 | 112 | const TOKENS = { 113 | num: token('value'), 114 | some: token('some'), 115 | }; 116 | 117 | injected(Some, TOKENS.num); 118 | 119 | const dependencyModule = new DependencyModule(); 120 | dependencyModule.bind(TOKENS.num).toConstant(moduleValue); 121 | dependencyModule.bind(TOKENS.some).toInstance(Some).inTransientScope(); 122 | 123 | const parentContainer = new Container(); 124 | parentContainer.bind(TOKENS.num).toConstant(containerValue); 125 | 126 | const childContainer = new Container().extend(parentContainer); 127 | childContainer.use(TOKENS.some).from(dependencyModule); 128 | 129 | const instance = childContainer.get(TOKENS.some); 130 | 131 | expect(instance).toBeInstanceOf(Some); 132 | expect(instance.num).toBe(containerValue); 133 | }); 134 | 135 | it('uses hierarchical dependency modules', () => { 136 | const value = 1; 137 | 138 | class SomeDependency { 139 | constructor(public num: number) {} 140 | } 141 | 142 | class AnotherDependency { 143 | constructor(public some: SomeDependency) {} 144 | } 145 | 146 | class Target { 147 | constructor(public another: AnotherDependency) {} 148 | } 149 | 150 | const TOKENS = { 151 | num: token('num'), 152 | someDependency: token('someDependency'), 153 | anotherDependency: token('anotherDependency'), 154 | target: token('target'), 155 | }; 156 | 157 | injected(SomeDependency, TOKENS.num); 158 | injected(AnotherDependency, TOKENS.someDependency); 159 | injected(Target, TOKENS.anotherDependency); 160 | 161 | const someModule = new DependencyModule(); 162 | someModule.bind(TOKENS.num).toConstant(value); 163 | someModule 164 | .bind(TOKENS.someDependency) 165 | .toInstance(SomeDependency) 166 | .inTransientScope(); 167 | 168 | const anotherModule = new DependencyModule(); 169 | anotherModule.use(TOKENS.someDependency).from(someModule); 170 | anotherModule 171 | .bind(TOKENS.anotherDependency) 172 | .toInstance(AnotherDependency) 173 | .inTransientScope(); 174 | 175 | const container = new Container(); 176 | container.use(TOKENS.anotherDependency).from(anotherModule); 177 | container.bind(TOKENS.target).toInstance(Target).inTransientScope(); 178 | 179 | const instance = container.get(TOKENS.target); 180 | 181 | expect(instance.another).toBeInstanceOf(AnotherDependency); 182 | expect(instance.another.some).toBeInstanceOf(SomeDependency); 183 | }); 184 | 185 | it("ignores dependency module's binding if there is a higher-level dependency module's binding", () => { 186 | const someValue = 1; 187 | const anotherValue = 2; 188 | 189 | class SomeDependency { 190 | constructor(public num: number) {} 191 | } 192 | 193 | class AnotherDependency { 194 | constructor(public some: SomeDependency) {} 195 | } 196 | 197 | class Target { 198 | constructor( 199 | public some: SomeDependency, 200 | public another: AnotherDependency, 201 | ) {} 202 | } 203 | 204 | const TOKENS = { 205 | num: token('num'), 206 | someDependency: token('someDependency'), 207 | anotherDependency: token('anotherDependency'), 208 | target: token('target'), 209 | }; 210 | 211 | injected(SomeDependency, TOKENS.num); 212 | injected(AnotherDependency, TOKENS.someDependency); 213 | injected(Target, TOKENS.someDependency, TOKENS.anotherDependency); 214 | 215 | const someModule = new DependencyModule(); 216 | someModule.bind(TOKENS.num).toConstant(someValue); 217 | someModule 218 | .bind(TOKENS.someDependency) 219 | .toInstance(SomeDependency) 220 | .inTransientScope(); 221 | 222 | const anotherModule = new DependencyModule(); 223 | anotherModule.use(TOKENS.someDependency).from(someModule); 224 | anotherModule.bind(TOKENS.num).toConstant(anotherValue); 225 | anotherModule 226 | .bind(TOKENS.anotherDependency) 227 | .toInstance(AnotherDependency) 228 | .inTransientScope(); 229 | 230 | const container = new Container(); 231 | container.use(TOKENS.someDependency).from(someModule); 232 | container.use(TOKENS.anotherDependency).from(anotherModule); 233 | container.bind(TOKENS.target).toInstance(Target).inTransientScope(); 234 | 235 | const instance = container.get(TOKENS.target); 236 | 237 | expect(instance.some.num).toBe(someValue); 238 | expect(instance.another.some.num).toBe(anotherValue); 239 | }); 240 | 241 | it('uses a dependency module by conditions', () => { 242 | class SomeDependency {} 243 | class AnotherDependency {} 244 | 245 | class Some { 246 | constructor(public dependency: SomeDependency) {} 247 | } 248 | 249 | class Another { 250 | constructor(public dependency: SomeDependency) {} 251 | } 252 | 253 | const TOKENS = { 254 | dependency: token('dependency'), 255 | some: token('some'), 256 | another: token('another'), 257 | }; 258 | 259 | injected(Some, TOKENS.dependency); 260 | injected(Another, TOKENS.dependency); 261 | 262 | const someDependencyModule = new DependencyModule(); 263 | someDependencyModule 264 | .bind(TOKENS.dependency) 265 | .toInstance(SomeDependency) 266 | .inTransientScope(); 267 | 268 | const anotherDependencyModule = new DependencyModule(); 269 | anotherDependencyModule 270 | .bind(TOKENS.dependency) 271 | .toInstance(AnotherDependency) 272 | .inTransientScope(); 273 | 274 | const container = new Container(); 275 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 276 | container.bind(TOKENS.another).toInstance(Another).inTransientScope(); 277 | container.use(TOKENS.dependency).from(someDependencyModule); 278 | container 279 | .when(Another) 280 | .use(TOKENS.dependency) 281 | .from(anotherDependencyModule); 282 | 283 | const someInstance = container.get(TOKENS.some); 284 | const anotherInstance = container.get(TOKENS.another); 285 | 286 | expect(someInstance.dependency).toBeInstanceOf(SomeDependency); 287 | expect(anotherInstance.dependency).toBeInstanceOf(AnotherDependency); 288 | }); 289 | 290 | it('gets a dependency from the dependency module by conditions', () => { 291 | const someValue = 1; 292 | const anotherValue = 2; 293 | 294 | class Some { 295 | constructor(public num: number) {} 296 | } 297 | 298 | class Another { 299 | constructor(public num: number) {} 300 | } 301 | 302 | const TOKENS = { 303 | num: token('num'), 304 | some: token('some'), 305 | another: token('another'), 306 | }; 307 | 308 | const TAGS = { 309 | another: tag('another'), 310 | }; 311 | 312 | injected(Some, TOKENS.num); 313 | 314 | injected(Another, TOKENS.num); 315 | tagged(Another, TAGS.another); 316 | 317 | const dependencyModule = new DependencyModule(); 318 | dependencyModule.bind(TOKENS.num).toConstant(someValue); 319 | dependencyModule 320 | .when(TAGS.another) 321 | .bind(TOKENS.num) 322 | .toConstant(anotherValue); 323 | 324 | const container = new Container(); 325 | container.use(TOKENS.num).from(dependencyModule); 326 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 327 | container.bind(TOKENS.another).toInstance(Another).inTransientScope(); 328 | 329 | const someInstance = container.get(TOKENS.some); 330 | const anotherInstance = container.get(TOKENS.another); 331 | 332 | expect(someInstance.num).toBe(someValue); 333 | expect(anotherInstance.num).toBe(anotherValue); 334 | }); 335 | 336 | it('separates resolution chains', () => { 337 | const someValue = 1; 338 | const anotherValue = 2; 339 | 340 | class SomeDependency { 341 | constructor(public num: number) {} 342 | } 343 | class AnotherDependency { 344 | constructor(public num: number) {} 345 | } 346 | 347 | class Target { 348 | constructor( 349 | public some: SomeDependency, 350 | public another: AnotherDependency, 351 | ) {} 352 | } 353 | 354 | const TOKENS = { 355 | num: token('num'), 356 | someDependency: token('someDependency'), 357 | anotherDependency: token('anotherDependency'), 358 | target: token('target'), 359 | }; 360 | 361 | injected(SomeDependency, TOKENS.num); 362 | injected(AnotherDependency, TOKENS.num); 363 | injected(Target, TOKENS.someDependency, TOKENS.anotherDependency); 364 | 365 | const someDependencyModule = new DependencyModule(); 366 | someDependencyModule.bind(TOKENS.num).toConstant(someValue); 367 | someDependencyModule 368 | .bind(TOKENS.someDependency) 369 | .toInstance(SomeDependency) 370 | .inTransientScope(); 371 | 372 | const anotherDependencyModule = new DependencyModule(); 373 | anotherDependencyModule.bind(TOKENS.num).toConstant(anotherValue); 374 | anotherDependencyModule 375 | .bind(TOKENS.anotherDependency) 376 | .toInstance(AnotherDependency) 377 | .inTransientScope(); 378 | 379 | const container = new Container(); 380 | container.use(TOKENS.someDependency).from(someDependencyModule); 381 | container.use(TOKENS.anotherDependency).from(anotherDependencyModule); 382 | container.bind(TOKENS.target).toInstance(Target).inTransientScope(); 383 | 384 | const instance = container.get(TOKENS.target); 385 | 386 | expect(instance.some).toBeInstanceOf(SomeDependency); 387 | expect(instance.another).toBeInstanceOf(AnotherDependency); 388 | expect(instance.some.num).toBe(someValue); 389 | expect(instance.another.num).toBe(anotherValue); 390 | }); 391 | 392 | it('uses a container as dependency module', () => { 393 | const value = 1; 394 | 395 | const TOKENS = { 396 | value: token('value'), 397 | }; 398 | 399 | const containerAsModule = new Container(); 400 | containerAsModule.bind(TOKENS.value).toConstant(value); 401 | 402 | const container = new Container(); 403 | container.use(TOKENS.value).from(containerAsModule); 404 | 405 | expect(container.get(TOKENS.value)).toBe(value); 406 | }); 407 | 408 | it('uses a container with parent as dependency module', () => { 409 | const value = 1; 410 | 411 | const TOKENS = { 412 | value: token('value'), 413 | }; 414 | 415 | const parentContainerAsModule = new Container(); 416 | parentContainerAsModule.bind(TOKENS.value).toConstant(value); 417 | 418 | const childContainerAsModule = new Container().extend( 419 | parentContainerAsModule, 420 | ); 421 | 422 | const container = new Container(); 423 | container.use(TOKENS.value).from(childContainerAsModule); 424 | 425 | expect(container.get(TOKENS.value)).toBe(value); 426 | }); 427 | 428 | it("creates a dependency module by 'createDependencyModule'", () => { 429 | expect(createDependencyModule()).toBeInstanceOf(DependencyModule); 430 | }); 431 | }); 432 | -------------------------------------------------------------------------------- /packages/brandi/spec/injected.spec.ts: -------------------------------------------------------------------------------- 1 | import { injected, token } from '../src'; 2 | import { injectsRegistry } from '../src/registries'; 3 | 4 | describe('injected', () => { 5 | beforeAll(() => { 6 | injectsRegistry.clear(); 7 | }); 8 | 9 | afterEach(() => { 10 | injectsRegistry.clear(); 11 | }); 12 | 13 | it('registers a single token', () => { 14 | class Some { 15 | constructor(public value: number) {} 16 | } 17 | 18 | const TOKENS = { 19 | value: token('value'), 20 | }; 21 | 22 | injected(Some, TOKENS.value); 23 | 24 | expect(injectsRegistry.get(Some)).toStrictEqual([TOKENS.value]); 25 | }); 26 | 27 | it('registers multiple tokens', () => { 28 | class Some { 29 | constructor(public num: number, public str: string) {} 30 | } 31 | 32 | const TOKENS = { 33 | num: token('num'), 34 | str: token('value'), 35 | }; 36 | 37 | injected(Some, TOKENS.num, TOKENS.str); 38 | 39 | expect(injectsRegistry.get(Some)).toStrictEqual([TOKENS.num, TOKENS.str]); 40 | }); 41 | 42 | it('registers an optional token', () => { 43 | class Some { 44 | constructor(public required: number, public optional?: string) {} 45 | } 46 | 47 | const TOKENS = { 48 | some: token('some'), 49 | another: token('another'), 50 | }; 51 | 52 | injected(Some, TOKENS.some, TOKENS.another.optional); 53 | 54 | expect(injectsRegistry.get(Some)).toStrictEqual([ 55 | TOKENS.some, 56 | TOKENS.another.optional, 57 | ]); 58 | }); 59 | 60 | it('rewrites tokens', () => { 61 | class Some { 62 | constructor(public value: number) {} 63 | } 64 | 65 | const TOKENS = { 66 | some: token('some'), 67 | another: token('another'), 68 | }; 69 | 70 | injected(Some, TOKENS.some); 71 | injected(Some, TOKENS.another); 72 | 73 | expect(injectsRegistry.get(Some)).toStrictEqual([TOKENS.another]); 74 | }); 75 | 76 | describe('typings', () => { 77 | it('requires to pass dependencies', () => { 78 | expect.assertions(0); 79 | 80 | class Some { 81 | constructor(public value: number) {} 82 | } 83 | 84 | class Another { 85 | constructor(public value?: number) {} 86 | } 87 | 88 | const createSome = (value: number): Some => new Some(value); 89 | 90 | const createAnother = (value?: number): Another => new Another(value); 91 | 92 | // @ts-expect-error: Arguments for the rest parameter 'tokens' were not provided. 93 | injected(Some); 94 | 95 | // @ts-expect-error: Arguments for the rest parameter 'tokens' were not provided. 96 | injected(Another); 97 | 98 | // @ts-expect-error: Arguments for the rest parameter 'tokens' were not provided. 99 | injected(createSome); 100 | 101 | // @ts-expect-error: Arguments for the rest parameter 'tokens' were not provided. 102 | injected(createAnother); 103 | }); 104 | 105 | it('requires to pass the same type of dependency and token', () => { 106 | expect.assertions(0); 107 | 108 | class Some { 109 | constructor(public num: number, public str?: string) {} 110 | } 111 | 112 | const createSome = (num: number, str?: string): Some => 113 | new Some(num, str); 114 | 115 | const TOKENS = { 116 | num: token('num'), 117 | str: token('str'), 118 | }; 119 | 120 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 121 | injected(Some, TOKENS.num, TOKENS.num); 122 | 123 | // @ts-expect-error: Argument of type 'OptionalToken' is not assignable to parameter of type 'OptionalToken'. 124 | injected(Some, TOKENS.num, TOKENS.num.optional); 125 | 126 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 127 | injected(createSome, TOKENS.num, TOKENS.num); 128 | 129 | // @ts-expect-error: Argument of type 'OptionalToken' is not assignable to parameter of type 'OptionalToken'. 130 | injected(createSome, TOKENS.num, TOKENS.num.optional); 131 | }); 132 | 133 | it('requires to separate required and optional dependencies', () => { 134 | expect.assertions(0); 135 | 136 | class Some { 137 | constructor(public required: number, public optional?: string) {} 138 | } 139 | 140 | const createSome = (required: number, optional?: string): Some => 141 | new Some(required, optional); 142 | 143 | const TOKENS = { 144 | some: token('some'), 145 | another: token('another'), 146 | }; 147 | 148 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 149 | injected(Some, TOKENS.some, TOKENS.another); 150 | 151 | // @ts-expect-error: Argument of type 'OptionalToken' is not assignable to parameter of type 'RequiredToken'. 152 | injected(Some, TOKENS.some.optional, TOKENS.another.optional); 153 | 154 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 155 | injected(createSome, TOKENS.some, TOKENS.another); 156 | 157 | // @ts-expect-error: Argument of type 'OptionalToken' is not assignable to parameter of type 'RequiredToken'. 158 | injected(createSome, TOKENS.some.optional, TOKENS.another.optional); 159 | }); 160 | 161 | it("requires to pass optional token for 'unknown' and 'any' dependencies", () => { 162 | expect.assertions(0); 163 | 164 | class Some { 165 | constructor(public any: any, public unknown: unknown) {} 166 | } 167 | 168 | const createSome = (any: any, unknown: unknown): Some => 169 | new Some(any, unknown); 170 | 171 | const TOKENS = { 172 | any: token('any'), 173 | unknown: token('unknown'), 174 | }; 175 | 176 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 177 | injected(Some, TOKENS.any, TOKENS.unknown); 178 | 179 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 180 | injected(Some, TOKENS.any.optional, TOKENS.unknown); 181 | 182 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 183 | injected(Some, TOKENS.any, TOKENS.unknown.optional); 184 | 185 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 186 | injected(createSome, TOKENS.any, TOKENS.unknown); 187 | 188 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 189 | injected(createSome, TOKENS.any.optional, TOKENS.unknown); 190 | 191 | // @ts-expect-error: Argument of type 'Token' is not assignable to parameter of type 'OptionalToken'. 192 | injected(createSome, TOKENS.any, TOKENS.unknown.optional); 193 | 194 | injected(Some, TOKENS.any.optional, TOKENS.unknown.optional); 195 | injected(createSome, TOKENS.any.optional, TOKENS.unknown.optional); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /packages/brandi/spec/optional-dependencies.spec.ts: -------------------------------------------------------------------------------- 1 | import { Container, injected, token } from '../src'; 2 | 3 | describe('optional dependencies', () => { 4 | it('gets an optional dependency from the container', () => { 5 | const value = 1; 6 | 7 | const TOKENS = { 8 | some: token('some'), 9 | another: token('another'), 10 | }; 11 | 12 | const container = new Container(); 13 | 14 | container.bind(TOKENS.some).toConstant(value); 15 | 16 | expect(container.get(TOKENS.some.optional)).toBe(value); 17 | expect(container.get(TOKENS.another.optional)).toBeUndefined(); 18 | }); 19 | 20 | it('injects an optional dependency', () => { 21 | const someValue = 1; 22 | 23 | class Some { 24 | constructor(public value?: number) {} 25 | } 26 | 27 | const TOKENS = { 28 | value: token('value'), 29 | some: token('some'), 30 | }; 31 | 32 | injected(Some, TOKENS.value.optional); 33 | 34 | const container = new Container(); 35 | 36 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 37 | 38 | const firstInstance = container.get(TOKENS.some); 39 | 40 | container.bind(TOKENS.value).toConstant(someValue); 41 | 42 | const secondInstance = container.get(TOKENS.some); 43 | 44 | expect(firstInstance.value).toBeUndefined(); 45 | expect(secondInstance.value).toBe(someValue); 46 | }); 47 | 48 | it("allows to skip 'injected' registration of entities where all dependencies have a default value", () => { 49 | const defaultNum = 1; 50 | const defaultStr = 'A'; 51 | 52 | class Some { 53 | constructor( 54 | public num: number = defaultNum, 55 | public str: string = defaultStr, 56 | ) {} 57 | } 58 | 59 | const TOKENS = { 60 | some: token('some'), 61 | }; 62 | 63 | const container = new Container(); 64 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 65 | 66 | const instance = container.get(TOKENS.some); 67 | 68 | expect(instance.num).toBe(defaultNum); 69 | expect(instance.str).toBe(defaultStr); 70 | }); 71 | 72 | it('ignores a default value of an optional dependency if the dependency was got from the container', () => { 73 | const defaultValue = 1; 74 | const someValue = 2; 75 | 76 | class Some { 77 | constructor(public value: number = defaultValue) {} 78 | } 79 | 80 | const TOKENS = { 81 | value: token('value'), 82 | some: token('some'), 83 | }; 84 | 85 | injected(Some, TOKENS.value.optional); 86 | 87 | const container = new Container(); 88 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 89 | container.bind(TOKENS.value).toConstant(someValue); 90 | 91 | const instance = container.get(TOKENS.some); 92 | 93 | expect(instance.value).toBe(someValue); 94 | }); 95 | 96 | it('uses the default value if the optional dependency was not injected', () => { 97 | const defaultValue = 1; 98 | 99 | class Some { 100 | constructor(public value: number = defaultValue) {} 101 | } 102 | 103 | const TOKENS = { 104 | value: token('value'), 105 | some: token('some'), 106 | }; 107 | 108 | injected(Some, TOKENS.value.optional); 109 | 110 | const container = new Container(); 111 | container.bind(TOKENS.some).toInstance(Some).inTransientScope(); 112 | 113 | const instance = container.get(TOKENS.some); 114 | 115 | expect(instance.value).toBe(defaultValue); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/brandi/spec/tagged.spec.ts: -------------------------------------------------------------------------------- 1 | import { tag, tagged } from '../src'; 2 | import { tagsRegistry } from '../src/registries'; 3 | 4 | describe('tagged', () => { 5 | beforeAll(() => { 6 | tagsRegistry.clear(); 7 | }); 8 | 9 | afterEach(() => { 10 | tagsRegistry.clear(); 11 | }); 12 | 13 | it('registers a single tag', () => { 14 | class Some {} 15 | 16 | const TAGS = { 17 | some: tag('some'), 18 | }; 19 | 20 | tagged(Some, TAGS.some); 21 | 22 | expect(tagsRegistry.get(Some)).toStrictEqual([TAGS.some]); 23 | }); 24 | 25 | it('registers multiple tags', () => { 26 | class Some {} 27 | 28 | const TAGS = { 29 | some: tag('some'), 30 | another: tag('another'), 31 | }; 32 | 33 | tagged(Some, TAGS.some, TAGS.another); 34 | 35 | expect(tagsRegistry.get(Some)).toStrictEqual([TAGS.some, TAGS.another]); 36 | }); 37 | 38 | it('rewrites tags', () => { 39 | class Some {} 40 | 41 | const TAGS = { 42 | some: tag('some'), 43 | another: tag('another'), 44 | }; 45 | 46 | tagged(Some, TAGS.some); 47 | tagged(Some, TAGS.another); 48 | 49 | expect(tagsRegistry.get(Some)).toStrictEqual([TAGS.another]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/brandi/spec/toConstant.spec.ts: -------------------------------------------------------------------------------- 1 | import { Container, token } from '../src'; 2 | 3 | describe('toConstant', () => { 4 | it('returns a primitive value', () => { 5 | const value = 0; 6 | 7 | const TOKENS = { 8 | value: token('value'), 9 | }; 10 | 11 | const container = new Container(); 12 | container.bind(TOKENS.value).toConstant(value); 13 | 14 | expect(container.get(TOKENS.value)).toBe(value); 15 | }); 16 | 17 | it('returns a constructor', () => { 18 | class Some {} 19 | 20 | const TOKENS = { 21 | SomeCtor: token('SomeCtor'), 22 | }; 23 | 24 | const container = new Container(); 25 | container.bind(TOKENS.SomeCtor).toConstant(Some); 26 | 27 | expect(container.get(TOKENS.SomeCtor)).toBe(Some); 28 | }); 29 | 30 | describe('typings', () => { 31 | it('requires to bind the same type of dependency and token', () => { 32 | expect.assertions(0); 33 | 34 | const TOKENS = { 35 | num: token('num'), 36 | }; 37 | 38 | const container = new Container(); 39 | 40 | // @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'number'. 41 | container.bind(TOKENS.num).toConstant(''); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/brandi/spec/utils.ts: -------------------------------------------------------------------------------- 1 | export type Expect = T; 2 | 3 | export type Equal = (() => T extends X ? 1 : 2) extends < 4 | T 5 | >() => T extends Y ? 1 : 2 6 | ? true 7 | : false; 8 | 9 | export const setEnv = (value: string): (() => void) => { 10 | const env = { ...process.env }; 11 | process.env.NODE_ENV = value; 12 | 13 | return () => { 14 | process.env = { ...env }; 15 | }; 16 | }; 17 | 18 | export const wait = (callback: () => T): Promise => 19 | new Promise((resolve) => { 20 | setTimeout(() => { 21 | resolve(callback()); 22 | }, 20); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/brandi/src/container/BindingsVault.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionCondition, UnknownCreator } from '../types'; 2 | import { Token, TokenValue, tag as createTag } from '../pointers'; 3 | 4 | import { Binding } from './bindings'; 5 | import { ResolutionCache } from './ResolutionCache'; 6 | 7 | type BindingsMap = Map; 8 | 9 | export class BindingsVault { 10 | private static notag = createTag('NO_TAG'); 11 | 12 | public parent: BindingsVault | null = null; 13 | 14 | private readonly map = new Map(); 15 | 16 | public copy?(): BindingsVault; 17 | 18 | constructor() { 19 | if (process.env.NODE_ENV !== 'production') { 20 | this.copy = (): BindingsVault => 21 | this.from((prev) => { 22 | const next = new Map(); 23 | prev.forEach((binding, key) => { 24 | if (binding instanceof BindingsVault) { 25 | next.set(key, binding.copy!()); 26 | } else { 27 | next.set(key, binding.clone?.() ?? binding); 28 | } 29 | }); 30 | return next; 31 | }); 32 | } 33 | } 34 | 35 | public set( 36 | binding: Binding | BindingsVault, 37 | token: Token, 38 | condition: ResolutionCondition = BindingsVault.notag, 39 | ): void { 40 | const current = this.map.get(token.__s); 41 | 42 | if (current) current.set(condition, binding); 43 | else 44 | this.map.set( 45 | token.__s, 46 | new Map().set( 47 | condition, 48 | binding, 49 | ), 50 | ); 51 | } 52 | 53 | private find( 54 | token: TokenValue, 55 | conditions?: ResolutionCondition[], 56 | target?: UnknownCreator, 57 | ): Binding | BindingsVault | undefined { 58 | const bindings = this.map.get(token.__s); 59 | 60 | if (bindings === undefined) return undefined; 61 | 62 | if (target) { 63 | const targetBinding = bindings.get(target); 64 | if (targetBinding) return targetBinding; 65 | } 66 | 67 | if ( 68 | process.env.NODE_ENV !== 'production' && 69 | conditions && 70 | conditions.reduce( 71 | (acc, condition) => (bindings.has(condition) ? acc + 1 : acc), 72 | 0, 73 | ) > 1 74 | ) { 75 | const conditionsDisplayString = conditions 76 | .map((condition) => 77 | typeof condition === 'function' 78 | ? condition.name 79 | : `tag(${condition.description})`, 80 | ) 81 | .join(', '); 82 | 83 | console.warn( 84 | 'Warning: ' + 85 | `When resolving a binding by '${token.__d}' token with [${conditionsDisplayString}] conditions, ` + 86 | 'more than one binding was found. ' + 87 | "In this case, Brandi resolves the binding by the first tag assigned by 'tagged(target, ...tags)' function " + 88 | "or, if you explicitly passed conditions through 'Container.get(token, conditions)' method, " + 89 | 'by the first resolved condition. ' + 90 | 'Try to avoid such implicit logic.', 91 | ); 92 | } 93 | 94 | if (conditions) { 95 | for (let i = 0, len = conditions.length; i < len; i += 1) { 96 | const binding = bindings.get(conditions[i]!); 97 | if (binding) return binding; 98 | } 99 | } 100 | 101 | return bindings.get(BindingsVault.notag); 102 | } 103 | 104 | private resolve( 105 | token: TokenValue, 106 | cache: ResolutionCache, 107 | conditions?: ResolutionCondition[], 108 | target?: UnknownCreator, 109 | ): Binding | null { 110 | const binding = this.find(token, conditions, target); 111 | 112 | if (binding === undefined) 113 | return this.parent 114 | ? this.parent.resolve(token, cache, conditions, target) 115 | : null; 116 | 117 | if (binding instanceof BindingsVault) { 118 | cache.vaults.push(binding); 119 | return binding.resolve(token, cache, conditions, target); 120 | } 121 | 122 | return binding; 123 | } 124 | 125 | public get( 126 | token: TokenValue, 127 | cache: ResolutionCache, 128 | conditions?: ResolutionCondition[], 129 | target?: UnknownCreator, 130 | ): Binding | null { 131 | const ownBinding = this.resolve(token, cache, conditions, target); 132 | 133 | if (ownBinding) return ownBinding; 134 | 135 | for (let i = 0, v = cache.vaults, len = v.length; i < len; i += 1) { 136 | const cacheBinding = v[i]!.resolve(token, cache, conditions, target); 137 | if (cacheBinding) return cacheBinding; 138 | } 139 | 140 | return null; 141 | } 142 | 143 | private from( 144 | callback: (bindings: BindingsMap) => BindingsMap, 145 | ): BindingsVault { 146 | const vault = new BindingsVault(); 147 | vault.parent = this.parent; 148 | 149 | this.map.forEach((bindings, key) => { 150 | vault.map.set(key, callback(bindings)); 151 | }); 152 | 153 | return vault; 154 | } 155 | 156 | public clone(): BindingsVault { 157 | return this.from( 158 | (prev) => new Map(prev), 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packages/brandi/src/container/Container.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionCondition, UnknownCreator } from '../types'; 2 | import { TokenType, TokenValue } from '../pointers'; 3 | import { callableRegistry, injectsRegistry, tagsRegistry } from '../registries'; 4 | 5 | import { 6 | Binding, 7 | FactoryInitializer, 8 | InstanceBinding, 9 | isFactoryBinding, 10 | isInstanceBinding, 11 | isInstanceContainerScopedBinding, 12 | isInstanceResolutionScopedBinding, 13 | isInstanceSingletonScopedBinding, 14 | } from './bindings'; 15 | import { BindingsVault } from './BindingsVault'; 16 | import { DependencyModule } from './DependencyModule'; 17 | import { ResolutionCache } from './ResolutionCache'; 18 | 19 | export class Container extends DependencyModule { 20 | private snapshot: BindingsVault | null = null; 21 | 22 | /** 23 | * @description 24 | * Captures (snapshots) the current container state. 25 | * 26 | * @link https://brandi.js.org/reference/container#capture 27 | */ 28 | public capture?(): void; 29 | 30 | /** 31 | * @description 32 | * Restores the captured container state. 33 | * 34 | * @link https://brandi.js.org/reference/container#restore 35 | */ 36 | public restore?(): void; 37 | 38 | constructor() { 39 | super(); 40 | 41 | if (process.env.NODE_ENV !== 'production') { 42 | this.capture = (): void => { 43 | this.snapshot = this.vault.copy!(); 44 | }; 45 | 46 | this.restore = (): void => { 47 | if (this.snapshot) { 48 | this.vault = this.snapshot.copy!(); 49 | } else { 50 | console.error( 51 | "Error: It looks like a trying to restore a non-captured container state. Did you forget to call 'capture()' method?", 52 | ); 53 | } 54 | }; 55 | } 56 | } 57 | 58 | /** 59 | * @description 60 | * Sets the parent container. 61 | * 62 | * @param container - a `Container` or `null` that will be set as the parent container. 63 | * @returns `this`. 64 | * 65 | * @link https://brandi.js.org/reference/container#extendcontainer 66 | */ 67 | public extend(container: Container | null): this { 68 | this.vault.parent = container === null ? null : container.vault; 69 | return this; 70 | } 71 | 72 | /** 73 | * @description 74 | * Creates an unlinked clone of the container. 75 | * 76 | * @returns `Container`. 77 | * 78 | * @link https://brandi.js.org/reference/container#clone 79 | */ 80 | public clone(): Container { 81 | const container = new Container(); 82 | container.vault = this.vault.clone(); 83 | return container; 84 | } 85 | 86 | /** 87 | * @description 88 | * Gets a dependency bound to the token. 89 | * 90 | * @param token - token for which a dependence will be got. 91 | * @returns `TokenType`. 92 | * 93 | * @link https://brandi.js.org/reference/container#gettoken 94 | */ 95 | public get(token: T): TokenType; 96 | 97 | /** 98 | * @access package 99 | * @deprecated 100 | * `conditions` argument is added for internal use. 101 | * Use it if you really understand that it is necessary. 102 | */ 103 | public get( 104 | token: T, 105 | conditions: ResolutionCondition[], 106 | ): TokenType; 107 | 108 | public get( 109 | token: T, 110 | conditions?: ResolutionCondition[], 111 | ): TokenType { 112 | return this.resolveToken(token, conditions) as TokenType; 113 | } 114 | 115 | private resolveTokens( 116 | tokens: TokenValue[], 117 | cache: ResolutionCache, 118 | conditions?: ResolutionCondition[], 119 | target?: UnknownCreator, 120 | ): unknown[] { 121 | return tokens.map((token) => 122 | this.resolveToken(token, conditions, target, cache.split()), 123 | ); 124 | } 125 | 126 | private resolveToken( 127 | token: TokenValue, 128 | conditions?: ResolutionCondition[], 129 | target?: UnknownCreator, 130 | cache: ResolutionCache = new ResolutionCache(), 131 | ): unknown { 132 | const binding = this.vault.get(token, cache, conditions, target); 133 | 134 | if (binding) return this.resolveBinding(binding, cache); 135 | if (token.__o) return undefined; 136 | 137 | throw new Error(`No matching bindings found for '${token.__d}' token.`); 138 | } 139 | 140 | private resolveBinding(binding: Binding, cache: ResolutionCache): unknown { 141 | if (isInstanceBinding(binding)) { 142 | if (isInstanceSingletonScopedBinding(binding)) { 143 | return this.resolveCache( 144 | binding, 145 | cache, 146 | () => binding.cache, 147 | (instance) => { 148 | // eslint-disable-next-line no-param-reassign 149 | binding.cache = instance; 150 | }, 151 | ); 152 | } 153 | 154 | if (isInstanceContainerScopedBinding(binding)) { 155 | return this.resolveCache( 156 | binding, 157 | cache, 158 | () => binding.cache.get(this.vault), 159 | (instance) => { 160 | binding.cache.set(this.vault, instance); 161 | }, 162 | ); 163 | } 164 | 165 | if (isInstanceResolutionScopedBinding(binding)) { 166 | return this.resolveCache( 167 | binding, 168 | cache, 169 | () => cache.instances.get(binding), 170 | (instance) => { 171 | cache.instances.set(binding, instance); 172 | }, 173 | ); 174 | } 175 | 176 | return this.createInstance(binding.impl, cache); 177 | } 178 | 179 | if (isFactoryBinding(binding)) { 180 | return (...args: unknown[]) => { 181 | const instance = this.createInstance(binding.impl.creator, cache); 182 | 183 | return instance instanceof Promise 184 | ? instance.then((i) => 185 | Container.resolveInitialization( 186 | i, 187 | args, 188 | binding.impl.initializer, 189 | ), 190 | ) 191 | : Container.resolveInitialization( 192 | instance, 193 | args, 194 | binding.impl.initializer, 195 | ); 196 | }; 197 | } 198 | 199 | return binding.impl; 200 | } 201 | 202 | private resolveCache( 203 | binding: InstanceBinding, 204 | cache: ResolutionCache, 205 | getCache: () => unknown, 206 | setCache: (instance: unknown) => void, 207 | ) { 208 | const instanceCache = getCache(); 209 | 210 | if (instanceCache !== undefined) return instanceCache; 211 | 212 | const instance = this.createInstance(binding.impl, cache); 213 | setCache(instance); 214 | return instance; 215 | } 216 | 217 | private createInstance( 218 | creator: UnknownCreator, 219 | cache: ResolutionCache, 220 | ): unknown { 221 | const parameters = this.getParameters(creator, cache); 222 | const isCallable = callableRegistry.get(creator); 223 | 224 | if (isCallable !== undefined) { 225 | return isCallable 226 | ? // @ts-expect-error: This expression is not callable. 227 | creator(...parameters) 228 | : // @ts-expect-error: This expression is not constructable. 229 | // eslint-disable-next-line new-cap 230 | new creator(...parameters); 231 | } 232 | 233 | try { 234 | // @ts-expect-error: This expression is not callable. 235 | const instance = creator(...parameters); 236 | callableRegistry.set(creator, true); 237 | return instance; 238 | } catch { 239 | // @ts-expect-error: This expression is not constructable. 240 | // eslint-disable-next-line new-cap 241 | const instance = new creator(...parameters); 242 | callableRegistry.set(creator, false); 243 | return instance; 244 | } 245 | } 246 | 247 | private getParameters( 248 | target: UnknownCreator, 249 | cache: ResolutionCache, 250 | ): unknown[] { 251 | const injects = injectsRegistry.get(target); 252 | 253 | if (injects) 254 | return this.resolveTokens( 255 | injects, 256 | cache, 257 | tagsRegistry.get(target), 258 | target, 259 | ); 260 | 261 | if (target.length === 0) return []; 262 | 263 | throw new Error( 264 | `Missing required 'injected' registration of '${target.name}'`, 265 | ); 266 | } 267 | 268 | private static resolveInitialization( 269 | instance: T, 270 | args: unknown[], 271 | initializer?: FactoryInitializer, 272 | ) { 273 | const initialization = initializer?.(instance, ...args); 274 | return initialization instanceof Promise 275 | ? initialization.then(() => instance) 276 | : instance; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /packages/brandi/src/container/DependencyModule.ts: -------------------------------------------------------------------------------- 1 | import { BindingsVault } from './BindingsVault'; 2 | import { WhenSyntax } from './syntax'; 3 | 4 | export class DependencyModule extends WhenSyntax { 5 | constructor() { 6 | super(new BindingsVault()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/brandi/src/container/ResolutionCache.ts: -------------------------------------------------------------------------------- 1 | import { Binding } from './bindings'; 2 | import type { BindingsVault } from './BindingsVault'; 3 | 4 | export class ResolutionCache { 5 | constructor( 6 | public readonly instances = new Map(), 7 | public readonly vaults: BindingsVault[] = [], 8 | ) {} 9 | 10 | public split(): ResolutionCache { 11 | return new ResolutionCache(this.instances, this.vaults.slice()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/brandi/src/container/bindings/Binding.ts: -------------------------------------------------------------------------------- 1 | export enum Type { 2 | Constant, 3 | Instance, 4 | Factory, 5 | } 6 | 7 | export enum Scope { 8 | Container, 9 | Resolution, 10 | Singleton, 11 | Transient, 12 | } 13 | 14 | export interface Binding { 15 | readonly impl: unknown; 16 | readonly type: Type; 17 | 18 | clone?: () => Binding; 19 | } 20 | -------------------------------------------------------------------------------- /packages/brandi/src/container/bindings/ConstantBinding.ts: -------------------------------------------------------------------------------- 1 | import { Binding, Type } from './Binding'; 2 | 3 | export class ConstantBinding implements Binding { 4 | public readonly type = Type.Constant; 5 | 6 | constructor(public readonly impl: unknown) {} 7 | } 8 | -------------------------------------------------------------------------------- /packages/brandi/src/container/bindings/FactoryBinding.ts: -------------------------------------------------------------------------------- 1 | import { UnknownCreator } from '../../types'; 2 | 3 | import { Binding, Type } from './Binding'; 4 | 5 | export type FactoryInitializer = ( 6 | instance: unknown, 7 | ...args: unknown[] 8 | ) => unknown; 9 | 10 | export class FactoryBinding implements Binding { 11 | public readonly type = Type.Factory; 12 | 13 | constructor( 14 | public readonly impl: { 15 | creator: UnknownCreator; 16 | initializer?: FactoryInitializer; 17 | }, 18 | ) {} 19 | } 20 | 21 | export const isFactoryBinding = (binding: Binding): binding is FactoryBinding => 22 | binding.type === Type.Factory; 23 | -------------------------------------------------------------------------------- /packages/brandi/src/container/bindings/InstanceBinding.ts: -------------------------------------------------------------------------------- 1 | import { UnknownCreator } from '../../types'; 2 | 3 | import type { BindingsVault } from '../BindingsVault'; 4 | 5 | import { Binding, Scope, Type } from './Binding'; 6 | 7 | export abstract class InstanceBinding implements Binding { 8 | public readonly type = Type.Instance; 9 | 10 | public abstract readonly scope: Scope; 11 | 12 | constructor(public readonly impl: UnknownCreator) {} 13 | } 14 | 15 | export class InstanceContainerScopedBinding extends InstanceBinding { 16 | public readonly scope = Scope.Container; 17 | 18 | public readonly cache = new WeakMap(); 19 | } 20 | 21 | export class InstanceResolutionScopedBinding extends InstanceBinding { 22 | public readonly scope = Scope.Resolution; 23 | } 24 | 25 | export class InstanceSingletonScopedBinding extends InstanceBinding { 26 | public readonly scope = Scope.Singleton; 27 | 28 | public cache?: unknown; 29 | 30 | public clone?(): InstanceSingletonScopedBinding; 31 | 32 | constructor(public readonly impl: UnknownCreator) { 33 | super(impl); 34 | 35 | if (process.env.NODE_ENV !== 'production') { 36 | this.clone = (): InstanceSingletonScopedBinding => { 37 | const binding = new InstanceSingletonScopedBinding(this.impl); 38 | binding.cache = this.cache; 39 | return binding; 40 | }; 41 | } 42 | } 43 | } 44 | 45 | export class InstanceTransientScopedBinding extends InstanceBinding { 46 | public readonly scope = Scope.Transient; 47 | } 48 | 49 | export const isInstanceBinding = ( 50 | binding: Binding, 51 | ): binding is InstanceBinding => binding.type === Type.Instance; 52 | 53 | export const isInstanceContainerScopedBinding = ( 54 | binding: InstanceBinding, 55 | ): binding is InstanceContainerScopedBinding => 56 | binding.scope === Scope.Container; 57 | 58 | export const isInstanceResolutionScopedBinding = ( 59 | binding: InstanceBinding, 60 | ): binding is InstanceResolutionScopedBinding => 61 | binding.scope === Scope.Resolution; 62 | 63 | export const isInstanceSingletonScopedBinding = ( 64 | binding: InstanceBinding, 65 | ): binding is InstanceSingletonScopedBinding => 66 | binding.scope === Scope.Singleton; 67 | -------------------------------------------------------------------------------- /packages/brandi/src/container/bindings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Binding'; 2 | export * from './ConstantBinding'; 3 | export * from './FactoryBinding'; 4 | export * from './InstanceBinding'; 5 | -------------------------------------------------------------------------------- /packages/brandi/src/container/createContainer.ts: -------------------------------------------------------------------------------- 1 | import { Container } from './Container'; 2 | 3 | /** 4 | * @description 5 | * Alias for `new Container()`. 6 | * 7 | * @example 8 | * Example usage of `createContainer()`. 9 | * 10 | * const container = createContainer(); 11 | * console.log(container instanceof Container); // → true 12 | * 13 | * @link https://brandi.js.org/reference/container#createcontainer 14 | */ 15 | export const createContainer = () => new Container(); 16 | -------------------------------------------------------------------------------- /packages/brandi/src/container/createDependencyModule.ts: -------------------------------------------------------------------------------- 1 | import { DependencyModule } from './DependencyModule'; 2 | 3 | /** 4 | * @description 5 | * Alias for `new DependencyModule()`. 6 | * 7 | * @example 8 | * Example usage of `createDependencyModule()`. 9 | * 10 | * const dependencyModule = createDependencyModule(); 11 | * console.log(dependencyModule instanceof DependencyModule); // → true 12 | * 13 | * @link https://brandi.js.org/reference/dependency-modules#createdependencymodule 14 | */ 15 | export const createDependencyModule = () => new DependencyModule(); 16 | -------------------------------------------------------------------------------- /packages/brandi/src/container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Container'; 2 | export * from './DependencyModule'; 3 | export * from './createContainer'; 4 | export * from './createDependencyModule'; 5 | -------------------------------------------------------------------------------- /packages/brandi/src/container/syntax/BindOrUseSyntax.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenType } from '../../pointers'; 2 | import { ResolutionCondition } from '../../types'; 3 | 4 | import { BindingsVault } from '../BindingsVault'; 5 | 6 | import { FromSyntax } from './FromSyntax'; 7 | import { TypeSyntax } from './TypeSyntax'; 8 | 9 | export class BindOrUseSyntax { 10 | protected static vault(target: BindOrUseSyntax) { 11 | return target.vault; 12 | } 13 | 14 | constructor( 15 | protected vault: BindingsVault, 16 | private readonly condition?: ResolutionCondition, 17 | ) {} 18 | 19 | /** 20 | * @description 21 | * Binds the token to an implementation. 22 | * 23 | * @param token - a token to be bound. 24 | * 25 | * @returns 26 | * Binding Type syntax: 27 | * - `toConstant(value)` 28 | * - `toInstance(creator)` 29 | * - `toFactory(creator, [initializer])` 30 | * 31 | * @link https://brandi.js.org/reference/container#bindtoken 32 | */ 33 | public bind(token: T): TypeSyntax> { 34 | return new TypeSyntax>(this.vault, token, this.condition); 35 | } 36 | 37 | /** 38 | * @description 39 | * Uses bindings from a dependency module. 40 | * 41 | * @param tokens - tokens to be used from a dependency module. 42 | * @returns `.from(module)` syntax. 43 | * 44 | * @link https://brandi.js.org/reference/container#usetokensfrommodule 45 | */ 46 | public use(...tokens: Token[]): FromSyntax { 47 | return new FromSyntax( 48 | this.vault, 49 | tokens, 50 | BindOrUseSyntax.vault, 51 | this.condition, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/brandi/src/container/syntax/FromSyntax.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionCondition } from '../../types'; 2 | import { Token } from '../../pointers'; 3 | 4 | import { BindingsVault } from '../BindingsVault'; 5 | import type { DependencyModule } from '../DependencyModule'; 6 | 7 | import type { BindOrUseSyntax } from './BindOrUseSyntax'; 8 | 9 | export class FromSyntax { 10 | constructor( 11 | private readonly vault: BindingsVault, 12 | private readonly tokens: Token[], 13 | private readonly getVault: (target: BindOrUseSyntax) => BindingsVault, 14 | private readonly condition?: ResolutionCondition, 15 | ) {} 16 | 17 | /** 18 | * @description 19 | * Uses bindings from the dependency module. 20 | * 21 | * @param dependencyModule - the dependency module from which the tokens will be used. 22 | * 23 | * @link https://brandi.js.org/reference/container#usetokensfrommodule 24 | */ 25 | public from(dependencyModule: DependencyModule): void { 26 | const { tokens } = this; 27 | for (let i = 0, len = tokens.length; i < len; i += 1) { 28 | this.vault.set( 29 | this.getVault(dependencyModule), 30 | tokens[i]!, 31 | this.condition, 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/brandi/src/container/syntax/ScopeSyntax.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionCondition, UnknownCreator } from '../../types'; 2 | import { Token } from '../../pointers'; 3 | 4 | import { 5 | InstanceContainerScopedBinding, 6 | InstanceResolutionScopedBinding, 7 | InstanceSingletonScopedBinding, 8 | InstanceTransientScopedBinding, 9 | } from '../bindings'; 10 | import { BindingsVault } from '../BindingsVault'; 11 | 12 | export class ScopeSyntax { 13 | private readonly warningTimeout?: NodeJS.Timeout; 14 | 15 | constructor( 16 | private readonly vault: BindingsVault, 17 | private readonly impl: UnknownCreator, 18 | private readonly token: Token, 19 | private readonly condition?: ResolutionCondition, 20 | ) { 21 | if (process.env.NODE_ENV !== 'production') { 22 | this.warningTimeout = setTimeout(() => { 23 | console.warn( 24 | `Warning: did you forget to set a scope for '${this.token.__d}' token binding? ` + 25 | "Call 'inTransientScope()', 'inSingletonScope()', 'inContainerScope()' or 'inResolutionScope()'.", 26 | ); 27 | }); 28 | } 29 | } 30 | 31 | /** 32 | * @description 33 | * The container will return the same instance with each getting. 34 | * This is similar to being a singleton, however if the container has a child container or a clone, 35 | * that child container or clone will get an instance unique to it. 36 | * 37 | * @link https://brandi.js.org/reference/binding-scopes#incontainerscope 38 | */ 39 | public inContainerScope(): void { 40 | this.set(InstanceContainerScopedBinding); 41 | } 42 | 43 | /** 44 | * @description 45 | * The same instance will be got for each getting of this dependency during a single resolution chain. 46 | * 47 | * @link https://brandi.js.org/reference/binding-scopes#inresolutionscope 48 | */ 49 | public inResolutionScope(): void { 50 | this.set(InstanceResolutionScopedBinding); 51 | } 52 | 53 | /** 54 | * @description 55 | * Each getting will return the same instance. 56 | * 57 | * @link https://brandi.js.org/reference/binding-scopes#insingletonscope 58 | */ 59 | public inSingletonScope(): void { 60 | this.set(InstanceSingletonScopedBinding); 61 | } 62 | 63 | /** 64 | * @description 65 | * New instance will be created with each getting. 66 | * 67 | * @link https://brandi.js.org/reference/binding-scopes#intransientscope 68 | */ 69 | public inTransientScope(): void { 70 | this.set(InstanceTransientScopedBinding); 71 | } 72 | 73 | private set( 74 | Ctor: 75 | | typeof InstanceContainerScopedBinding 76 | | typeof InstanceResolutionScopedBinding 77 | | typeof InstanceSingletonScopedBinding 78 | | typeof InstanceTransientScopedBinding, 79 | ): void { 80 | if (process.env.NODE_ENV !== 'production') 81 | clearTimeout(this.warningTimeout!); 82 | 83 | this.vault.set(new Ctor(this.impl), this.token, this.condition); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/brandi/src/container/syntax/TypeSyntax.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncFactory, 3 | Factory, 4 | ResolutionCondition, 5 | UnknownCreator, 6 | } from '../../types'; 7 | import { Token } from '../../pointers'; 8 | 9 | import { 10 | ConstantBinding, 11 | FactoryBinding, 12 | FactoryInitializer, 13 | } from '../bindings'; 14 | import { BindingsVault } from '../BindingsVault'; 15 | 16 | import { ScopeSyntax } from './ScopeSyntax'; 17 | 18 | export class TypeSyntax { 19 | constructor( 20 | private readonly vault: BindingsVault, 21 | private readonly token: Token, 22 | private readonly condition?: ResolutionCondition, 23 | ) {} 24 | 25 | /** 26 | * @description 27 | * Binds the token to the constant value. 28 | * 29 | * @param value - the value that will be bound to the token. 30 | * 31 | * @link https://brandi.js.org/reference/binding-types#toconstantvalue 32 | */ 33 | public toConstant(value: Dependency): void { 34 | this.vault.set(new ConstantBinding(value), this.token, this.condition); 35 | } 36 | 37 | /** 38 | * @description 39 | * Binds the token to the factory. 40 | * 41 | * @param creator - the instance creator which the factory will use; 42 | * @param [initializer] - optional function called after the instance is created. 43 | * 44 | * @link https://brandi.js.org/reference/binding-types#tofactorycreator-initializer 45 | */ 46 | public toFactory( 47 | creator: Dependency extends AsyncFactory 48 | ? UnknownCreator> 49 | : never, 50 | initializer?: Dependency extends AsyncFactory< 51 | infer Instance, 52 | infer Arguments 53 | > 54 | ? (instance: Instance, ...args: Arguments) => unknown 55 | : never, 56 | ): void; 57 | 58 | /** 59 | * @description 60 | * Binds the token to the factory. 61 | * 62 | * @param creator - the instance creator which the factory will use; 63 | * @param [initializer] - optional function called after the instance is created. 64 | * 65 | * @link https://brandi.js.org/reference/binding-types#tofactorycreator-initializer 66 | */ 67 | public toFactory( 68 | creator: Dependency extends AsyncFactory 69 | ? UnknownCreator 70 | : never, 71 | initializer: Dependency extends AsyncFactory< 72 | infer Instance, 73 | infer Arguments 74 | > 75 | ? (instance: Instance, ...args: Arguments) => Promise 76 | : never, 77 | ): void; 78 | 79 | /** 80 | * @description 81 | * Binds the token to the factory. 82 | * 83 | * @param creator - the instance creator which the factory will use; 84 | * @param [initializer] - optional function called after the instance is created. 85 | * 86 | * @link https://brandi.js.org/reference/binding-types#tofactorycreator-initializer 87 | */ 88 | public toFactory( 89 | creator: Dependency extends Factory 90 | ? Instance extends Promise 91 | ? never 92 | : UnknownCreator 93 | : never, 94 | initializer?: Dependency extends Factory 95 | ? InitializerReturnType extends Promise 96 | ? never 97 | : (instance: Instance, ...args: Arguments) => InitializerReturnType 98 | : never, 99 | ): void; 100 | 101 | public toFactory( 102 | creator: UnknownCreator, 103 | initializer?: FactoryInitializer, 104 | ): void { 105 | this.vault.set( 106 | new FactoryBinding({ creator, initializer }), 107 | this.token, 108 | this.condition, 109 | ); 110 | } 111 | 112 | /** 113 | * @description 114 | * Binds the token to an instance in one of the scopes. 115 | * 116 | * @param creator - the instance creator that will be bound to the token. 117 | * 118 | * @returns 119 | * Scope syntax: 120 | * - `inSingletonScope()` 121 | * - `inTransientScope()` 122 | * - `inContainerScope()` 123 | * - `inResolutionScope()` 124 | * 125 | * @link https://brandi.js.org/reference/binding-types#toinstancecreator 126 | */ 127 | public toInstance(creator: UnknownCreator): ScopeSyntax { 128 | return new ScopeSyntax(this.vault, creator, this.token, this.condition); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /packages/brandi/src/container/syntax/WhenSyntax.ts: -------------------------------------------------------------------------------- 1 | import { ResolutionCondition } from '../../types'; 2 | 3 | import { BindOrUseSyntax } from './BindOrUseSyntax'; 4 | 5 | export class WhenSyntax extends BindOrUseSyntax { 6 | /** 7 | * @description 8 | * Creates a conditional binding. 9 | * 10 | * @param condition - a condition. 11 | * @returns `bind` or `use` syntax. 12 | * 13 | * @link https://brandi.js.org/reference/conditional-bindings 14 | */ 15 | public when(condition: ResolutionCondition): BindOrUseSyntax { 16 | return new BindOrUseSyntax(this.vault, condition); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/brandi/src/container/syntax/index.ts: -------------------------------------------------------------------------------- 1 | export * from './WhenSyntax'; 2 | -------------------------------------------------------------------------------- /packages/brandi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Container, 3 | DependencyModule, 4 | createContainer, 5 | createDependencyModule, 6 | } from './container'; 7 | export type { 8 | Tag, 9 | Token, 10 | TokenType, 11 | TokenTypeMap, 12 | TokenValue, 13 | RequiredToken, 14 | OptionalToken, 15 | } from './pointers'; 16 | export { tag, token } from './pointers'; 17 | export { injected, tagged } from './registrators'; 18 | export type { AsyncFactory, Factory, ResolutionCondition } from './types'; 19 | -------------------------------------------------------------------------------- /packages/brandi/src/pointers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tag'; 2 | export * from './token'; 3 | -------------------------------------------------------------------------------- /packages/brandi/src/pointers/tag.ts: -------------------------------------------------------------------------------- 1 | export type Tag = symbol & { __tag__: true }; 2 | 3 | /** 4 | * @description 5 | * Creates a unique tag. 6 | * 7 | * @param {string} description - a description of the tag to be used in logs and error messages. 8 | * @returns `Tag`. 9 | * 10 | * @link https://brandi.js.org/reference/pointers-and-registrators#tagdescription 11 | */ 12 | export const tag = (description: string): Tag => Symbol(description) as Tag; 13 | -------------------------------------------------------------------------------- /packages/brandi/src/pointers/token.ts: -------------------------------------------------------------------------------- 1 | export interface TokenValue { 2 | /** 3 | * @description Token type. 4 | */ 5 | __t: T; 6 | 7 | /** 8 | * @description Description of token. 9 | */ 10 | __d: string; 11 | 12 | /** 13 | * @description Unique symbol. 14 | */ 15 | 16 | __s: symbol; 17 | 18 | /** 19 | * @description Is binding by token optional. 20 | */ 21 | __o: boolean; 22 | } 23 | 24 | export interface RequiredToken extends TokenValue { 25 | __o: false; 26 | } 27 | 28 | export interface OptionalToken extends TokenValue { 29 | __o: true; 30 | } 31 | 32 | export interface Token extends RequiredToken { 33 | optional: OptionalToken; 34 | } 35 | 36 | export type TokenType = T extends RequiredToken 37 | ? T['__t'] 38 | : T['__t'] | undefined; 39 | 40 | export type TokenTypeMap = { 41 | [K in keyof T]: T[K] extends Token ? TokenType : TokenTypeMap; 42 | }; 43 | 44 | export type ToToken = undefined extends T 45 | ? OptionalToken> 46 | : RequiredToken; 47 | 48 | /** 49 | * @description 50 | * Creates a unique token with the type. 51 | * 52 | * @param {string} description - a description of the token to be used in logs and error messages. 53 | * @returns a unique `Token` token with the type. 54 | * 55 | * @link https://brandi.js.org/reference/pointers-and-registrators#tokentdescription 56 | */ 57 | export const token = (description: string): Token => { 58 | const s = Symbol(description); 59 | return { 60 | __t: (null as unknown) as T, 61 | __d: description, 62 | __s: s, 63 | __o: false, 64 | optional: { 65 | __t: (null as unknown) as T, 66 | __d: description, 67 | __s: s, 68 | __o: true, 69 | }, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /packages/brandi/src/registrators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './injected'; 2 | export * from './tagged'; 3 | -------------------------------------------------------------------------------- /packages/brandi/src/registrators/injected.ts: -------------------------------------------------------------------------------- 1 | import { ToToken, TokenValue } from '../pointers'; 2 | import { UnknownCreator, UnknownCreatorParameters } from '../types'; 3 | import { injectsRegistry } from '../registries'; 4 | 5 | type ToTokens = { 6 | [K in keyof T]-?: ToToken; 7 | }; 8 | 9 | /** 10 | * @description 11 | * Registers target injections. 12 | * 13 | * @param target - constructor or function whose dependencies will be injected. 14 | * @param ...tokens - dependency tokens. 15 | * @returns the `target` first argument. 16 | * 17 | * @link https://brandi.js.org/reference/pointers-and-registrators#injectedtarget-tokens 18 | */ 19 | export const injected = ( 20 | target: T, 21 | ...tokens: ToTokens> extends TokenValue[] 22 | ? ToTokens> 23 | : never 24 | ) => { 25 | injectsRegistry.set(target, tokens); 26 | return target; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/brandi/src/registrators/tagged.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from '../pointers'; 2 | import { UnknownCreator } from '../types'; 3 | import { tagsRegistry } from '../registries'; 4 | 5 | /** 6 | * @description 7 | * Tags target. 8 | * 9 | * @param target - constructor or function that will be tagged. 10 | * @param ...tags - tags. 11 | * @returns the `target` first argument. 12 | * 13 | * @link https://brandi.js.org/reference/conditional-bindings 14 | */ 15 | export const tagged = ( 16 | target: T, 17 | ...tags: Tag[] 18 | ): T => { 19 | tagsRegistry.set(target, tags); 20 | return target; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/brandi/src/registries/callableRegistry.ts: -------------------------------------------------------------------------------- 1 | import { UnknownCreator } from '../types'; 2 | 3 | export const callableRegistry = new WeakMap(); 4 | -------------------------------------------------------------------------------- /packages/brandi/src/registries/index.ts: -------------------------------------------------------------------------------- 1 | export * from './callableRegistry'; 2 | export * from './injectsRegistry'; 3 | export * from './tagsRegistry'; 4 | -------------------------------------------------------------------------------- /packages/brandi/src/registries/injectsRegistry.ts: -------------------------------------------------------------------------------- 1 | import { TokenValue } from '../pointers'; 2 | import { UnknownCreator } from '../types'; 3 | 4 | export const injectsRegistry = new Map(); 5 | -------------------------------------------------------------------------------- /packages/brandi/src/registries/tagsRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from '../pointers'; 2 | import { UnknownCreator } from '../types'; 3 | 4 | export const tagsRegistry = new Map(); 5 | -------------------------------------------------------------------------------- /packages/brandi/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from './pointers'; 2 | 3 | export type ResolutionCondition = Tag | UnknownCreator; 4 | 5 | type UnknownConstructor = new ( 6 | ...args: never[] 7 | ) => T; 8 | 9 | type UnknownFunction = (...args: never[]) => T; 10 | 11 | export type UnknownCreator = 12 | | UnknownConstructor 13 | | UnknownFunction; 14 | 15 | export type UnknownCreatorParameters< 16 | T extends UnknownCreator 17 | > = T extends UnknownConstructor 18 | ? ConstructorParameters 19 | : T extends UnknownFunction 20 | ? Parameters 21 | : never; 22 | 23 | export type Factory = ( 24 | ...args: A 25 | ) => T; 26 | 27 | export type AsyncFactory = Factory< 28 | Promise, 29 | A 30 | >; 31 | -------------------------------------------------------------------------------- /packages/brandi/tsconfig.typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "removeComments": false, 5 | "declaration": true, 6 | "declarationDir": "./lib/typings", 7 | "emitDeclarationOnly": true 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "isolatedModules": true, 10 | "jsx": "react", 11 | 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUncheckedIndexedAccess": true, 18 | "forceConsistentCasingInFileNames": true 19 | }, 20 | "include": ["**/*", "**/.*.js"] 21 | } 22 | -------------------------------------------------------------------------------- /website/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .docusaurus 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .docusaurus 3 | .cache-loader 4 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .docusaurus 4 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | const codeTheme = require('./src/js/codeTheme.js'); 2 | 3 | /** @type {import('@docusaurus/types').DocusaurusConfig} */ 4 | module.exports = { 5 | title: 'Brandi', 6 | tagline: 'The dependency injection container powered by TypeScript.', 7 | url: 'https://your-docusaurus-test-site.com', 8 | baseUrl: '/', 9 | onBrokenLinks: 'throw', 10 | onBrokenMarkdownLinks: 'warn', 11 | favicon: 'images/favicon.ico', 12 | organizationName: 'brandi', 13 | projectName: 'brandi', 14 | themeConfig: { 15 | prism: { 16 | theme: codeTheme, 17 | }, 18 | navbar: { 19 | title: 'Brandi', 20 | logo: { 21 | alt: 'Brandi Logo', 22 | src: 'images/brandi.svg', 23 | }, 24 | items: [ 25 | { 26 | label: 'Getting Started', 27 | to: 'getting-started', 28 | activeBasePath: 'getting-started', 29 | }, 30 | { 31 | label: 'Reference', 32 | to: 'reference', 33 | activeBasePath: 'reference', 34 | }, 35 | { 36 | label: 'Brandi-React', 37 | to: 'brandi-react', 38 | activeBasePath: 'brandi-react', 39 | }, 40 | { 41 | label: 'Examples', 42 | to: 'examples', 43 | activeBasePath: 'examples', 44 | }, 45 | { 46 | label: 'GitHub', 47 | href: 'https://github.com/vovaspace/brandi/', 48 | position: 'right', 49 | }, 50 | ], 51 | }, 52 | footer: { 53 | style: 'light', 54 | links: [ 55 | { 56 | title: 'Docs', 57 | items: [ 58 | { 59 | label: 'Getting Started', 60 | to: 'getting-started', 61 | }, 62 | { 63 | label: 'Reference', 64 | to: 'reference', 65 | }, 66 | { 67 | label: 'Brandi-React', 68 | to: 'brandi-react', 69 | }, 70 | { 71 | label: 'Examples', 72 | to: 'examples', 73 | }, 74 | ], 75 | }, 76 | { 77 | title: 'More', 78 | items: [ 79 | { 80 | label: 'GitHub', 81 | href: 'https://github.com/vovaspace/brandi/', 82 | }, 83 | ], 84 | }, 85 | ], 86 | logo: { 87 | alt: 'Brandi Logo', 88 | src: 'images/brandi.svg', 89 | href: '/', 90 | }, 91 | copyright: `Copyright © ${new Date().getFullYear()} Vladimir Lewandowski. Built with Docusaurus.`, 92 | }, 93 | colorMode: { 94 | respectPrefersColorScheme: true, 95 | }, 96 | algolia: { 97 | appId: 'ZJ99TQS6KK', 98 | apiKey: '4ca1bea483527bfd468f79bf940899cb', 99 | indexName: 'brandi-js', 100 | }, 101 | }, 102 | presets: [ 103 | [ 104 | '@docusaurus/preset-classic', 105 | { 106 | docs: { 107 | path: '../docs', 108 | routeBasePath: '/', 109 | sidebarPath: require.resolve('./sidebars.js'), 110 | editUrl: 'https://github.com/vovaspace/brandi/edit/main/docs/', 111 | }, 112 | theme: { 113 | customCss: require.resolve('./src/css/custom.css'), 114 | }, 115 | }, 116 | ], 117 | ], 118 | }; 119 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brandi-website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "translations:write": "docusaurus write-translations", 14 | "heading-ids:write": "docusaurus write-heading-ids", 15 | "typecheck": "tsc --noEmit", 16 | "code:lint": "prettier --check . && eslint .", 17 | "code:fix": "prettier --write . && eslint --fix ." 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.5%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "dependencies": { 32 | "@docusaurus/core": "2.3.1", 33 | "@docusaurus/preset-classic": "2.3.1", 34 | "@mdx-js/react": "^1.6.21", 35 | "react": "^17.0.1", 36 | "react-dom": "^17.0.1" 37 | }, 38 | "devDependencies": { 39 | "@docusaurus/module-type-aliases": "^2.3.1", 40 | "@types/react": "^17.0.1", 41 | "@types/react-helmet": "^6.1.0", 42 | "@types/react-router-dom": "^5.1.7" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sidebar: { 3 | 'Getting Started': [ 4 | 'getting-started/overview', 5 | 'getting-started/installation', 6 | ], 7 | 'Reference': [ 8 | 'reference/api-reference', 9 | 'reference/pointers-and-registrators', 10 | 'reference/container', 11 | 'reference/binding-types', 12 | 'reference/binding-scopes', 13 | 'reference/optional-dependencies', 14 | 'reference/dependency-modules', 15 | 'reference/hierarchical-containers', 16 | 'reference/conditional-bindings', 17 | ], 18 | 'Brandi-React': [ 19 | 'brandi-react/overview', 20 | 'brandi-react/container-provider', 21 | 'brandi-react/use-injection', 22 | 'brandi-react/create-injection-hooks', 23 | 'brandi-react/tagged', 24 | ], 25 | 'Examples': ['examples/basic-examples'], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ifm-color-primary: #34aae5; 3 | --ifm-color-primary-dark: #1d9fe0; 4 | --ifm-color-primary-darker: #1b96d4; 5 | --ifm-color-primary-darkest: #167cae; 6 | --ifm-color-primary-light: #4db4e8; 7 | --ifm-color-primary-lighter: #59baea; 8 | --ifm-color-primary-lightest: #7fc9ef; 9 | } 10 | 11 | .navbar__logo { 12 | margin-top: -0.12rem; 13 | } 14 | 15 | .navbar__toggle { 16 | margin-right: 1rem; 17 | } 18 | 19 | .footer__links { 20 | margin-bottom: 2rem; 21 | } 22 | 23 | .footer__logo { 24 | max-width: 3.2rem; 25 | } 26 | 27 | .footer__copyright { 28 | margin-top: 0.8rem; 29 | } 30 | -------------------------------------------------------------------------------- /website/src/js/codeTheme.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plain: { 3 | color: '#f8f8f2', 4 | backgroundColor: '#272822', 5 | }, 6 | styles: [ 7 | { 8 | types: ['comment', 'prolog', 'doctype', 'cdata'], 9 | style: { 10 | color: '#c6cad2', 11 | }, 12 | }, 13 | { 14 | types: ['punctuation'], 15 | style: { 16 | color: '#F8F8F2', 17 | }, 18 | }, 19 | { 20 | types: ['property', 'tag', 'constant', 'symbol', 'deleted'], 21 | style: { 22 | color: '#F92672', 23 | }, 24 | }, 25 | { 26 | types: ['boolean', 'number'], 27 | style: { 28 | color: '#AE81FF', 29 | }, 30 | }, 31 | { 32 | types: ['selector', 'attr-name', 'string', 'char', 'builtin', 'inserted'], 33 | style: { 34 | color: '#a6e22e', 35 | }, 36 | }, 37 | { 38 | types: ['operator', 'entity', 'url', 'variable'], 39 | style: { 40 | color: '#F8F8F2', 41 | }, 42 | }, 43 | { 44 | types: ['atrule', 'attr-value', 'function'], 45 | style: { 46 | color: '#E6D874', 47 | }, 48 | }, 49 | { 50 | types: ['keyword'], 51 | style: { 52 | color: '#F92672', 53 | }, 54 | }, 55 | { 56 | types: ['regex', 'important'], 57 | style: { 58 | color: '#FD971F', 59 | }, 60 | }, 61 | ], 62 | }; 63 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | padding: 4rem 0; 3 | } 4 | 5 | @media screen and (max-width: 966px) { 6 | .hero { 7 | padding: 2rem 0; 8 | } 9 | } 10 | 11 | .features { 12 | display: flex; 13 | align-items: center; 14 | padding: 2rem 0; 15 | width: 100%; 16 | } 17 | 18 | .featureImage { 19 | height: 7.5rem; 20 | width: 7.5rem; 21 | } 22 | -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | import Layout from '@theme/Layout'; 3 | import Link from '@docusaurus/Link'; 4 | import useBaseUrl from '@docusaurus/useBaseUrl'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | 7 | import styles from './index.module.css'; 8 | 9 | const features = [ 10 | { 11 | id: 'framework-agnostic', 12 | title: 'Framework Agnostic', 13 | imageUrl: 'images/cubes.svg', 14 | description: 'Can work with any UI or server framework.', 15 | }, 16 | { 17 | id: 'lightweight-and-effective', 18 | title: 'Lightweight and Effective', 19 | imageUrl: 'images/lightning.svg', 20 | description: 'Brandi is tiny and designed for maximum performance.', 21 | }, 22 | { 23 | id: 'strongly-typed', 24 | title: 'Strongly Typed', 25 | imageUrl: 'images/weapon.svg', 26 | description: 'TypeScript support out of box.', 27 | }, 28 | { 29 | id: 'decorators-free', 30 | title: 'Decorators Free', 31 | imageUrl: 'images/ok.svg', 32 | description: ( 33 | <> 34 | Does not require additional parameters in tsconfig.json and{' '} 35 | Reflect polyfill. 36 | 37 | ), 38 | }, 39 | ]; 40 | 41 | const Feature: FC<{ 42 | imageUrl: string; 43 | title: string; 44 | description: ReactNode; 45 | }> = ({ imageUrl, title, description }) => { 46 | const imgUrl = useBaseUrl(imageUrl); 47 | 48 | return ( 49 |

50 | {imgUrl && ( 51 |
52 | {title} 53 |
54 | )} 55 |

{title}

56 |

{description}

57 |
58 | ); 59 | }; 60 | 61 | export default function Home() { 62 | const context = useDocusaurusContext(); 63 | const { siteConfig = {} } = context; 64 | 65 | return ( 66 | 70 |
71 |
72 |

{siteConfig.title}

73 |

{siteConfig.tagline}

74 | 78 | Get Started 79 | 80 |
81 |
82 |
83 | {features && features.length > 0 && ( 84 |
85 |
86 |
87 | {features.map((feature) => ( 88 | 94 | ))} 95 |
96 |
97 |
98 | )} 99 |
100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vovaspace/brandi/1da1dbe36a7981bea6ce2650dd090f6d6fd92731/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/images/brandi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/static/images/cubes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vovaspace/brandi/1da1dbe36a7981bea6ce2650dd090f6d6fd92731/website/static/images/favicon.ico -------------------------------------------------------------------------------- /website/static/images/lightning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/images/ok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/images/weapon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["@docusaurus/module-type-aliases"] 5 | }, 6 | "include": ["./src", "./*"] 7 | } 8 | --------------------------------------------------------------------------------