├── r.bat ├── bin └── cli ├── nodemon.json ├── jest.config.js ├── .npmignore ├── examples ├── nodejs │ ├── README.MD │ ├── package.json │ ├── src │ │ └── index.ts │ ├── package-lock.json │ └── tsconfig.json └── react │ ├── README.md │ ├── src │ ├── index.tsx │ ├── services │ │ └── toast.tsx │ └── App.tsx │ ├── .gitignore │ ├── public │ └── index.html │ ├── tsconfig.json │ └── package.json ├── .vscode ├── tasks.json └── launch.json ├── LICENSE ├── .gitignore ├── package.json ├── tsconfig.json ├── README.md └── src ├── test └── index.test.ts └── index.ts /r.bat: -------------------------------------------------------------------------------- 1 | npm run %1 -------------------------------------------------------------------------------- /bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../build/cli.js'); -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node ./src/cli.ts" 6 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: [ 5 | "build" 6 | ] 7 | }; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules/ 3 | .vscode/ 4 | .gitignore 5 | *.log 6 | r.bat 7 | test/ 8 | build/test/ 9 | todo.txt 10 | .nyc_output/ 11 | tsconfig.json 12 | tslint.json 13 | nodemon.json 14 | jest.config.js 15 | .env 16 | examples/ -------------------------------------------------------------------------------- /examples/nodejs/README.MD: -------------------------------------------------------------------------------- 1 | A simple example of using Fusion in TypeScript under Node.js. 2 | 3 | This example shows how a logging service can be automatically injected into another class. 4 | 5 | To run this: 6 | 7 | cd fusion/examples/nodejs 8 | npm install 9 | npm start -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | A simple example of using Fusion in TypeScript under a React web app. 2 | 3 | Created using create-react-app (and then simplified). 4 | 5 | This example shows how a toast service can be automatically injected into a React component. 6 | 7 | To run this: 8 | 9 | cd fusion/examples/react 10 | npm install 11 | npm start 12 | -------------------------------------------------------------------------------- /examples/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './services/toast'; // Make sure the Toast implementation is imported. 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "typescript", 8 | "tsconfig": "tsconfig.json", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./build/index.js", 6 | "scripts": { 7 | "start": "ts-node ./src/index.ts" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@types/node": "^14.0.13", 14 | "ts-node": "^8.10.2", 15 | "typescript": "^3.9.5" 16 | }, 17 | "dependencies": { 18 | "@codecapers/fusion": "^1.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/react/src/services/toast.tsx: -------------------------------------------------------------------------------- 1 | import { InjectableSingleton } from "@codecapers/fusion"; 2 | 3 | // 4 | // Interface to the toast service. 5 | // 6 | export interface IToast { 7 | success(msg: string): void; 8 | } 9 | 10 | // 11 | // This is a lazily injected singleton that's constructed just before it's injected. 12 | // 13 | @InjectableSingleton("IToast") 14 | export class Toast implements IToast { 15 | success(msg: string): void { 16 | alert(msg); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | React App 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "experimentalDecorators": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InjectProperty, InjectableClass } from "@codecapers/fusion"; 3 | import { IToast } from './services/toast'; 4 | 5 | @InjectableClass() 6 | class App extends React.Component { 7 | 8 | @InjectProperty("IToast") 9 | toast!: IToast; 10 | 11 | render() { 12 | return ( 13 |
14 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reacy", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@codecapers/fusion": "^1.0.0", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "@types/jest": "^24.9.1", 11 | "@types/node": "^12.12.47", 12 | "@types/react": "^16.9.36", 13 | "@types/react-dom": "^16.9.8", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-scripts": "3.4.1", 17 | "typescript": "^3.7.5" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ashley Davis 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build/ 61 | *.bak 62 | src/todo.txt 63 | examples/react/src/react-app-env.d.ts 64 | -------------------------------------------------------------------------------- /examples/nodejs/src/index.ts: -------------------------------------------------------------------------------- 1 | import { InjectProperty, InjectableClass, InjectableSingleton } from "@codecapers/fusion"; 2 | 3 | // 4 | // Creates a type safe reference to a dependency. 5 | // 6 | export function createDependencyRef(name: string): { T: T; id: string } { 7 | return { 8 | T: null!, 9 | id: name 10 | }; 11 | } 12 | // 13 | // Interface to the logging service. 14 | // 15 | interface ILog { 16 | info(msg: string): void; 17 | } 18 | 19 | // 20 | // Extend the ILog namespace to create the dependency reference. 21 | // 22 | namespace ILog { 23 | export const ref = createDependencyRef("ILog"); 24 | } 25 | 26 | // 27 | // This is a lazily injected singleton that's constructed just before it's injected. 28 | // 29 | @InjectableSingleton("ILog") 30 | class Log implements ILog { 31 | info(msg: string): void { 32 | console.log(msg); 33 | } 34 | } 35 | 36 | @InjectableClass() 37 | class MyClass { 38 | 39 | // 40 | // Injects the logging service into this property. 41 | // 42 | @InjectProperty(ILog.ref.id) 43 | log!: typeof ILog.ref.T; 44 | 45 | myFunction() { 46 | // 47 | // Use the injected logging service. 48 | // By the time we get to this code path the logging service has been automatically constructed and injected. 49 | // 50 | this.log.info("Hello world!"); 51 | } 52 | 53 | } 54 | 55 | const myObject = new MyClass(); // The logging singleton is lazily created at this point. 56 | myObject.myFunction(); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codecapers/fusion", 3 | "version": "1.0.15", 4 | "description": "A simple automated dependency injection library for TypeScript, supporting React class and functional compoents.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "c": "npm run clean", 9 | "clean": "rm -rf build/*", 10 | "b": "npm run build", 11 | "build": "tsc", 12 | "build-prod": "tsc --sourceMap false", 13 | "cb": "npm run clean-build", 14 | "clean-build": "npm run clean && npm run build", 15 | "bw": "npm run build:watch", 16 | "build:watch": "tsc --watch", 17 | "cbw": "npm run clean-build:watch", 18 | "clean-build:watch": "npm run clean-build && npm run build:watch", 19 | "prepublishOnly": "npm run clean && npm test && npm run build-prod", 20 | "t": "npm run test", 21 | "test": "jest", 22 | "tw": "npm run test:watch", 23 | "test:watch": "jest --watch" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://ashleydavis@github.com/ashleydavis/fusion.git" 28 | }, 29 | "keywords": [ 30 | "typescript", 31 | "dependency injection", 32 | "di" 33 | ], 34 | "author": "ashley@codecapers.com.au", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/ashleydavis/fusion/issues" 38 | }, 39 | "homepage": "https://github.com/ashleydavis/fusion#readme", 40 | "dependencies": {}, 41 | "devDependencies": { 42 | "@types/jest": "^26.0.0", 43 | "@types/node": "^14.0.13", 44 | "jest": "^26.0.1", 45 | "source-map-support": "0.5.19", 46 | "ts-jest": "^26.1.0", 47 | "ts-node": "8.10.2", 48 | "typescript": "^3.9.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Main", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/src/cli.ts", 12 | "cwd": "${workspaceRoot}", 13 | "env": { 14 | "NODE_ENV": "development" 15 | }, 16 | "args": [ 17 | // Your args here. 18 | ], 19 | "skipFiles": [ 20 | "node_modules/**/*.js" 21 | ], 22 | "outFiles": [ 23 | "${workspaceRoot}/build/**/*.js" 24 | ], 25 | "sourceMaps": true, 26 | "stopOnEntry": false, 27 | "console": "internalConsole", 28 | "trace": "all" 29 | }, 30 | { 31 | "name": "Test All", 32 | "type": "node", 33 | "request": "launch", 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | "cwd": "${workspaceRoot}", 36 | "env": { 37 | "NODE_ENV": "development" 38 | }, 39 | "args": [ 40 | "--config", 41 | "jest.config.js", 42 | "--runInBand" 43 | ], 44 | "sourceMaps": true, 45 | "stopOnEntry": false, 46 | "console": "integratedTerminal", 47 | "disableOptimisticBPs": true 48 | }, 49 | { 50 | "name": "Test Current", 51 | "type": "node", 52 | "request": "launch", 53 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 54 | "cwd": "${workspaceRoot}", 55 | "env": { 56 | "NODE_ENV": "development" 57 | }, 58 | "args": [ 59 | "${relativeFile}", 60 | "--config", 61 | "jest.config.js", 62 | "--runInBand" 63 | ], 64 | "sourceMaps": true, 65 | "stopOnEntry": false, 66 | "console": "integratedTerminal", 67 | "disableOptimisticBPs": true 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /examples/nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@codecapers/fusion": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/@codecapers/fusion/-/fusion-1.0.0.tgz", 10 | "integrity": "sha512-XWIUpaA/sjvqQDM9iUkI3d/fBTooWQrcuz2pqtWXVW63ZwJlE+TKxVZYXYJJI7GJEEgW4+U3oqqJuRDn+uNg1g==" 11 | }, 12 | "@types/node": { 13 | "version": "14.0.13", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz", 15 | "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==", 16 | "dev": true 17 | }, 18 | "arg": { 19 | "version": "4.1.3", 20 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 21 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 22 | "dev": true 23 | }, 24 | "buffer-from": { 25 | "version": "1.1.1", 26 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 27 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 28 | "dev": true 29 | }, 30 | "diff": { 31 | "version": "4.0.2", 32 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 33 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 34 | "dev": true 35 | }, 36 | "make-error": { 37 | "version": "1.3.6", 38 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 39 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 40 | "dev": true 41 | }, 42 | "source-map": { 43 | "version": "0.6.1", 44 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 45 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 46 | "dev": true 47 | }, 48 | "source-map-support": { 49 | "version": "0.5.19", 50 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 51 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 52 | "dev": true, 53 | "requires": { 54 | "buffer-from": "^1.0.0", 55 | "source-map": "^0.6.0" 56 | } 57 | }, 58 | "ts-node": { 59 | "version": "8.10.2", 60 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", 61 | "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", 62 | "dev": true, 63 | "requires": { 64 | "arg": "^4.1.0", 65 | "diff": "^4.0.1", 66 | "make-error": "^1.1.1", 67 | "source-map-support": "^0.5.17", 68 | "yn": "3.1.1" 69 | } 70 | }, 71 | "typescript": { 72 | "version": "3.9.5", 73 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", 74 | "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", 75 | "dev": true 76 | }, 77 | "yn": { 78 | "version": "3.1.1", 79 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 80 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 81 | "dev": true 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/nodejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, /* Setting a top-level property compileOnSave signals to the IDE to generate all files for a given tsconfig.json upon saving. */ 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ /* Specify library files to be included in the compilation: */ 8 | "es2015" 9 | ], 10 | //"allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./build", /* Redirect output structure to the directory. */ 17 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 29 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 30 | 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | 37 | /* Module Resolution Options */ 38 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 42 | "typeRoots": [ "node_modules/@types" ], /* List of folders to include type definitions from. */ 43 | // "types": [], /* Type declaration files to be included in compilation. */ 44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | }, 56 | "exclude": [ 57 | "node_modules", 58 | "build" 59 | ] 60 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, /* Setting a top-level property compileOnSave signals to the IDE to generate all files for a given tsconfig.json upon saving. */ 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ /* Specify library files to be included in the compilation: */ 8 | "es2015" 9 | ], 10 | //"allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./build", /* Redirect output structure to the directory. */ 17 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 29 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 30 | 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | 37 | /* Module Resolution Options */ 38 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 42 | "typeRoots": [ "node_modules/@types" ], /* List of folders to include type definitions from. */ 43 | // "types": [], /* Type declaration files to be included in compilation. */ 44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | }, 56 | "exclude": [ 57 | "node_modules", 58 | "build", 59 | "examples" 60 | ] 61 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusion 2 | 3 | A simple automated dependency injection library for TypeScript, supporting React class and functional components. 4 | 5 | Learn more about Fusion in this blog post: 6 | - [https://www.the-data-wrangler.com/roll-your-own-di](https://www.the-data-wrangler.com/roll-your-own-di) 7 | 8 | If you like this project, please star this repo and [support my work](https://www.codecapers.com.au/about#support-my-work) 9 | 10 | 11 | # Aims 12 | 13 | - To have a simple dependency injection library with minimal configuration that can be used in TypeScript code and with React. 14 | 15 | # Features 16 | 17 | - Less than 400 lines of code (used to be 300, but you know how it goes, I keep adding extra stuff) 18 | - Configuration via TypeScript decorators. 19 | - Injects properties into generic TypeScript class. 20 | - Injects properties into React class components. 21 | - Injects parameters into React functional components. 22 | - Unfortuntely decorators can't be applied to global functions (seems like a big thing missing from TypeScript??) - so the injection approach for functional components doesn't use decorators. 23 | - Automated dependency injection. 24 | - Just add mark up and let Fusion do the wiring for you. 25 | - Detects and breaks circular references (with an error) at any level of nesting. 26 | - But only when NODE_ENV is not set to "production" (to make it fast in production). 27 | - Unit tested. 28 | 29 | # Examples 30 | 31 | See [the `examples` sub-directory](https://github.com/ashleydavis/fusion/tree/master/examples) in this repo for runnable Node.js and React examples. 32 | 33 | Read the individual readme files for instructions. 34 | 35 | There's also [a separate repo](https://github.com/ashleydavis/fusion-examples) with separate examples for React class components and functional components. 36 | 37 | # Usage 38 | 39 | First enable decorators in your `tsconfig.json` file: 40 | 41 | ```json 42 | "experimentalDecorators": true 43 | ``` 44 | 45 | Install the Fusion library: 46 | 47 | ```bash 48 | npm install --save @codecapers/fusion 49 | ``` 50 | 51 | Import the bits you need: 52 | 53 | ```typescript 54 | import { InjectProperty, InjectableClass, InjectableSingleton, injectable } from "@codecapers/fusion"; 55 | ``` 56 | 57 | ## Create dependencies 58 | 59 | Create dependencies that can be injected: 60 | 61 | ### `log.ts` 62 | ```typescript 63 | // 64 | // Interface to the logging service. 65 | // 66 | interface ILog { 67 | info(msg: string): void; 68 | } 69 | 70 | // 71 | // This is a lazily injected singleton that's constructed when it's injected. 72 | // 73 | @InjectableSingleton("ILog") 74 | class Log implements ILog { 75 | info(msg: string): void { 76 | console.log(msg); 77 | } 78 | } 79 | ``` 80 | 81 | **Note:** if you can't get over the magic string, please skip to the last section! 82 | 83 | ## Inject properties into classes 84 | 85 | Mark up your class to have dependencies injected: 86 | 87 | ### `my-class.ts` 88 | ```typescript 89 | import { InjectProperty, InjectableClass } from "@codecapers/fusion"; 90 | import { ILog } from "./log"; 91 | 92 | @InjectableClass() 93 | class MyClass { 94 | 95 | // 96 | // Injects the logging service into this property. 97 | // 98 | @InjectProperty("ILog") 99 | log!: ILog; 100 | 101 | myFunction() { 102 | // 103 | // Use the injected logging service. 104 | // By the time we get to this code path the logging service 105 | // has been automatically constructed and injected. 106 | // 107 | this.log.info("Hello world!"); 108 | } 109 | 110 | // ... Other functions and other stuff ... 111 | } 112 | ``` 113 | 114 | Now instance your injectable class: 115 | 116 | 117 | ```typescript 118 | import { MyClass } from "./my-class"; 119 | 120 | // The logging singleton is lazily created at this point. 121 | const myObject = new MyClass(); 122 | ``` 123 | 124 | Injected properties are solved during construction and available for use after the consturctor has returned. 125 | 126 | So after your class is constructed you can call functions that rely on injected properties: 127 | 128 | ```typescript 129 | myObject.myFunction(); 130 | ``` 131 | 132 | ## Inject parameters into functions 133 | 134 | This can be used for injection into React functional components. 135 | 136 | Create a functional component that needs dependencies: 137 | 138 | ### `my-component.jsx` 139 | ```javascript 140 | import React from "react"; 141 | import { injectable } from "@codecapers/fusion"; 142 | 143 | function myComponent(props, context, dependency1, dependency2) { 144 | 145 | // Setup the component, use your dependencies... 146 | 147 | return ( 148 |
149 | // ... Your JSX goes here ... 150 |
; 151 | ); 152 | } 153 | ``` 154 | 155 | Wrap your functional component in the `injectable` higher order component (HOC): 156 | 157 | ### `my-component.jsx` (extended) 158 | ```javascript 159 | export default injectable(myComponent, ["IDependency1", "IDependency2"]); 160 | ``` 161 | 162 | The exported component will have the dependencies injected as parameters in the order specified (after props and context of course). 163 | 164 | ## Getting rid of the magic strings 165 | 166 | I like to get rid of the magic string by using constants co-located with the dependencies: 167 | 168 | ### `log.ts` 169 | ```javascript 170 | const ILog_id = "ILog"; 171 | 172 | // 173 | // Interface to the logging service. 174 | // 175 | interface ILog { 176 | info(msg: string): void; 177 | } 178 | 179 | // 180 | // This is a lazily injected singleton that's constructed when it's injected. 181 | // 182 | @InjectableSingleton(ILog_id) 183 | class Log implements ILog { 184 | info(msg: string): void { 185 | console.log(msg); 186 | } 187 | } 188 | ``` 189 | 190 | Then use the constant to identify your dependencies: 191 | 192 | ### `my-class.ts` 193 | ```typescript 194 | @InjectableClass() 195 | class MyClass { 196 | 197 | // 198 | // Injects the logging service into this property. 199 | // 200 | @InjectProperty(ILog_id) 201 | log!: ILog; 202 | 203 | // ... Other properties and methods ... 204 | } 205 | ``` 206 | 207 | 208 | Have fun! There's more to it than this of course, but getting started is that simple. 209 | 210 | See [the blog post](https://www.the-data-wrangler.com/roll-your-own-di) to learn more. 211 | -------------------------------------------------------------------------------- /src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { InjectProperty, InjectableClass, registerSingleton, InjectableSingleton, instantiateSingleton, setLogger, disableInjector, enableInjector, enableVerbose } from ".."; 2 | 3 | describe("fusion", () => { 4 | 5 | setLogger({ 6 | info: () => {}, 7 | error: () => {}, 8 | }); 9 | 10 | it("can construct injectable class", () => { 11 | 12 | @InjectableClass() 13 | class MyClass { 14 | } 15 | 16 | expect(new MyClass()).toBeDefined(); 17 | }); 18 | 19 | it("can inject property dependency", () => { 20 | 21 | interface IMyDependency { 22 | 23 | } 24 | 25 | class MyDependency implements IMyDependency { 26 | } 27 | 28 | @InjectableClass() 29 | class MyClass { 30 | @InjectProperty("IMyDependency") 31 | dep!: IMyDependency; 32 | } 33 | 34 | const theDependency = new MyDependency(); 35 | registerSingleton("IMyDependency", theDependency); 36 | 37 | expect(new MyClass().dep).toBe(theDependency); 38 | }); 39 | 40 | it("can inject two property dependencies", () => { 41 | 42 | interface IMyDependency1 { 43 | } 44 | 45 | class MyDependency1 implements IMyDependency1 { 46 | } 47 | 48 | interface IMyDependency2 { 49 | } 50 | 51 | class MyDependency2 implements IMyDependency2 { 52 | } 53 | 54 | @InjectableClass() 55 | class MyClass { 56 | @InjectProperty("IMyDependency1") 57 | dep1!: IMyDependency1; 58 | 59 | @InjectProperty("IMyDependency2") 60 | dep2!: IMyDependency2; 61 | } 62 | 63 | const theDependency1 = new MyDependency1(); 64 | registerSingleton("IMyDependency1", theDependency1); 65 | 66 | const theDependency2 = new MyDependency2(); 67 | registerSingleton("IMyDependency2", theDependency2); 68 | 69 | const theInstance = new MyClass(); 70 | expect(theInstance.dep1).toBe(theDependency1); 71 | expect(theInstance.dep2).toBe(theDependency2); 72 | }); 73 | 74 | it("can disable injector", () => { 75 | 76 | disableInjector(); 77 | 78 | interface IMyDependency { 79 | } 80 | 81 | class MyDependency implements IMyDependency { 82 | } 83 | 84 | @InjectableClass() 85 | class MyClass { 86 | @InjectProperty("IMyDependency") 87 | dep!: IMyDependency; 88 | } 89 | 90 | const theDependency = new MyDependency(); 91 | registerSingleton("IMyDependency", theDependency); 92 | 93 | const theInstance = new MyClass(); 94 | 95 | enableInjector(); 96 | 97 | expect(theInstance.dep).toBeUndefined(); 98 | 99 | }); 100 | 101 | it("throws when property dependency is not found", () => { 102 | 103 | interface IMyDependency { 104 | } 105 | 106 | @InjectableClass() 107 | class MyClass { 108 | @InjectProperty("A-dependency-that-does-not-exist") 109 | dep!: IMyDependency; 110 | } 111 | 112 | expect(() => new MyClass()).toThrowError(); 113 | }); 114 | 115 | it("can lazily instantiate and inject singleton", () => { 116 | 117 | interface IMySingleton1 { 118 | } 119 | 120 | let numInstances = 0; 121 | 122 | @InjectableSingleton("IMySingleton1") 123 | class MySingleton1 implements IMySingleton1 { 124 | constructor() { 125 | ++numInstances; 126 | } 127 | } 128 | 129 | @InjectableClass() 130 | class MyClass { 131 | @InjectProperty("IMySingleton1") 132 | dep!: IMySingleton1; 133 | } 134 | 135 | expect(new MyClass().dep).toBeDefined(); 136 | expect(numInstances).toBe(1); 137 | }); 138 | 139 | it("singleton is only instantiated once", () => { 140 | 141 | interface IMySingleton2 { 142 | } 143 | 144 | let numInstances = 0; 145 | 146 | @InjectableSingleton("IMySingleton2") 147 | class MySingleton2 implements IMySingleton2 { 148 | constructor() { 149 | ++numInstances; 150 | } 151 | } 152 | 153 | @InjectableClass() 154 | class MyClass1 { 155 | @InjectProperty("IMySingleton2") 156 | dep!: IMySingleton2; 157 | } 158 | 159 | @InjectableClass() 160 | class MyClass2 { 161 | @InjectProperty("IMySingleton2") 162 | dep!: IMySingleton2; 163 | } 164 | 165 | const theInstance1 = new MyClass1(); 166 | expect(theInstance1.dep).toBeDefined(); 167 | 168 | const theInstance2 = new MyClass1(); 169 | expect(theInstance2.dep).toBe(theInstance1.dep); 170 | expect(numInstances).toBe(1); 171 | }); 172 | 173 | it("throws when singleton constructor throws", () => { 174 | 175 | interface IMySingleton3 { 176 | } 177 | 178 | @InjectableSingleton("IMySingleton3") 179 | class MySingleton3 implements IMySingleton3 { 180 | constructor() { 181 | throw new Error("An error!"); 182 | } 183 | } 184 | 185 | @InjectableClass() 186 | class MyClass { 187 | @InjectProperty("IMySingleton3") 188 | dep!: IMySingleton3; 189 | } 190 | 191 | expect(() => new MyClass()).toThrow(); 192 | }); 193 | 194 | it("can manually instantiate singleton", () => { 195 | 196 | interface IMySingleton4 { 197 | } 198 | 199 | let numInstances = 0; 200 | 201 | @InjectableSingleton("IMySingleton4") 202 | class MySingleton4 implements IMySingleton4 { 203 | constructor() { 204 | ++numInstances; 205 | } 206 | } 207 | 208 | expect(instantiateSingleton("IMySingleton4")).toBeDefined(); 209 | expect(numInstances).toBe(1); 210 | }); 211 | 212 | it("throws error for circular dependency", () => { 213 | 214 | interface IMySingleton5 { 215 | } 216 | 217 | @InjectableSingleton("IMySingleton5") 218 | class MySingleton5 implements IMySingleton5 { 219 | @InjectProperty("MySingleton7") 220 | dep1!: MySingleton7; 221 | } 222 | 223 | interface IMySingleton6 { 224 | } 225 | 226 | @InjectableSingleton("IMySingleton6") 227 | class MySingleton6 implements IMySingleton6 { 228 | @InjectProperty("IMySingleton5") 229 | dep1!: IMySingleton5; 230 | } 231 | 232 | interface IMySingleton7 { 233 | } 234 | 235 | @InjectableSingleton("IMySingleton7") 236 | class MySingleton7 implements IMySingleton7 { 237 | @InjectProperty("IMySingleton6") 238 | dep1!: IMySingleton6; 239 | } 240 | 241 | @InjectableClass() 242 | class MyClass { 243 | @InjectProperty("IMySingleton7") 244 | dep!: IMySingleton7; 245 | } 246 | 247 | expect(() => new MyClass()).toThrowError(); 248 | }); 249 | 250 | it("can enable and disable verbose logging", () => { 251 | enableVerbose(true); 252 | enableVerbose(false); 253 | }); 254 | 255 | }); 256 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Allows the automated injector to be enabled/disabled, good for automated testing where you want to manually do the injection. 3 | // 4 | let injectorEnabled = true; 5 | 6 | // 7 | // Allows automatic singletons to be enabled/disabled. 8 | // 9 | let automaticSingletons = true; 10 | 11 | // 12 | // Allows checking for circular dependencies (there's a performance cost to this, so only enabled on request). 13 | // 14 | let enableCircularCheck = false; 15 | 16 | // 17 | // Enables automatic singleons. 18 | // 19 | export function enableAutomaticSingletons(): void { 20 | automaticSingletons = true; 21 | } 22 | 23 | // 24 | // Disables automatic singleons. 25 | // 26 | export function disableAutomaticSingletons(): void { 27 | automaticSingletons = false; 28 | } 29 | 30 | // 31 | // Enables circular dependency checking. 32 | // 33 | export function enableCircularDependencyCheck() { 34 | enableCircularCheck = true; 35 | } 36 | 37 | // 38 | // Disables circular dependency checking. 39 | // 40 | export function disableCircularDependencyCheck() { 41 | enableCircularCheck = false; 42 | } 43 | 44 | // 45 | // Enable the injector. 46 | // 47 | export function enableInjector(): void { 48 | injectorEnabled = true; 49 | } 50 | 51 | // 52 | // Disable the injector. 53 | // 54 | export function disableInjector(): void { 55 | injectorEnabled = false; 56 | } 57 | 58 | // 59 | // Enable or disable verbose mode which is good for debugging. 60 | // 61 | export function enableVerbose(enable: boolean): void { 62 | verbose = enable; 63 | } 64 | 65 | // 66 | // Interface to the logger. 67 | // 68 | export interface ILog { 69 | // 70 | // Log an information message. 71 | // 72 | info(msg: string): void; 73 | 74 | // 75 | // Log an error. 76 | // 77 | error(err: string): void; 78 | } 79 | 80 | let log: ILog = { 81 | // 82 | // Log an information message. 83 | // 84 | info(msg: string): void { 85 | console.log(msg); 86 | }, 87 | 88 | // 89 | // Log an error. 90 | // 91 | error(msg: string): void { 92 | console.error(msg); 93 | }, 94 | }; 95 | 96 | // 97 | // Set a new logger. 98 | // 99 | export function setLogger(newLog: ILog) { 100 | log = newLog; 101 | } 102 | 103 | // 104 | // Constructors that can be called to instantiate singletons. 105 | // 106 | const singletonConstructors = new Map(); 107 | 108 | // 109 | // Collection of all singletons objects that can be injected. 110 | // 111 | const instantiatedSingletons = new Map(); 112 | 113 | // 114 | // What is currently being injected. 115 | // This allows us to deals with circular dependencies. 116 | // This is only enabled when NODE_ENV is not equal to "production". 117 | // 118 | const injectionMap = new Set(); 119 | 120 | // 121 | // Stack of constructor names. 122 | // Available when verbose mode is enabled. 123 | // 124 | const constructorStack: string[] = []; 125 | 126 | // 127 | // Set to true to enable verbose mode. 128 | // 129 | let verbose: boolean = false; 130 | 131 | // 132 | // Manually registers a singleton. 133 | // 134 | export function registerSingleton(dependencyId: string, singleton: any): void { 135 | if (verbose) { 136 | log.info("@@@@ Manually registered singleton: " + dependencyId); 137 | } 138 | 139 | instantiatedSingletons.set(dependencyId, singleton); 140 | } 141 | 142 | // 143 | // Register many singletons at once. 144 | // 145 | export function registerSingletons(singletons: any): void { 146 | for (const [name, singleton] of Object.entries(singletons)) { 147 | registerSingleton(name, singleton); 148 | } 149 | } 150 | 151 | // 152 | // Clear all singletons. Useful for testing. 153 | // 154 | export function clearSingletons(): void { 155 | instantiatedSingletons.clear(); 156 | } 157 | 158 | let nextConstructorId = 1; 159 | 160 | // 161 | // Takes a constructor and makes it 'injectable'. 162 | // Wraps the constructor in a proxy that handles injecting dependencies. 163 | // 164 | function makeConstructorInjectable(origConstructor: Function): Function { 165 | if (verbose) { 166 | log.info("@@@@ Making constructor injectable: " + origConstructor.name); 167 | } 168 | 169 | if (origConstructor.prototype.__id__ !== undefined) { 170 | throw new Error(`Constructor ${origConstructor.name} has already been made injectable with id ${origConstructor.prototype.__id__}.`); 171 | } 172 | 173 | origConstructor.prototype.__id__ = nextConstructorId++; 174 | 175 | if (!origConstructor.prototype.__injections__) { 176 | // Record properties to be injected against the constructor prototype. 177 | origConstructor.prototype.__injections__ = []; 178 | } 179 | 180 | const proxyHandler = { 181 | construct(target: any, args: any[], newTarget: any) { 182 | 183 | if (verbose) { 184 | log.info("++++ Proxy constructor for injectable class: " + origConstructor.name); 185 | 186 | constructorStack.push(origConstructor.name); 187 | } 188 | 189 | try { 190 | // 191 | // Construct the object ... 192 | // 193 | const obj = Reflect.construct(target, args, newTarget); 194 | 195 | if (injectorEnabled) { 196 | try { 197 | // 198 | // ... and then resolve property dependencies. 199 | // 200 | resolvePropertyDependencies(origConstructor.prototype.__id__, origConstructor.name, obj, origConstructor.prototype.__injections__); 201 | } 202 | catch (err) { 203 | log.error(`Failed to construct ${origConstructor.name} due to exception thrown by ${resolvePropertyDependencies.name}.`); 204 | throw err; 205 | } 206 | } 207 | 208 | return obj; 209 | } 210 | finally { 211 | if (verbose) { 212 | constructorStack.pop(); 213 | } 214 | } 215 | } 216 | }; 217 | 218 | // Wrap the original constructor in a proxy. 219 | // Use the proxy to inject dependencies. 220 | // Returns the proxy constructor to use in place of the original constructor. 221 | return new Proxy(origConstructor, proxyHandler); 222 | } 223 | 224 | // 225 | // Returns true if a singleton is registered. 226 | // 227 | export function isSingletonRegistered(dependencyId: string): boolean { 228 | return singletonConstructors.has(dependencyId); 229 | } 230 | 231 | // 232 | // Returns true if a singleton is instantiated. 233 | // 234 | export function isSingletonInstantiated(dependencyId: string): boolean { 235 | return instantiatedSingletons.has(dependencyId); 236 | } 237 | 238 | // 239 | // Instantiates a singleton. 240 | // If it's already instantiated then the original is returned instead. 241 | // 242 | export function instantiateSingleton(dependencyId: string): T { 243 | if (verbose) { 244 | log.info("<<< Requesting singleton: " + dependencyId); 245 | } 246 | 247 | try { 248 | const existingSingleton = instantiatedSingletons.get(dependencyId); 249 | if (existingSingleton) { 250 | if (verbose) { 251 | log.info("= Singleton already exists: " + dependencyId); 252 | } 253 | // The singleton has previously been instantiated. 254 | return existingSingleton; 255 | } 256 | 257 | const singletonConstructor = singletonConstructors.get(dependencyId); 258 | if (!singletonConstructor) { 259 | // The requested constructor was not found. 260 | let msg = "No constructor found for singleton " + dependencyId; 261 | if (constructorStack.length > 0) { 262 | msg += `\r\nConstructor stack: ${constructorStack.join(" -> ")}`; 263 | } 264 | log.error(msg); 265 | log.info("Available constructors: \r\n" + 266 | Array.from(singletonConstructors.entries()) 267 | .map(entry => 268 | "\t" + entry[0] + " -> " + entry[1].name 269 | ) 270 | .join("\r\n") 271 | ); 272 | throw new Error(msg); 273 | } 274 | 275 | if (verbose) { 276 | log.info("= Lazily instantiating singleton: " + dependencyId); 277 | } 278 | 279 | // Construct the singleton. 280 | const instantiatedSingleton = Reflect.construct(singletonConstructor, []); 281 | 282 | // Cache the instantiated singleton for later reuse. 283 | instantiatedSingletons.set(dependencyId, instantiatedSingleton); 284 | if (verbose) { 285 | log.info("= Lazily instantiated singleton: " + dependencyId); 286 | } 287 | return instantiatedSingleton; 288 | } 289 | catch (err) { 290 | log.error("Failed to instantiate singleton " + dependencyId); 291 | log.error(err && err.stack || err); 292 | throw err; 293 | } 294 | } 295 | 296 | // 297 | // Resolve dependencies for properties of an instantiated object. 298 | // 299 | function resolvePropertyDependencies(constructorId: number, constructorName: string, obj: any, injections: any[]): void { 300 | 301 | if (verbose) { 302 | log.info(`>>>> Resolving dependencies for new instance of ${constructorName}.`); 303 | } 304 | 305 | if (injections) { 306 | if (enableCircularCheck) { 307 | if (injectionMap.has(constructorId)) { 308 | throw new Error(`${constructorName} has already been injected, this exception breaks a circular reference that would crash the app.`); 309 | } 310 | 311 | injectionMap.add(constructorId); 312 | } 313 | 314 | try { 315 | 316 | for (const injection of injections) { 317 | const dependencyId = injection[1]; 318 | 319 | if (verbose) { 320 | log.info(">>>> Injecting " + dependencyId); 321 | } 322 | 323 | const singleton = instantiateSingleton(dependencyId); 324 | if (!singleton) { 325 | throw new Error("Failed to instantiate singleton " + dependencyId); 326 | } 327 | 328 | obj[injection[0]] = singleton; 329 | } 330 | } 331 | finally { 332 | if (enableCircularCheck) { 333 | injectionMap.delete(constructorId); 334 | } 335 | } 336 | } 337 | } 338 | 339 | // 340 | // TypeScript decorator: 341 | // Marks a class as an automatically created singleton that's available for injection. 342 | // Makes a singleton available for injection. 343 | // 344 | export function InjectableSingleton(dependencyId: string): Function { 345 | if (verbose) { 346 | log.info("@@@@ Registering singleton " + dependencyId); 347 | } 348 | 349 | // Returns a factory function that records the constructor of the class so that 350 | // it can be lazily created later as as a singleton when required as a dependency. 351 | return (origConstructor: Function): Function => { 352 | if (verbose) { 353 | log.info("@@@@ Caching constructor for singleton: " + dependencyId); 354 | } 355 | 356 | const injectableConstructor = makeConstructorInjectable(origConstructor); 357 | 358 | // Adds the target constructor to the set of lazily createable singletons. 359 | if (automaticSingletons) { 360 | singletonConstructors.set(dependencyId, injectableConstructor); 361 | } 362 | 363 | return injectableConstructor; 364 | } 365 | } 366 | 367 | // 368 | // TypeScript decorator: 369 | // Marks a class as injectable. 370 | // Not require for singletons, they are automatically injectable. 371 | // 372 | export function InjectableClass(): Function { 373 | // Returns a factory function that creates a proxy constructor. 374 | return makeConstructorInjectable; 375 | } 376 | 377 | // 378 | // TypeScript decorator: 379 | // Injects a dependency to a property. 380 | // 381 | export function InjectProperty(dependencyId: string): Function { 382 | // Returns a function that is invoked for the property that is to be injected. 383 | return (prototype: any, propertyName: string): void => { 384 | if (verbose) { 385 | log.info("@@@@ Setup to inject " + dependencyId + " to property " + propertyName + " in " + prototype.constructor.name); 386 | } 387 | 388 | if (!prototype.__injections__) { 389 | // Record properties to be injected against the constructor prototype. 390 | prototype.__injections__ = []; 391 | } 392 | 393 | // Record injections to be resolved later when an instance is created. 394 | prototype.__injections__.push([propertyName, dependencyId]); 395 | }; 396 | } 397 | 398 | // 399 | // Injects a list of dependencies into a function. 400 | // 401 | export function injectable(fn: Function, dependencyIds: string []) { 402 | return (...args: any) => { 403 | return fn( 404 | ...args, 405 | ...dependencyIds.map(id => instantiateSingleton(id)) 406 | ); 407 | }; 408 | } 409 | --------------------------------------------------------------------------------