├── .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 | 
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(""+innercomponent.selector+">")) 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(""+innercomponent.selector+">")) 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 |
--------------------------------------------------------------------------------