├── 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 |
--------------------------------------------------------------------------------