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