├── .gitignore ├── README.md ├── index.html ├── package.json ├── res ├── graphscreencap.png └── legend.png ├── src ├── fshandler.ts ├── index.js ├── index.ts ├── models.ts └── testcompiler.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /output 3 | /node_modules 4 | package-lock.json 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DependenSi 2 | Generates component dependency graphs for Angular2+ projects. 3 | 4 | ## Usage 5 | `npm run build` 6 | 7 | `npm run analyze -- '[angularprojectdir]/app.module.ts'` 8 | 9 | `npm run serve` 10 | 11 | ![alt text](./res/graphscreencap.png "Component dependency graph example") 12 | 13 | ## Routing information (optional) 14 | Run the script below on the homepage of your standing angular2+ application and paste the results into output/dependenSiRoutes.json 15 | 16 | ```javascript 17 | fix = function(o) { 18 | o.forEach(item => { 19 | if(item.component) item.component = item.component.name; 20 | if(item.children) fix(item.children); 21 | }); 22 | } 23 | let tmp = ng.probe($('app-root')).injector.get(ng.coreTokens.Router).config; 24 | fix(tmp); 25 | console.log(JSON.stringify(tmp, null, '\t')); 26 | ``` 27 | 28 | **TODO:** Automate routing information retrieval, git changes integration 29 | 30 | ## License 31 | 32 | The MIT License (MIT) 33 | 34 | Copyright (c) 2018 Alejandro Munoz 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 96 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependensi", 3 | "version": "0.0.1", 4 | "description": "A set of tools to generate a dependencies graph of an Angular application", 5 | "scripts": { 6 | "build": "tsc src/index.ts --m commonjs --outDir dist/", 7 | "analyze": "node src/index.js", 8 | "serve": "http-server", 9 | "ci:test": "jasmine" 10 | }, 11 | "author": "Alejandro Munoz", 12 | "engines": { 13 | "node": ">= 5.4.1" 14 | }, 15 | "license": "MIT", 16 | "private": true, 17 | "dependencies": { 18 | "@types/node": "^6.0.39", 19 | "http-server": "^0.11.1", 20 | "rimraf": "^2.6.1", 21 | "safe-json-stringify": "^1.0.4", 22 | "typescript": "^2.4.1", 23 | "vis": "^4.20.1" 24 | }, 25 | "main": "dist/index.js", 26 | "devDependencies": {} 27 | } 28 | -------------------------------------------------------------------------------- /res/graphscreencap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbro/dependensi-angular/8e807b15cb0fd109633303159cb18ce33a672491/res/graphscreencap.png -------------------------------------------------------------------------------- /res/legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbro/dependensi-angular/8e807b15cb0fd109633303159cb18ce33a672491/res/legend.png -------------------------------------------------------------------------------- /src/fshandler.ts: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | 3 | export default class fshandler { 4 | static readJSON(dir) { 5 | return JSON.parse(fs.readFileSync(dir, 'utf8')); 6 | } 7 | 8 | static readFileSync(dir) { 9 | return fs.readFileSync(dir, 'utf8'); 10 | } 11 | 12 | static writeFileSync(dir, contents) { 13 | return fs.writeFileSync(dir, contents, 'utf8'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | let compiler = require('../dist/index'); 2 | let fs = require('fs'); 3 | let outputDir = './output/'; 4 | let routes; 5 | 6 | if(!fs.existsSync(outputDir)) { 7 | fs.mkdirSync(outputDir); 8 | } 9 | else { 10 | try { 11 | routes = JSON.parse(fs.readFileSync('./output/dependenSiRoutes.json', 'utf8')); 12 | } 13 | catch(e) { } 14 | } 15 | 16 | let angCompiler = new compiler.compiler(process.argv[2]); 17 | 18 | angCompiler.compile({"routes":routes, "outDir": outputDir}); 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import tcompile from './testcompiler'; 2 | import * as ts from 'typescript'; 3 | let rimraf = require('rimraf'); 4 | 5 | export class compiler { 6 | 7 | files: string[]; 8 | routes: any; 9 | outDir: string; 10 | 11 | constructor(file: string) { 12 | this.files = [file]; 13 | } 14 | 15 | setRoutes(routes) { 16 | this.routes = routes; 17 | } 18 | 19 | compile(options: any = {}) { 20 | if(this.files) { 21 | let exitCode = tcompile(this.files, { 22 | experimentalDecorators: true, allowJs: true, outDir: './tmp', 23 | target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.ES2015 24 | }, options.outDir, options.routes); 25 | rimraf.sync('./tmp'); 26 | // console.log(`Process exiting with code '${exitCode}'.`); 27 | return exitCode; 28 | } 29 | else { 30 | throw "file not defined."; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | import fshandler from './fshandler'; 2 | 3 | let isRoute = function(routes, comp) { 4 | if(routes) { 5 | return routes.some(route => { 6 | if(route.component && comp.name && route.component === comp.name) return true; 7 | if(route.children) return isRoute(route.children, comp); 8 | }); 9 | } 10 | } 11 | 12 | export interface NodeObject { 13 | kind: Number; 14 | pos: Number; 15 | end: Number; 16 | text: string; 17 | initializer: NodeObject, 18 | name?: { text: string }; 19 | expression?: NodeObject; 20 | elements?: NodeObject[]; 21 | arguments?: NodeObject[]; 22 | properties?: any[]; 23 | parserContextFlags?: Number; 24 | equalsGreaterThanToken?: NodeObject[]; 25 | parameters?: NodeObject[]; 26 | Component?: String; 27 | body?: { 28 | pos: Number; 29 | end: Number; 30 | statements: NodeObject[]; 31 | } 32 | } 33 | 34 | export class ComponentContainer { 35 | components: Component[] = []; 36 | 37 | types:string[] = ["Component", "Injectable", "NgModule"]; 38 | 39 | add(component) { 40 | this.components.push(component); 41 | } 42 | 43 | getComponents() { 44 | return this.components; 45 | } 46 | 47 | toDependenSiMap(routerMap) { 48 | return this.components.reduce((prev, current) => { 49 | return prev.concat(current.toDependenSiMap(routerMap)); 50 | },[]); 51 | } 52 | 53 | toDependenSiMapEdges() { 54 | return this.components.reduce((prev, current) => { 55 | return prev.concat(current.toDependenSiMapEdges()); 56 | },[]); 57 | } 58 | 59 | toString() { 60 | return this.components.reduce((prev, component) => { 61 | return prev.concat(component.toString()); 62 | },[]).join("\n\n"); 63 | } 64 | } 65 | 66 | let colorMap = function(type) { 67 | switch(type) { 68 | case "Component": 69 | return { 70 | background: "#82b2ff", 71 | border: "#6083bc", 72 | highlight: { 73 | background: "#b7d3ff", 74 | border: "#6083bc" 75 | } 76 | } 77 | case "Injectable": 78 | return { 79 | background: "#f7f5a3", 80 | border: "#a09f6b", 81 | highlight: { 82 | background: "#fffed6", 83 | border: "#a09f6b" 84 | } 85 | } 86 | case "NgModule": 87 | return { 88 | background: "#c7a3f7", 89 | border: "#896ead", 90 | highlight: { 91 | background: "#decdf4", 92 | border: "#896ead" 93 | } 94 | } 95 | case "RouterLink": 96 | return { 97 | background: "#93ffa0", 98 | border: "#5ea366", 99 | highlight: { 100 | background: "#ccffd1", 101 | border: "#5ea366" 102 | } 103 | } 104 | default: 105 | return { 106 | background: "#c9c9c9", 107 | border: "#9e9e9e", 108 | highlight: { 109 | background: "#e0e0e0", 110 | border: "#9e9e9e", 111 | } 112 | } 113 | } 114 | } 115 | 116 | export class Component { 117 | name: string; 118 | dir: string; 119 | filename: string; 120 | selector: string; 121 | type: string; 122 | templateUrl: string; 123 | template: string; 124 | htmlcomponents: Component[]; 125 | routerLink: string[]; 126 | importcomponents: Component[]; 127 | externalimports: string[]; 128 | rawimports: string[]; 129 | toString = function (verbose = null) { 130 | let result = ""; 131 | if(this.name) result += "\nName: " + this.name; 132 | result += "\nFilename: " + this.filename; 133 | result += "\nDirectory: " + this.dir; 134 | if(this.selector) result += "\nSelector: " + this.selector; 135 | if(this.type) result += "\nType: " + this.type; 136 | // if(this.imports) result += "\nImports: " + this.imports.join(', '); 137 | if(this.externalimports) result += "\nExternal Imports: " + this.externalimports.join(', '); 138 | if(this.importcomponents) result += "\nImports: " + this.importcomponents.reduce((prev, component) => { if(component.name) return prev.concat(component.name); return prev.concat(component.filename) },[]).join(", "); 139 | if(this.htmlcomponents) result += "\nHTMLComponents: " + this.htmlcomponents.reduce((prev, component) => { return prev.concat(component.name); },[]).join(", "); 140 | if(this.routerLink) result += "\nRouterLink: " + this.routerLink; 141 | if(verbose) { 142 | if(this.template) result += "\nTemplate: " + this.template; 143 | } 144 | return result; 145 | } 146 | toDependenSiMap = function (routerMap) { 147 | let result = { 148 | id: this.dir + this.filename, 149 | label: "", 150 | color: colorMap(this.type), 151 | mass: 3, 152 | title: this.toString() 153 | } 154 | if(this.name && isRoute(routerMap, this)) result.color = colorMap("RouterLink"); 155 | if(this.name) result.label = this.name; 156 | else result.label = this.filename; 157 | return result; 158 | } 159 | toDependenSiMapEdges = function () { 160 | let result = []; 161 | let result2 = []; 162 | if(this.importcomponents) { 163 | result = this.importcomponents.reduce((prev, current) => { 164 | return prev.concat({from: this.dir + this.filename, to: current.dir + current.filename, arrows: "to", color: "blue", length: 100, selectionWidth: 3}); 165 | },[]); 166 | } 167 | if(this.htmlcomponents) { 168 | result2 = this.htmlcomponents.reduce((prev, current) => { 169 | return prev.concat({from: this.dir + this.filename, to: current.dir + current.filename, arrows: "to", color: "red", length: 100, selectionWidth: 3}); 170 | },[]); 171 | } 172 | return result.concat(result2); 173 | } 174 | } 175 | 176 | export interface Dependencies { 177 | name: string; 178 | selector?: string; 179 | label?: string; 180 | file?: string; 181 | templateUrl?: string[]; 182 | styleUrls?: string[]; 183 | providers?: Dependencies[]; 184 | imports?: Dependencies[]; 185 | exports?: Dependencies[]; 186 | declarations?: Dependencies[]; 187 | bootstrap?: Dependencies[]; 188 | __raw?: any 189 | } 190 | -------------------------------------------------------------------------------- /src/testcompiler.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | import fshandler from './fshandler'; 4 | import { NodeObject, ComponentContainer, Component, Dependencies } from './models'; 5 | const safeJsonStringify = require('safe-json-stringify'); 6 | 7 | export default function tcompile(fileNames: string[], options: ts.CompilerOptions, outDir: string = "./output/", routes = null): number { 8 | fileNames.forEach(file => { 9 | try{ 10 | fshandler.readFileSync(file); 11 | } 12 | catch(e) { 13 | throw 'Source file ' + file + ' does not exist.'; 14 | } 15 | }); 16 | let program = ts.createProgram(fileNames, options); 17 | let emitResult = program.emit(); 18 | 19 | let exitCode: number = emitResult.emitSkipped ? 1 : 0; 20 | 21 | exitCode = exitCode? 1 : extractData(program, outDir, routes); 22 | return exitCode; 23 | } 24 | 25 | let container: ComponentContainer = new ComponentContainer(); 26 | 27 | function extractData(program, outDir: string, routerMap) { 28 | let sourceFiles = program.getSourceFiles() || []; 29 | 30 | //Iterate through all source files 31 | sourceFiles.map((file: ts.SourceFile) => { 32 | 33 | let filePath = file.fileName; 34 | 35 | //Only parse ts files 36 | if (path.extname(filePath) === '.ts') { 37 | 38 | //Ignore node_modules and testing files 39 | if (filePath.lastIndexOf('.d.ts') === -1 && filePath.lastIndexOf('spec.ts') === -1 && !(filePath as any).includes('node_modules')) { 40 | 41 | let tmpcomp: Component = new Component(); 42 | 43 | //Get file information 44 | tmpcomp.dir = filePath.split('/').slice(0,-1).join('/') + "/"; 45 | tmpcomp.filename = filePath.split('/').slice(-1)[0]; 46 | 47 | try { 48 | ts.forEachChild(file, (node: any) => { 49 | 50 | //Get class name 51 | if(node.name && node.name.text) tmpcomp.name = node.name.text; 52 | 53 | if(node.parent.locals) { 54 | node.parent.locals.forEach((value, key) => { 55 | container.types.some((item) => { 56 | if(key === item) { 57 | tmpcomp.type = key; 58 | return true; 59 | } 60 | }) 61 | }); 62 | } 63 | 64 | //Get imports 65 | let externalimports: string[] = []; 66 | let imports: string[] = []; 67 | 68 | if(node.parent.resolvedModules) { 69 | node.parent.resolvedModules.forEach((value, key)=> { 70 | if(!value || value.isExternalLibraryImport) externalimports.push(key); 71 | else imports.push(value.resolvedFileName); 72 | }, []); 73 | } 74 | 75 | if(imports.length > 0) tmpcomp.rawimports = imports; 76 | 77 | if(externalimports.length > 0) tmpcomp.externalimports = externalimports; 78 | 79 | if(node.decorators) { 80 | 81 | node.decorators.forEach((visitedNode) => { 82 | 83 | if(visitedNode.expression.arguments.slice(-1)[0]) { 84 | //Get relevant decorator properties 85 | let props = visitedNode.expression.arguments.slice(-1)[0].properties; 86 | 87 | if(props) { 88 | if(getComponentSelector(props)) tmpcomp.selector = getComponentSelector(props)[0]; 89 | 90 | //Get templateurl and routerlinks 91 | if(getComponentTemplateUrl(props)[0]) { 92 | tmpcomp.templateUrl = getComponentTemplateUrl(props)[0]; 93 | let templatecontent = fshandler.readFileSync(tmpcomp.dir + tmpcomp.templateUrl); 94 | // if(templatecontent.includes('routerLink')) { 95 | // let tmp = templatecontent.substring(templatecontent.search('routerLink')); 96 | // tmpcomp.routerLink = tmp.match(/".+?"/).map(item => item.substring(1, item.length-1)); 97 | // } 98 | } 99 | 100 | //Get template html and routerlinks 101 | if(getComponentTemplate(props)[0]) { 102 | tmpcomp.template = getComponentTemplate(props)[0]; 103 | // if((tmpcomp.template as any).includes('routerLink')) { 104 | // let tmp = tmpcomp.template.substring(tmpcomp.template.search('routerLink')); 105 | // tmpcomp.routerLink = tmp.match(/".+?"/).map(item => item.substring(1, item.length-1)); 106 | // } 107 | } 108 | } 109 | } 110 | 111 | }); 112 | } 113 | }); 114 | } 115 | catch (e) { 116 | console.log(e + " : " + file.fileName); 117 | } 118 | 119 | container.add(tmpcomp); 120 | } 121 | } 122 | }); 123 | 124 | container.getComponents().map(component => { 125 | //Parse html template for component dependencies 126 | if(component.templateUrl) { 127 | let fileOut = fshandler.readFileSync(component.dir + component.templateUrl); 128 | let foundSelectors: Component[] = container.getComponents().reduce((prev, innercomponent) => { 129 | if(fileOut.includes("")) return prev.concat(innercomponent); 130 | return prev; 131 | }, []); 132 | if(foundSelectors.length > 0) component.htmlcomponents = foundSelectors; 133 | } 134 | else if(component.template) { 135 | let foundSelectors: Component[] = container.getComponents().reduce((prev, innercomponent) => { 136 | if((component.template as any).includes("")) return prev.concat(innercomponent); 137 | return prev; 138 | }, []); 139 | if(foundSelectors.length > 0) component.htmlcomponents = foundSelectors; 140 | } 141 | 142 | //Convert relative import directory paths to respective component objects 143 | if(component.rawimports) { 144 | let importcomponents: Component[] = []; 145 | component.rawimports.map(imprt => { 146 | container.getComponents().map(innercomponent => { 147 | if(imprt === innercomponent.dir + innercomponent.filename) { 148 | importcomponents.push(innercomponent); 149 | } 150 | }); 151 | }); 152 | if(importcomponents.length > 0) component.importcomponents = importcomponents; 153 | } 154 | }); 155 | 156 | //console.log(container.toString()); 157 | let res = "["; 158 | res += container.getComponents().reduce((prev, item) => prev.concat(safeJsonStringify(item)), []).join(","); 159 | res += "]" 160 | fshandler.writeFileSync((outDir + 'dependenSi.json'), res); 161 | fshandler.writeFileSync((outDir + 'dependenSiMap.json'), JSON.stringify(container.toDependenSiMap(routerMap), null, '\t')); 162 | fshandler.writeFileSync((outDir + 'dependenSiMapEdges.json'), JSON.stringify(container.toDependenSiMapEdges(), null, '\t')); 163 | return 0; 164 | } 165 | 166 | function getComponentTemplateUrl(props: NodeObject[]): string[] { 167 | return sanitizeUrls(getSymbolDeps(props, 'templateUrl')); 168 | } 169 | 170 | function getComponentTemplate(props: NodeObject[]): string[] { 171 | return getSymbolDeps(props, 'template'); 172 | } 173 | 174 | function getComponentSelector(props: NodeObject[]): string[] { 175 | return getSymbolDeps(props, 'selector').slice(-1); 176 | } 177 | 178 | function getComponentStyleUrls(props: NodeObject[]): string[] { 179 | return sanitizeUrls(getSymbolDeps(props, 'styleUrls')); 180 | } 181 | 182 | function sanitizeUrls(urls: string[]) { 183 | return urls.map(url => url.replace('./', '')); 184 | } 185 | 186 | function getSymbolDeps(props: NodeObject[], type: string): string[] { 187 | 188 | let deps = props.filter((node: NodeObject) => { 189 | return node.name.text === type; 190 | }); 191 | 192 | let buildIdentifierName = (node: NodeObject, name = '') => { 193 | 194 | if (node.expression) { 195 | name = name ? `.${name}` : name; 196 | 197 | let nodeName; 198 | if (node.name) { 199 | nodeName = node.name.text; 200 | } 201 | else if (node.text) { 202 | nodeName = node.text; 203 | } 204 | else if (node.expression) { 205 | 206 | if (node.expression.text) { 207 | nodeName = node.expression.text; 208 | } 209 | else if(node.expression.elements) { 210 | 211 | if (node.expression.kind === ts.SyntaxKind.ArrayLiteralExpression) { 212 | nodeName = node.expression.elements.map( el => el.text ).join(', '); 213 | nodeName = `[${nodeName}]`; 214 | } 215 | 216 | } 217 | } 218 | 219 | if (node.kind === ts.SyntaxKind.SpreadElement) { 220 | return `...${nodeName}`; 221 | } 222 | return `${buildIdentifierName(node.expression, nodeName)}${name}` 223 | } 224 | 225 | return `${node.text}.${name}`; 226 | } 227 | 228 | let parseProviderConfiguration = (o: NodeObject): string => { 229 | // parse expressions such as: 230 | // { provide: APP_BASE_HREF, useValue: '/' }, 231 | // or 232 | // { provide: 'Date', useFactory: (d1, d2) => new Date(), deps: ['d1', 'd2'] } 233 | 234 | let _genProviderName: string[] = []; 235 | let _providerProps: string[] = []; 236 | 237 | (o.properties || []).forEach((prop: NodeObject) => { 238 | 239 | let identifier = prop.initializer.text; 240 | if (prop.initializer.kind === ts.SyntaxKind.StringLiteral) { 241 | identifier = `'${identifier}'`; 242 | } 243 | 244 | // lambda function (i.e useFactory) 245 | if (prop.initializer.body) { 246 | let params = (prop.initializer.parameters || []).map((params: NodeObject) => params.name.text); 247 | identifier = `(${params.join(', ')}) => {}`; 248 | } 249 | 250 | // factory deps array 251 | else if (prop.initializer.elements) { 252 | let elements = (prop.initializer.elements || []).map((n: NodeObject) => { 253 | 254 | if (n.kind === ts.SyntaxKind.StringLiteral) { 255 | return `'${n.text}'`; 256 | } 257 | 258 | return n.text; 259 | }); 260 | identifier = `[${elements.join(', ')}]`; 261 | } 262 | 263 | _providerProps.push([ 264 | 265 | // i.e provide 266 | prop.name.text, 267 | 268 | // i.e OpaqueToken or 'StringToken' 269 | identifier 270 | 271 | ].join(': ')); 272 | 273 | }); 274 | 275 | return `{ ${_providerProps.join(', ')} }`; 276 | } 277 | 278 | let parseSymbolElements = (o: NodeObject | any): string => { 279 | // parse expressions such as: AngularFireModule.initializeApp(firebaseConfig) 280 | if (o.arguments) { 281 | let className = buildIdentifierName(o.expression); 282 | 283 | // function arguments could be really complexe. There are so 284 | // many use cases that we can't handle. Just print "args" to indicate 285 | // that we have arguments. 286 | 287 | let functionArgs = o.arguments.length > 0 ? 'args' : ''; 288 | let text = `${className}(${functionArgs})`; 289 | return text; 290 | } 291 | 292 | // parse expressions such as: Shared.Module 293 | else if (o.expression) { 294 | let identifier = buildIdentifierName(o); 295 | return identifier; 296 | } 297 | 298 | return o.text ? o.text : parseProviderConfiguration(o); 299 | }; 300 | 301 | let parseSymbols = (node: NodeObject): string[] => { 302 | 303 | let text = node.initializer.text; 304 | if (text) { 305 | return [text]; 306 | } 307 | 308 | else if (node.initializer.expression) { 309 | let identifier = parseSymbolElements(node.initializer); 310 | return [ 311 | identifier 312 | ]; 313 | } 314 | 315 | else if (node.initializer.elements) { 316 | return node.initializer.elements.map(parseSymbolElements); 317 | } 318 | 319 | }; 320 | return deps.map(parseSymbols).pop() || []; 321 | } 322 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es2017", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", "dom" 17 | ] 18 | } 19 | } 20 | --------------------------------------------------------------------------------