├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── task.json ├── .vscodeignore ├── LICENSE ├── README.md ├── __test__ └── test.ts ├── dev.md ├── icons ├── ts-128x128.png ├── ts-256x256.png ├── ts.png └── ts.svg ├── images ├── base-usage.gif └── dynamic-usage.gif ├── package.json ├── src ├── Parser.ts ├── WebView │ ├── components │ │ ├── Board.tsx │ │ ├── Content.tsx │ │ ├── Grid.tsx │ │ ├── SingleFile.tsx │ │ └── Table.tsx │ ├── index.tsx │ └── utils │ │ ├── addDragToSVGElement.ts │ │ ├── getConnectPathString.ts │ │ ├── getTransform.ts │ │ └── layout.ts ├── config.ts ├── extension.ts └── getFileDocEntries.ts ├── tsconfig.json └── webpack.config.js /.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 (https://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 | # next.js build output 61 | .next 62 | 63 | build 64 | out 65 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outDir": "${workspaceRoot}/out/src", 16 | // "preLaunchTask": "npm: watch" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsc.autoDetect": "off" 3 | } -------------------------------------------------------------------------------- /.vscode/task.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | src 2 | dev.md 3 | .vscode 4 | package 5 | images 6 | webpack.config.js 7 | tsconfig.json 8 | .gitignore 9 | **/*.ts 10 | **/tsconfig.json 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 myxvisual 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-ts-uml 2 | 3 | [![Version](https://vsmarketplacebadge.apphb.com/version/myxvisual.vscode-ts-uml.svg)](https://marketplace.visualstudio.com/items?itemName=myxvisual.vscode-ts-uml) 4 | [![Installs](https://vsmarketplacebadge.apphb.com/installs/myxvisual.vscode-ts-uml.svg)](https://marketplace.visualstudio.com/items?itemName=myxvisual.vscode-ts-uml) 5 | [![Ratings](https://vsmarketplacebadge.apphb.com/rating/myxvisual.vscode-ts-uml.svg)](https://marketplace.visualstudio.com/items?itemName=myxvisual.vscode-ts-uml) 6 | 7 | [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/myxvisual/vscode-ts-uml.svg)](https://github.com/myxvisual/vscode-ts-uml/issues "Average time to resolve an issue") 8 | [![Percentage of issues still open](https://isitmaintained.com/badge/open/myxvisual/vscode-ts-uml.svg)](https://github.com/myxvisual/vscode-ts-uml/issues?q=is%3Aopen+is%3Aissue "Percentage of issues still open") 9 | 10 | --- 11 | 12 | Dynamic generates TypeScript UML diagrams to [Visual Studio Code](https://code.visualstudio.com/) (**minimum supported version: `1.25.0`**) 13 | 14 | Supported filename extensions: `ts`, `tsx`, `.d.ts`, only supported single file now. 15 | 16 | ![base-usage](https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/master/images/base-usage.gif) 17 | 18 | ![dynamic-usage](https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/master/images/dynamic-usage.gif) 19 | 20 | --- 21 | 22 | ## Installation 23 | 24 | To install the extension just execute the following command: 25 | 26 | ```sh 27 | ext install vscode-ts-uml 28 | ``` 29 | 30 | ## Usage 31 | 32 | After reloading `vscode`. 33 | 34 | * RightClick `.ts`, `.tsx`, `.d.ts` files, select `Show Typescript UML from file` menu to open `vscode-ts-uml` window. 35 | * Use shortkey `Ctrl + Alt + Shift + Q` to select current editor focus file and open `vscode-ts-uml` window. 36 | 37 | ## License 38 | 39 | The source code is licensed under the [MIT](https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/master/LICENSE) license. 40 | 41 | The icons are licensed under the [Creative Commons - ShareAlike (CC BY-SA)](https://creativecommons.org/licenses/by-sa/4.0/) license. 42 | 43 | Branded icons are licensed under their copyright license. 44 | 45 | 46 | ## Change Log 47 | 48 | You can checkout all our changes in our [change log](https://github.com/vscode-icons/vscode-icons/blob/master/CHANGELOG.md). 49 | 50 | **Enjoy!** 51 | -------------------------------------------------------------------------------- /__test__/test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { getFileDocEntries, Config } from "../src/getFileDocEntries"; 3 | 4 | export const testConfig: Config = { 5 | getAllLocalMembers: true, 6 | showType: "member", 7 | fileMaxDepth: 0, 8 | tableStyle: { 9 | itemPadding: 12, 10 | textPadding: 20, 11 | headerHeight: 36, 12 | itemHeight: 28, 13 | headerFontSize: 14, 14 | itemFontSize: 12 15 | }, 16 | fileStyle: { 17 | widthPadding: 8, 18 | heightPadding: 16, 19 | headerHeight: 24, 20 | headerFontSize: 12, 21 | tableOffset: 48, 22 | fileOffset: 40 23 | }, 24 | connectPathStyle: { 25 | color: "#333", 26 | strokeDasharray: "4 2", 27 | arrowSize: 6 28 | }, 29 | contentStyle: { 30 | background: "#e5e5e5", 31 | padding: 24 32 | }, 33 | theme: { 34 | accent: 0 ? "red" : "#005aa0" 35 | }, 36 | maxShowTypeLength: 20 37 | }; 38 | 39 | const testFile = path.join(__dirname, "../src/WebView/components/Board.tsx") 40 | const result = getFileDocEntries(testFile, 0, testConfig) 41 | 42 | console.log(result) 43 | -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | # Dev 2 | ## Run the hot compiler 3 | ```sh 4 | npm run watch 5 | npm run webpack-w 6 | ``` 7 | ## Debugger extension 8 | - open current project 9 | - run vscode launch (in current workplace, press (`F5`) keyboard). 10 | 11 | # Publishing 12 | ```sh 13 | npm install -g vsce 14 | vsce login myxvisual 15 | vsce publish 16 | ``` 17 | 18 | [working-with-extensions/publishing-extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) 19 | [Azure DevOps Organization](https://aex.dev.azure.com/me?mkt=en-US) 20 | -------------------------------------------------------------------------------- /icons/ts-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/b1b5c889e7add1e3c9a6f5a3d65bf17d2658ccd2/icons/ts-128x128.png -------------------------------------------------------------------------------- /icons/ts-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/b1b5c889e7add1e3c9a6f5a3d65bf17d2658ccd2/icons/ts-256x256.png -------------------------------------------------------------------------------- /icons/ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/b1b5c889e7add1e3c9a6f5a3d65bf17d2658ccd2/icons/ts.png -------------------------------------------------------------------------------- /icons/ts.svg: -------------------------------------------------------------------------------- 1 | 2 | 25 | 27 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /images/base-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/b1b5c889e7add1e3c9a6f5a3d65bf17d2658ccd2/images/base-usage.gif -------------------------------------------------------------------------------- /images/dynamic-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myxvisual/vscode-ts-uml/b1b5c889e7add1e3c9a6f5a3d65bf17d2658ccd2/images/dynamic-usage.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-ts-uml", 3 | "displayName": "vscode-ts-uml", 4 | "description": "Dynamic generates TypeScript UML diagrams to VSCode", 5 | "version": "1.0.4", 6 | "publisher": "myxvisual", 7 | "author": { 8 | "name": "myxvisual", 9 | "email": "myxvisual@live.com" 10 | }, 11 | "icon": "icons/ts-128x128.png", 12 | "galleryBanner": { 13 | "color": "#005aa0", 14 | "theme": "dark" 15 | }, 16 | "homepage": "https://github.com/myxvisual/vscode-ts-uml/blob/master/README.md", 17 | "repository": { 18 | "git": "git@github.com:myxvisual/vscode-ts-uml.git", 19 | "http": "https://github.com/myxvisual/vscode-ts-uml.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/myxvisual/vscode-ts-uml/issues", 23 | "email": "myxvisual@live.com" 24 | }, 25 | "engines": { 26 | "vscode": "^1.25.0" 27 | }, 28 | "categories": [ 29 | "Other" 30 | ], 31 | "activationEvents": [ 32 | "*", 33 | "onCommand:tsUML.showFile", 34 | "onCommand:tsUML.showFolder", 35 | "onWebviewPanel:tsUML" 36 | ], 37 | "main": "./out/src/extension", 38 | "contributes": { 39 | "commands": [ 40 | { 41 | "command": "tsUML.showFile", 42 | "title": "Show Typescript UML from file" 43 | }, 44 | { 45 | "command": "tsUML.showFolder", 46 | "title": "Show Typescript UML from folder" 47 | } 48 | ], 49 | "menus": { 50 | "explorer/context": [ 51 | { 52 | "command": "tsUML.showFile", 53 | "when": "resourceLangId == typescript" 54 | }, 55 | { 56 | "command": "tsUML.showFile", 57 | "when": "resourceLangId == typescriptreact" 58 | }, 59 | { 60 | "command": "tsUML.showFolder", 61 | "when": "explorerResourceIsFolder" 62 | } 63 | ] 64 | }, 65 | "keybindings": [ 66 | { 67 | "key": "ctrl+shift+alt+q", 68 | "mac": "cmd+shift+alt+q", 69 | "command": "tsUML.showFile", 70 | "when": "editorTextFocus && editorLangId == typescript" 71 | }, 72 | { 73 | "key": "ctrl+shift+alt+q", 74 | "mac": "cmd+shift+alt+q", 75 | "command": "tsUML.showFile", 76 | "when": "editorFocus && editorLangId == typescriptreact" 77 | } 78 | ] 79 | }, 80 | "scripts": { 81 | "start": "npm run watch | npm run webpack-w", 82 | "webpack-w": "cross-env NODE_ENV=development webpack -w", 83 | "watch": "cross-env NODE_ENV=development tsc -w -p ./", 84 | "vscode:prepublish": "npm run compile && npm run webpack-p", 85 | "compile": "tsc -p ./", 86 | "webpack-p": "cross-env NODE_ENV=production webpack", 87 | "postinstall": "node ./node_modules/vscode/bin/install", 88 | "package:vsce": "vsce package", 89 | "publish:vsce": "vsce publish" 90 | }, 91 | "dependencies": { 92 | "md5": "^2.2.1", 93 | "typescript": "^3.0.1" 94 | }, 95 | "devDependencies": { 96 | "@types/node": "^10.5.2", 97 | "@types/react": "^16.4.13", 98 | "@types/react-dom": "^16.0.7", 99 | "awesome-typescript-loader": "^5.2.1", 100 | "cross-env": "^5.2.0", 101 | "json-loader": "^0.5.7", 102 | "react": "^16.5.0", 103 | "react-dom": "^16.5.0", 104 | "react-hot-loader": "^4.3.3", 105 | "react-uwp": "^1.2.2", 106 | "vscode": "^1.1.22", 107 | "webpack": "^4.21.0", 108 | "webpack-cli": "^3.1.2", 109 | "webpack-dev-middleware": "^1.10.1", 110 | "webpack-dev-server": "^3.1.9", 111 | "webpack-hot-middleware": "^2.17.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Parser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | export interface DocEntry { 4 | filename?: string; 5 | exportMembers?: string[]; 6 | escapedName?: string; 7 | name?: string; 8 | type?: string; 9 | locals?: DocEntry[]; 10 | exports?: DocEntry[]; 11 | members?: DocEntry[]; 12 | resolvedModules?: { 13 | name?: string; 14 | resolvedFileName: string; 15 | isExternalLibraryImport?: boolean; 16 | }[]; 17 | 18 | comment?: string; 19 | constructors?: DocEntry[]; 20 | isRequired?: boolean; 21 | documentation?: string; 22 | parameters?: DocEntry[]; 23 | returnType?: string; 24 | extends?: DocEntry[] | string[]; 25 | valueDeclarationText?: string; 26 | initializerText?: string; 27 | } 28 | 29 | const customType: any = { 30 | "8388608": "React", 31 | "16777220": "prototype" 32 | }; 33 | 34 | export class Parser { 35 | constructor(options?: ts.CompilerOptions, host?: ts.CompilerHost) { 36 | const defaultOptions: ts.CompilerOptions = { 37 | target: ts.ScriptTarget.ES5, 38 | maxNodeModuleJsDepth: 1, 39 | module: ts.ModuleKind.CommonJS 40 | }; 41 | this.options = options || defaultOptions; 42 | this.host = host; 43 | } 44 | 45 | getAllLocalMembers = false; 46 | 47 | private rootNames: string[]; 48 | private options: ts.CompilerOptions; 49 | private host: ts.CompilerHost; 50 | 51 | private program: ts.Program; 52 | private checker: ts.TypeChecker; 53 | private sourceFiles: ts.SourceFile[]; 54 | private currSourceFile: ts.SourceFile; 55 | private output: DocEntry = {}; 56 | getResultCallback: (result: DocEntry) => void; 57 | 58 | parse = (fileName: string | string[], callback = (result?: DocEntry) => { }) => { 59 | const rootNames = Array.isArray(fileName) ? fileName : [fileName]; 60 | this.rootNames = rootNames; 61 | this.program = ts.createProgram(rootNames, this.options, this.host); 62 | this.checker = this.program.getTypeChecker(); 63 | this.sourceFiles = this.program.getSourceFiles() as ts.SourceFile[]; 64 | 65 | for (const fileName of this.rootNames) { 66 | this.currSourceFile = this.program.getSourceFile(fileName); 67 | this.visit(this.currSourceFile); 68 | // ts.forEachChild(this.currSourceFile, this.visit); 69 | } 70 | if (this.output.members) { 71 | this.output.members = this.output.members.filter(member => member); 72 | } 73 | callback(this.output); 74 | return this.output; 75 | } 76 | 77 | visit = (node: ts.Node) => { 78 | if (!node) return; 79 | let symbol: ts.Symbol = null; 80 | switch (node.kind) { 81 | case ts.SyntaxKind.SourceFile: { 82 | symbol = this.getSymbolByType(node as ts.SourceFile); 83 | if (!symbol) { 84 | symbol = this.currSourceFile as any; 85 | } 86 | this.output.filename = (node as ts.SourceFile).fileName; 87 | break; 88 | } 89 | case ts.SyntaxKind.ClassDeclaration: { 90 | // const t = node as ts.ClassDeclaration; 91 | symbol = this.getSymbolByType(node as ts.ClassDeclaration); 92 | break; 93 | } 94 | case ts.SyntaxKind.InterfaceDeclaration: { 95 | symbol = this.getSymbolByType(node as ts.InterfaceDeclaration); 96 | break; 97 | } 98 | case ts.SyntaxKind.FunctionDeclaration: { 99 | symbol = this.getSymbolByType(node as ts.FunctionDeclaration); 100 | break; 101 | } 102 | case ts.SyntaxKind.MethodDeclaration: { 103 | symbol = this.getSymbolByType(node as ts.MethodDeclaration); 104 | break; 105 | } 106 | case ts.SyntaxKind.PropertyDeclaration: { 107 | symbol = this.getSymbolByType(node as ts.PropertyDeclaration); 108 | break; 109 | } 110 | case ts.SyntaxKind.EnumDeclaration: { 111 | symbol = this.getSymbolByType(node as ts.EnumDeclaration); 112 | break; 113 | } 114 | case ts.SyntaxKind.ImportDeclaration: { 115 | symbol = this.getSymbolByType(node as ts.ImportDeclaration); 116 | break; 117 | } 118 | case ts.SyntaxKind.VariableDeclaration: { 119 | symbol = this.getSymbolByType(node as ts.VariableDeclaration); 120 | break; 121 | } 122 | case ts.SyntaxKind.VariableStatement: { 123 | symbol = this.getSymbolByType(node as ts.VariableStatement); 124 | break; 125 | } 126 | case ts.SyntaxKind.ExportAssignment: { 127 | symbol = this.getSymbolByType(node as ts.ExportAssignment); 128 | break; 129 | } 130 | case ts.SyntaxKind.EndOfFileToken: { 131 | symbol = this.getSymbolByType(node as ts.EndOfFileToken); 132 | break; 133 | } 134 | default: { 135 | // console.log(`Missing parse kind: ${node.kind}`); 136 | break; 137 | } 138 | } 139 | 140 | if (node.kind === ts.SyntaxKind.SourceFile) { 141 | const result = this.serializeSymbol(symbol); 142 | Object.assign(this.output, result); 143 | } else { 144 | const result = this.serializeSymbol(symbol); 145 | if (this.getResultCallback) { 146 | this.getResultCallback(result); 147 | this.getResultCallback = void 0; 148 | } 149 | if (result && !this.getAllLocalMembers) { 150 | this.output.members = [...(this.output.members || []), result]; 151 | } 152 | } 153 | } 154 | 155 | getSymbolByType = (declaration: T | ts.SourceFile) => { 156 | return this.checker.getSymbolAtLocation(declaration["name"]) || this.checker.getSymbolAtLocation(declaration as ts.SourceFile); 157 | } 158 | 159 | serializeSymbol = (symbol: ts.Symbol, getAllAst = true): DocEntry => { 160 | if (!symbol || typeof symbol !== "object") { 161 | return; 162 | } 163 | 164 | let name = symbol.getName ? symbol.getName() : symbol.name; 165 | let docEntryFilename: string; 166 | let escapedName: string; 167 | 168 | 169 | let initializerText: string; 170 | if (symbol.valueDeclaration) { 171 | const initializer = symbol.valueDeclaration["initializer"]; 172 | if (initializer) { 173 | initializerText = initializer.getFullText(); 174 | } 175 | } 176 | 177 | let valueDeclarationText: string; 178 | if (symbol.valueDeclaration && symbol.valueDeclaration.getFullText) { 179 | valueDeclarationText = symbol.valueDeclaration.getFullText(); 180 | } 181 | 182 | let type: string; 183 | try { 184 | type = this.checker.typeToString(this.checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)); 185 | } catch (e) { 186 | // console.error(e); 187 | type = "unknown"; 188 | } 189 | if (!type || type === "any") { 190 | type = ts.SymbolFlags[symbol.flags] || "any"; 191 | } 192 | const isSourceFile = Boolean( 193 | symbol.flags === ts.SymbolFlags.ValueModule || ( 194 | symbol["kind"] && symbol["kind"] === ts.SyntaxKind.SourceFile 195 | ) 196 | ); 197 | 198 | const isNamseSpace = symbol.flags === ts.SymbolFlags.NamespaceModule; 199 | 200 | let documentation: string; 201 | try { 202 | documentation = ts.displayPartsToString(symbol.getDocumentationComment(void 0)); 203 | } catch (e) { 204 | // console.error(e); 205 | } 206 | 207 | let isRequired: boolean; 208 | const parentSymbol: ts.Symbol = (symbol as any).parent; 209 | if (parentSymbol && parentSymbol.flags === ts.SymbolFlags.Interface) { 210 | const valueDeclaration: any = symbol.valueDeclaration; 211 | isRequired = valueDeclaration ? !valueDeclaration.questionToken : false; 212 | } 213 | 214 | if (symbol.flags === ts.SymbolFlags.AliasExcludes || 215 | symbol.flags === 2097152 // ts.SymbolFlags.Alias 216 | ) { 217 | const aliasSymbol = this.checker.getAliasedSymbol(symbol); 218 | escapedName = aliasSymbol.escapedName.toString(); 219 | if (aliasSymbol["parent"]) { 220 | docEntryFilename = aliasSymbol["parent"].valueDeclaration.fileName; 221 | } 222 | } 223 | 224 | if (symbol.flags === ts.SymbolFlags.Property) { 225 | const docEntry: DocEntry = { 226 | name, 227 | isRequired 228 | }; 229 | 230 | docEntry.type = this.checker.typeToString(this.checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)); 231 | docEntry.documentation = ts.displayPartsToString(symbol.getDocumentationComment(void 0)); 232 | docEntry.initializerText = initializerText; 233 | docEntry.valueDeclarationText = valueDeclarationText; 234 | docEntry.documentation = docEntry.documentation ? docEntry.documentation : void 0; 235 | return docEntry; 236 | } 237 | 238 | 239 | let extendsDocEntry: DocEntry[] = []; 240 | let exportsDocEntry: DocEntry[]; 241 | let membersDocEntry: DocEntry[]; 242 | 243 | if (symbol.flags === ts.SymbolFlags.Class || symbol.flags === ts.SymbolFlags.Interface) { 244 | symbol.declarations.forEach((declaration, index) => { 245 | if (declaration["heritageClauses"]) { 246 | const firstHeritageClause = declaration["heritageClauses"]![0]; 247 | firstHeritageClause.types.forEach((type, index) => { 248 | const firstHeritageClauseType = firstHeritageClause.types![index]; 249 | const extendsSymbol = this.checker.getSymbolAtLocation(firstHeritageClauseType.expression); 250 | extendsDocEntry.push(this.serializeSymbol(extendsSymbol, false)); 251 | }); 252 | } 253 | }); 254 | } 255 | 256 | if ("exports" in symbol && symbol.exports.size) { 257 | exportsDocEntry = []; 258 | const values = symbol.exports.values(); 259 | for (let i = 0; i < symbol.exports.size; i++) { 260 | const result: any = values.next(); 261 | exportsDocEntry.push(this.serializeSymbol(result.value, isNamseSpace)); 262 | } 263 | } 264 | 265 | if (isSourceFile || ( 266 | getAllAst && (symbol.flags === ts.SymbolFlags.Class || symbol.flags === ts.SymbolFlags.Interface) 267 | )) { 268 | if (isSourceFile && this.getAllLocalMembers) { 269 | membersDocEntry = []; 270 | } else if ("members" in symbol && symbol.members.size) { 271 | membersDocEntry = []; 272 | const values = symbol.members.values(); 273 | for (let i = 0; i < symbol.members.size; i++) { 274 | const result = values.next(); 275 | const docEntry = this.serializeSymbol(result.value, isSourceFile ? false : (isNamseSpace ? false : true)); 276 | membersDocEntry.push(docEntry); 277 | } 278 | } 279 | } 280 | 281 | const docEntry: DocEntry = { 282 | name, 283 | escapedName, 284 | exports: isNamseSpace ? membersDocEntry : exportsDocEntry, 285 | members: isNamseSpace ? exportsDocEntry : membersDocEntry, 286 | type, 287 | isRequired, 288 | documentation: documentation ? documentation : void 0, 289 | extends: extendsDocEntry.length > 0 ? extendsDocEntry : void 0, 290 | filename: isSourceFile ? (symbol["fileName"] || symbol.valueDeclaration["resolvedPath"]) : docEntryFilename, 291 | initializerText, 292 | valueDeclarationText 293 | }; 294 | 295 | if (isSourceFile) { 296 | if (symbol.valueDeclaration) { 297 | const resolvedModules = symbol.valueDeclaration["resolvedModules"]; 298 | if (resolvedModules) { 299 | docEntry.resolvedModules = []; 300 | for (const moduleNode of resolvedModules) { 301 | const data = moduleNode[1]; 302 | docEntry.resolvedModules.push({ 303 | name: moduleNode[0], 304 | resolvedFileName: data ? data.resolvedFileName : void 0, 305 | isExternalLibraryImport: data ? data.isExternalLibraryImport : void 0 306 | }); 307 | } 308 | } 309 | } 310 | docEntry.locals = []; 311 | if (this.getAllLocalMembers) { 312 | docEntry.members = []; 313 | } 314 | 315 | docEntry.exportMembers = []; 316 | let locals = symbol["locals"]; 317 | if (!locals && symbol.valueDeclaration) { 318 | locals = symbol.valueDeclaration["locals"]; 319 | } 320 | for (const local of locals) { 321 | const isExportMember = Boolean(local[1]["exportSymbol"]); 322 | const symbol: ts.Symbol = isExportMember ? local[1]["exportSymbol"] : local[1]; 323 | if (isExportMember) { 324 | let name = symbol.name; 325 | if (!name && symbol.getName) { 326 | name = symbol.getName(); 327 | } 328 | if (name) { 329 | docEntry.exportMembers.push(name); 330 | } 331 | } 332 | docEntry.locals.push(this.serializeSymbol(symbol, false)); 333 | if (this.getAllLocalMembers) { 334 | docEntry.members.push(this.serializeSymbol(symbol, this.getAllLocalMembers)); 335 | } 336 | } 337 | } 338 | return docEntry; 339 | } 340 | } 341 | 342 | export default Parser; -------------------------------------------------------------------------------- /src/WebView/components/Board.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import Content, { DocEntry } from "./Content"; 4 | import { getScale, getTranslate } from "../utils/getTransform"; 5 | import { setContentLayout } from "../utils/layout"; 6 | import Grid from './Grid'; 7 | import { config, Config } from "../../config"; 8 | import { Theme, getTheme } from "react-uwp/Theme"; 9 | import Button from 'react-uwp/Button'; 10 | import CheckBox from 'react-uwp/CheckBox'; 11 | export { Config }; 12 | 13 | 14 | export interface Theme { 15 | accent?: string; 16 | } 17 | export interface Position { 18 | x: number; 19 | y: number; 20 | } 21 | 22 | const gridConfig = { 23 | gridWidth: window.innerWidth, 24 | gridHeight: window.innerHeight, 25 | lineColor: "#000", 26 | gridSize: 100, 27 | griItemCount: 10, 28 | scale: 1 29 | } 30 | export interface DataProps { 31 | staticContext?: any; 32 | } 33 | 34 | export interface BoardProps extends DataProps, React.HTMLAttributes, Config {} 35 | 36 | export interface BoardState { 37 | config?: Config; 38 | fileDocEntriesStr?: string; 39 | fileDocEntries?: DocEntry[]; 40 | } 41 | 42 | export class Board extends React.Component { 43 | context: { 44 | config: Config 45 | }; 46 | showScaleEl: HTMLSpanElement; 47 | state: BoardState = { 48 | config, 49 | fileDocEntries: [] 50 | } 51 | mouseStatPosition: { 52 | x?: number; 53 | y?: number 54 | } = {}; 55 | originTransform: { 56 | x?: number; 57 | y?: number; 58 | scale?: number; 59 | } = { 60 | x: 0, 61 | y: 0, 62 | scale: 1 63 | }; 64 | screenMatrix: SVGMatrix; 65 | content: Content; 66 | 67 | static childContextTypes = { 68 | config: PropTypes.object 69 | }; 70 | 71 | getChildContext = () => { 72 | return { 73 | config: this.state.config 74 | }; 75 | } 76 | 77 | static defaultProps = { 78 | style: { 79 | width: window.innerWidth, 80 | height: window.innerHeight 81 | } 82 | }; 83 | rootEl: SVGElement; 84 | grid: Grid; 85 | 86 | componentWillMount() { 87 | window.vscode.postMessage({ boardWillMount: true }); 88 | window.addEventListener("message", this.handleMessage); 89 | const parentEl = document.querySelector("body"); 90 | parentEl.style.overflow = "hidden"; 91 | parentEl.style.margin = "0"; 92 | } 93 | 94 | handleMessage = (event: any) => { 95 | const message = event.data; // The JSON data our extension sent 96 | 97 | if (message.docEntries) { 98 | this.setState({ fileDocEntries: message.docEntries }); 99 | } 100 | } 101 | 102 | componentDidMount() { 103 | window.addEventListener("resize", this.resize); 104 | this.rootEl.addEventListener("wheel", this.handleWheel); 105 | document.body.querySelectorAll("div").forEach(divEl => { 106 | if (divEl.className.includes("fluent-background")) { 107 | divEl.style.background = "none"; 108 | } 109 | }) 110 | } 111 | 112 | componentWillUnmount() { 113 | window.removeEventListener("message", this.handleMessage); 114 | window.removeEventListener("resize", this.resize); 115 | this.rootEl.removeEventListener("wheel", this.handleWheel); 116 | } 117 | 118 | handleWheel = (e: WheelEvent) => { 119 | if (!this.content) return; 120 | const el = this.content.rootEl; 121 | if (e.ctrlKey) { 122 | this.originTransform.scale = Number((this.originTransform.scale + e.deltaY * .001).toFixed(2)); 123 | if (this.originTransform.scale < .01) { 124 | this.originTransform.scale = .01; 125 | } 126 | el.setAttributeNS(null, "transform", `translate(${this.originTransform.x}, ${this.originTransform.y}), scale(${this.originTransform.scale})`); 127 | this.showScaleEl.innerText = `${Math.ceil(this.originTransform.scale * 100)}%`; 128 | setContentLayout({ x: this.originTransform.x, y: this.originTransform.y }, this.originTransform.scale); 129 | } else { 130 | this.setOriginTransform(); 131 | const x = this.originTransform.x - e.deltaX; 132 | const y = this.originTransform.y - e.deltaY; 133 | el.setAttributeNS(null, "transform", `translate(${x}, ${y}), scale(${this.originTransform.scale})`); 134 | setContentLayout({ x, y }, this.originTransform.scale); 135 | } 136 | } 137 | 138 | updateScale = (offsetScale: number) => { 139 | if (!this.content) return; 140 | const el = this.content.rootEl; 141 | this.originTransform.scale = Number((this.originTransform.scale + offsetScale).toFixed(2)); 142 | if (this.originTransform.scale < .01) { 143 | this.originTransform.scale = .01; 144 | } 145 | el.setAttributeNS(null, "transform", `translate(${this.originTransform.x}, ${this.originTransform.y}), scale(${this.originTransform.scale})`); 146 | this.showScaleEl.innerText = `${Math.ceil(this.originTransform.scale * 100)}%`; 147 | setContentLayout({ x: this.originTransform.x, y: this.originTransform.y }, this.originTransform.scale); 148 | } 149 | 150 | resize = (e: Event) => { 151 | const { innerHeight, innerWidth } = window; 152 | this.rootEl.style.height = `${innerHeight}px`; 153 | this.rootEl.style.width = `${innerWidth}px`; 154 | if (this.grid) { 155 | this.grid.setState({ 156 | currGridWidth: innerWidth, 157 | currGridHeight: innerHeight 158 | }); 159 | } 160 | } 161 | 162 | setOriginTransform = () => { 163 | if (!this.content) return; 164 | const el = this.content.rootEl; 165 | this.screenMatrix = el.getScreenCTM(); 166 | const transform = el.getAttributeNS(null, "transform"); 167 | const translate = getTranslate(transform); 168 | const scale = getScale(transform); 169 | if (translate) { 170 | Object.assign(this.originTransform, translate); 171 | } 172 | if (scale !== null) { 173 | this.originTransform.scale = scale; 174 | } 175 | } 176 | 177 | handleMouseDown = (e: React.MouseEvent) => { 178 | this.setOriginTransform(); 179 | 180 | this.mouseStatPosition.x = e.clientX / this.screenMatrix.a; 181 | this.mouseStatPosition.y = e.clientY / this.screenMatrix.d; 182 | 183 | document.documentElement.addEventListener("mousemove", this.handleMouseMove); 184 | document.documentElement.addEventListener("mouseup", this.handleMouseUp); 185 | } 186 | 187 | handleMouseMove = (e: MouseEvent) => { 188 | if (!this.content) return; 189 | const el = this.content.rootEl; 190 | const offsetX = e.clientX / this.screenMatrix.a - this.mouseStatPosition.x; 191 | const offsetY = e.clientY / this.screenMatrix.d - this.mouseStatPosition.y; 192 | if (this.originTransform) { 193 | const x = this.originTransform.x + offsetX; 194 | const y = this.originTransform.y + offsetY; 195 | el.setAttributeNS(null, "transform", `translate(${x}, ${y}), scale(${this.originTransform.scale})`); 196 | 197 | setContentLayout({ x, y }, this.originTransform.scale); 198 | } 199 | } 200 | 201 | handleMouseUp = (e: MouseEvent | React.MouseEvent) => { 202 | document.documentElement.removeEventListener("mousemove", this.handleMouseMove); 203 | document.documentElement.removeEventListener("mouseup", this.handleMouseUp); 204 | } 205 | 206 | render() { 207 | const { staticContext, ...attributes } = this.props; 208 | const { connectPathStyle, contentStyle, theme, showType } = this.getChildContext().config as Config; 209 | const { arrowSize, color } = connectPathStyle; 210 | 211 | const { fileDocEntries } = this.state; 212 | 213 | return ( 214 |
215 | this.rootEl = rootEl} 218 | onMouseDown={this.handleMouseDown} 219 | onMouseUp={this.handleMouseUp} 220 | fill={contentStyle.background} 221 | > 222 | 223 | 231 | 232 | 233 | 234 | {fileDocEntries && fileDocEntries.length > 0 && this.content = content} fileDocEntries={fileDocEntries} />} 235 | 236 | 237 | {/* this.grid = grid} /> */} 238 | 239 |
240 | 252 |
253 | { 255 | this.setState((prevState) => { 256 | prevState.config.showType = config.showType === "member" ? "export" : "member"; 257 | this.showScaleEl.innerText = "100%"; 258 | return prevState; 259 | }) 260 | }} 261 | style={{ marginBottom: 16 }} 262 | key={config.showType} 263 | defaultChecked={config.showType === "export"} 264 | label="Only Export" 265 | /> 266 |
267 |
268 | 271 | this.showScaleEl = showScaleEl} 273 | style={{ 274 | margin: "0 8px", 275 | display: "inline-block", 276 | width: 60, 277 | textAlign: "center" 278 | }} 279 | > 280 | {Math.ceil(this.originTransform.scale * 100)}% 281 | 282 | 285 |
286 |
287 |
288 | 289 |
290 | ); 291 | } 292 | } 293 | 294 | export default Board; 295 | -------------------------------------------------------------------------------- /src/WebView/components/Content.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import { DocEntry } from "../../getFileDocEntries"; 4 | import SingleFile from "./SingleFile"; 5 | import { Config } from "./Board"; 6 | import { getConnectPathString } from "../utils/getConnectPathString"; 7 | import { getLayout, getFilePosition, setFilePosition, BoardLayout } from "../utils/layout"; 8 | export interface Position { 9 | x: number; 10 | y: number; 11 | } 12 | export interface Line { 13 | outputPosition: Position; 14 | toMember: { 15 | filename: string; 16 | memberName: string; 17 | }; 18 | } 19 | export interface DocEntryDetail { 20 | lines?: Line[]; 21 | inputPosition: Position; 22 | } 23 | 24 | export { DocEntry }; 25 | 26 | export interface ContentProps extends DataProps, React.SVGAttributes {} 27 | 28 | export interface DataProps { 29 | fileDocEntries?: DocEntry[]; 30 | } 31 | export interface ContentState { 32 | layout?: BoardLayout; 33 | } 34 | 35 | export interface Position { 36 | x: number; 37 | y: number; 38 | } 39 | export interface TypeOut { 40 | leftPosition?: Position; 41 | rightPosition?: Position; 42 | toFileType: { 43 | isLocal?: boolean; 44 | type?: string; 45 | escapedName?: string; 46 | filename?: string; 47 | }; 48 | } 49 | export interface TypeIn { 50 | name?: string; 51 | leftPosition?: Position; 52 | rightPosition?: Position; 53 | type: string; 54 | filename: string; 55 | } 56 | 57 | export class Content extends React.Component { 58 | static contextTypes = { config: PropTypes.object }; 59 | context: { config: Config }; 60 | rootEl: SVGGElement; 61 | singleFiles: SingleFile[] = []; 62 | pathGroupEl: SVGGElement; 63 | 64 | componentWillMount() { 65 | this.readAllFilesByDepth(); 66 | } 67 | 68 | readAllFilesByDepth = () => { 69 | 70 | } 71 | 72 | componentDidMount() { 73 | this.drawLines(false); 74 | } 75 | 76 | componentDidUpdate() { 77 | this.drawLines(false); 78 | } 79 | 80 | _isMounted = false; 81 | drawLines = (isCallback: any = true) => { 82 | if (isCallback) { 83 | if (!this._isMounted) return; 84 | } 85 | const { fileDocEntries } = this.props; 86 | 87 | while (this.pathGroupEl.firstChild) { 88 | this.pathGroupEl.removeChild(this.pathGroupEl.firstChild); 89 | } 90 | const allTypeInOuts = this.singleFiles.map((singleFile, index) => { 91 | const typeInOuts = singleFile.getTypeInOuts(); 92 | return typeInOuts; 93 | }); 94 | allTypeInOuts.forEach((typeInOuts, index) => { 95 | typeInOuts.forEach((typeInOut, x) => { 96 | const { typeOuts } = typeInOut; 97 | typeOuts.forEach((typeOut, y) => { 98 | const { leftPosition, rightPosition, toFileType } = typeOut; 99 | const { isLocal, filename, escapedName } = toFileType; 100 | if (isLocal) { 101 | const typeInOutSize = typeInOuts.length; 102 | for (let z = 0; z < typeInOutSize; z++) { 103 | const { typeIn } = typeInOuts[z]; 104 | if (escapedName === typeInOut.typeIn.name) { 105 | this.insertPath(leftPosition, typeInOut.typeIn.leftPosition); 106 | } else if (typeIn.name === escapedName) { 107 | // const isLtr = leftPosition.x < typeIn.leftPosition.x; 108 | // this.insertPath(isLtr ? rightPosition : leftPosition, isLtr ? typeIn.leftPosition : typeIn.rightPosition); 109 | this.insertPath(leftPosition, typeIn.rightPosition); 110 | break; 111 | } 112 | } 113 | } else if (filename) { 114 | const allFileSize = fileDocEntries.length; 115 | for (let fileIndex = 0; fileIndex < allFileSize; fileIndex ++) { 116 | if (fileDocEntries[fileIndex].filename === filename) { 117 | const typeInOuts = allTypeInOuts[fileIndex]; 118 | const typeInOutSize = typeInOuts.length; 119 | for (let z = 0; z < typeInOutSize; z++) { 120 | const { typeIn } = typeInOuts[z]; 121 | if (typeIn.name === escapedName) { 122 | // const isLtr = leftPosition.x < typeIn.leftPosition.x; 123 | // const isMiddle = (rightPosition.x < typeIn.rightPosition.x && rightPosition.x > typeIn.leftPosition.x) || ( 124 | // typeIn.rightPosition.x < rightPosition.x && typeIn.rightPosition.x > leftPosition.x 125 | // ); 126 | // if (isMiddle) { 127 | // this.insertPath(rightPosition, typeIn.rightPosition); 128 | // } else { 129 | // this.insertPath(isLtr ? rightPosition : leftPosition, isLtr ? typeIn.leftPosition : typeIn.rightPosition); 130 | // } 131 | 132 | this.insertPath(leftPosition, typeIn.rightPosition); 133 | break; 134 | } 135 | } 136 | break; 137 | } 138 | } 139 | } 140 | }); 141 | }); 142 | }); 143 | this._isMounted = true; 144 | } 145 | 146 | insertPath = (startPosition: Position, endPosition: Position) => { 147 | const { color, strokeDasharray } = this.context.config.connectPathStyle; 148 | const svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); 149 | svgPath.setAttributeNS(null, "d", getConnectPathString(startPosition, endPosition)); 150 | svgPath.setAttributeNS(null, "stroke", color); 151 | svgPath.setAttributeNS(null, "stroke-opacity", ".5"); 152 | svgPath.setAttributeNS(null, "stroke-dasharray", strokeDasharray); 153 | svgPath.setAttributeNS(null, "marker-start", "url(#arrowStart)"); 154 | // svgPath.setAttributeNS(null, "marker-end", "url(#arrowStart)"); 155 | svgPath.setAttributeNS(null, "fill", "none"); 156 | this.pathGroupEl.appendChild(svgPath); 157 | } 158 | 159 | originDocEntrie: DocEntry[] = [...this.props.fileDocEntries]; 160 | 161 | render() { 162 | const { fileDocEntries, children, ...attributes } = this.props; 163 | 164 | for (const docEntry of fileDocEntries) { 165 | if (this.context.config.showType === "export") { 166 | docEntry["originMembers"] = docEntry.members; 167 | docEntry.members = docEntry.members.filter(member => docEntry.exportMembers.includes(member.name)); 168 | } else { 169 | if (docEntry["originMembers"]) { 170 | docEntry.members = docEntry["originMembers"]; 171 | } 172 | } 173 | } 174 | this.singleFiles = []; 175 | const layout = getLayout(fileDocEntries, this.context.config); 176 | const contentLayout = { position: layout.contentPosition, scale: layout.contentScale }; 177 | 178 | return ( 179 | this.rootEl = rootEl} 183 | > 184 | {fileDocEntries && fileDocEntries.map((fileDocEntry, index) => { 185 | const position = getFilePosition(fileDocEntry.filename); 186 | return ( 187 | this.singleFiles[index] = singleFile} 190 | key={index} 191 | fileWidth={fileDocEntry.fileWidth} 192 | transform={`translate(${position.x}, ${position.y})`} 193 | onChangeView={position => { 194 | this.drawLines(true); 195 | setFilePosition(fileDocEntry.filename, position); 196 | }} 197 | /> 198 | ); 199 | })} 200 | this.pathGroupEl = pathGroupEl} /> 201 | 202 | ); 203 | } 204 | } 205 | 206 | export default Content; 207 | -------------------------------------------------------------------------------- /src/WebView/components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface DataProps { 4 | gridWidth: number; 5 | gridHeight: number; 6 | lineColor: string; 7 | gridSize: number; 8 | griItemCount?: number; 9 | scale: number; 10 | } 11 | 12 | export interface GridProps extends DataProps, React.HTMLAttributes {} 13 | 14 | export interface GridState { 15 | offsetX: number; 16 | offsetY: number; 17 | scale?: number; 18 | currGridWidth: number; 19 | currGridHeight: number; 20 | } 21 | 22 | export class Grid extends React.Component { 23 | state: GridState = { 24 | offsetX: 0, 25 | offsetY: 0, 26 | scale: 1, 27 | currGridWidth: this.props.gridWidth, 28 | currGridHeight: this.props.gridHeight 29 | } 30 | render() { 31 | const { 32 | gridWidth: gridWitdh, 33 | gridHeight, 34 | lineColor, 35 | gridSize, 36 | griItemCount, 37 | ...attributes 38 | } = this.props; 39 | const { 40 | offsetX, 41 | offsetY, 42 | scale, 43 | currGridWidth: currGridWidth, 44 | currGridHeight 45 | } = this.state 46 | const rootStyle = { 47 | position: "fixed", 48 | left: 0, 49 | right: 0, 50 | width: "100%", 51 | height: "100%", 52 | pointerEvents: "none" 53 | } as React.CSSProperties; 54 | 55 | let rowMaxCount = currGridWidth / scale / gridSize; 56 | rowMaxCount = Number.isInteger(rowMaxCount) ? rowMaxCount + 1 : Math.ceil(rowMaxCount); 57 | 58 | return ( 59 | 60 | {Array(rowMaxCount * 10).fill(0).map((zero, index) => { 61 | const x = index * (gridSize / griItemCount); 62 | const strokeWidth = .2 / scale; 63 | return [ 64 | , 73 | 82 | ] 83 | })} 84 | {Array(rowMaxCount).fill(0).map((zero, index) => { 85 | const x = index * gridSize; 86 | const strokeWidth = .4 / scale; 87 | return [ 88 | , 96 | 104 | ] 105 | })} 106 | 107 | ); 108 | } 109 | } 110 | 111 | export default Grid; 112 | -------------------------------------------------------------------------------- /src/WebView/components/SingleFile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import { Config } from "./Board"; 4 | import Table from "./Table"; 5 | import { getTranslate } from "../utils/getTransform"; 6 | import { addDragToSVGElement } from "../utils/addDragToSVGElement"; 7 | import { getTablePosition, setTablePosition, getContentLayout } from "../utils/layout"; 8 | import { TypeOut, TypeIn } from "./Content"; 9 | 10 | const originOffsetX = -20; 11 | 12 | import { DocEntry } from "../../getFileDocEntries"; 13 | export interface DataProps extends DocEntry { 14 | onChangeView?: (position?: { x: number; y: number }) => void; 15 | originMembers?: DocEntry[]; 16 | } 17 | export interface SingleFileProps extends DataProps, React.SVGAttributes {} 18 | 19 | const reType = /(\w+)(\[\])*/i; 20 | export class SingleFile extends React.Component { 21 | static contextTypes = { config: PropTypes.object }; 22 | context: { config: Config }; 23 | tables: Table[] = []; 24 | rootEl: SVGGElement; 25 | wrapperEl: SVGGElement; 26 | tablesContainer: SVGGElement; 27 | backgroundEl: SVGRectElement; 28 | headerTextEl: SVGTextElement; 29 | headerEl: SVGRectElement; 30 | hue = Math.random() * 255; 31 | backgroundRect = { 32 | left: 0, 33 | top: 0, 34 | width: 0, 35 | height: 0 36 | }; 37 | typeInOuts: { 38 | typeIn?: TypeIn; 39 | typeOuts?: TypeOut[]; 40 | }[] = []; 41 | 42 | docEntries: DocEntry[] = []; 43 | 44 | componentDidMount() { 45 | this.originTablesRect = this.tablesContainer.getBoundingClientRect(); 46 | this.originRootTranslate = this.getSVGRootTranslate(); 47 | addDragToSVGElement(this.headerEl, this.rootEl, this.props.onChangeView); 48 | this.screenMatrix = this.tablesContainer.getCTM(); 49 | this.redrawDOM(); 50 | } 51 | 52 | componentDidUpdate() { 53 | this.redrawDOM(); 54 | } 55 | 56 | getFileTranslate = () => { 57 | const transform = this.rootEl.getAttributeNS(null, "transform"); 58 | const position = getTranslate(transform); 59 | return position; 60 | } 61 | 62 | getSVGRootTranslate = () => { 63 | if (!this.rootEl) { 64 | const contentLayout = getContentLayout(); 65 | return { x: contentLayout.position.x, y: contentLayout.position.y }; 66 | } 67 | const boardEl: SVGSVGElement = this.rootEl.parentElement as any; 68 | const boardTranslate = getTranslate(boardEl.getAttributeNS(null, "transform")) || { x: 0, y: 0 }; 69 | return boardTranslate; 70 | } 71 | 72 | getTypeInOuts = () => { 73 | const { locals } = this.props; 74 | const { 75 | headerHeight, 76 | itemHeight 77 | } = this.context.config.tableStyle; 78 | 79 | this.screenMatrix = this.tablesContainer.getCTM(); 80 | const boardTranslate = this.getSVGRootTranslate(); 81 | if (!this.docEntries) return []; 82 | 83 | this.docEntries.forEach((docEntry, index) => { 84 | const { members, type, filename, name, tableWidth } = docEntry; 85 | const table = this.tables[index]; 86 | const tableRect = table ? table.rootEl.getBoundingClientRect() : { left: 0, top: 0 } as any; 87 | const typeOuts: TypeOut[] = []; 88 | let leftX = tableRect.left - boardTranslate.x + originOffsetX; 89 | leftX = leftX / this.screenMatrix.a; 90 | let rightX = leftX + tableWidth; 91 | let y = tableRect.top / this.screenMatrix.d + headerHeight / 2 - boardTranslate.y / this.screenMatrix.d; 92 | const typeInOutFile = { 93 | typeIn: { 94 | name, 95 | leftPosition: { 96 | x: leftX, 97 | y 98 | }, 99 | rightPosition: { 100 | x: rightX, 101 | y 102 | }, 103 | type, 104 | filename 105 | }, 106 | typeOuts 107 | }; 108 | this.typeInOuts[index] = typeInOutFile; 109 | 110 | if (members) { 111 | members.forEach((member, index) => { 112 | const escapedName = reType.test(member.type) ? member.type.match(reType)[1] : member.name; 113 | let localIndex = -1; 114 | if (escapedName) { 115 | for (const index in locals) { 116 | if (locals[index].name === escapedName) { 117 | localIndex = Number(index); 118 | break; 119 | } 120 | } 121 | } 122 | const y = tableRect.top / this.screenMatrix.d + headerHeight + itemHeight * index + itemHeight / 2 - boardTranslate.y / this.screenMatrix.d; 123 | typeOuts[index] = { 124 | leftPosition: { 125 | x: leftX, 126 | y 127 | }, 128 | rightPosition: { 129 | x: rightX, 130 | y 131 | }, 132 | toFileType: { 133 | isLocal: localIndex > -1 ? (!locals[localIndex].filename) : false, 134 | escapedName, 135 | type: member.type, 136 | filename: localIndex > -1 ? locals[localIndex].filename : void 0 137 | } 138 | }; 139 | }); 140 | } 141 | 142 | 143 | const memberSize = docEntry.members ? docEntry.members.length : 0; 144 | const setPosition = (targetIndex: number, escapedName: string, type: string) => { 145 | let localIndex = -1; 146 | if (escapedName) { 147 | for (const index in locals) { 148 | if (locals[index].name === escapedName) { 149 | localIndex = Number(index); 150 | break; 151 | } 152 | } 153 | } 154 | typeOuts[targetIndex + memberSize] = { 155 | leftPosition: { 156 | x: leftX, 157 | y 158 | }, 159 | rightPosition: { 160 | x: rightX, 161 | y 162 | }, 163 | toFileType: { 164 | isLocal: localIndex > -1 ? (!locals[localIndex].filename) : false, 165 | escapedName, 166 | type, 167 | filename: localIndex > -1 ? locals[localIndex].filename : void 0 168 | } 169 | }; 170 | }; 171 | if (docEntry.extends) { 172 | let index = 0; 173 | let targetIndex = 0; 174 | const extendsSize = docEntry.extends.length; 175 | 176 | while (index < extendsSize) { 177 | const extendsItem: DocEntry = docEntry.extends[index] as DocEntry; 178 | const escapedName = reType.test(extendsItem.name) ? extendsItem.name.match(reType)[1] : extendsItem.name; 179 | 180 | // Get ReactComponent Extends 181 | if (escapedName === "Component") { 182 | const reComponent = /(?:React\.)?(?:Component)\?/im; 183 | const result = reComponent.exec(docEntry.valueDeclarationText); 184 | if (result[1]) { 185 | const escapedNames = result[1].split(",").map(str => str.trim()); 186 | setPosition(targetIndex, escapedNames[0], extendsItem.type); 187 | targetIndex += 1; 188 | setPosition(targetIndex, escapedNames[1], extendsItem.type); 189 | } 190 | } else { 191 | setPosition(targetIndex, escapedName, extendsItem.type); 192 | } 193 | index += 1; 194 | targetIndex += 1; 195 | } 196 | } 197 | }); 198 | return this.typeInOuts; 199 | } 200 | 201 | getAllExportDocEntry = (exportDocEntry: DocEntry) => { 202 | const { members } = this.props; 203 | let index = -1; 204 | if (members) { 205 | for (let i = 0; i < members.length; i++) { 206 | if (members[i].name === exportDocEntry.name) { 207 | index = i; 208 | break; 209 | } 210 | } 211 | } 212 | return index > -1 ? members[index] : exportDocEntry; 213 | } 214 | 215 | findMaxLetter = (docEntry: DocEntry) => { 216 | const headerCount = docEntry.name.length + docEntry.type.length; 217 | if (docEntry.members && docEntry.members.length > 0) { 218 | const maxCount = docEntry.members.reduce((prev, current) => { 219 | const letterCount = current.name ? current.name.length : 0 + current.type ? current.type.length : 0; 220 | return letterCount > prev ? letterCount : prev; 221 | }, 0); 222 | return headerCount > maxCount ? headerCount : maxCount; 223 | } else { 224 | return headerCount; 225 | } 226 | } 227 | 228 | screenMatrix: SVGMatrix; 229 | handleChangeTableView = (isCallback = true) => { 230 | const { onChangeView } = this.props; 231 | if (onChangeView) { 232 | onChangeView(this.getFileTranslate()); 233 | } 234 | this.redrawDOM(); 235 | } 236 | 237 | originRootTranslate: { x: number; y: number }; 238 | originTablesRect: ClientRect; 239 | redrawDOM = () => { 240 | const { members, fileWidth, fileHeight } = this.props; 241 | const hadMembers = members && members.length > 0; 242 | const tablesRect = this.tablesContainer.getBoundingClientRect(); 243 | const { widthPadding, headerHeight, heightPadding, headerFontSize } = this.context.config.fileStyle; 244 | this.wrapperEl.setAttributeNS(null, "display", "none"); 245 | 246 | const x = -widthPadding; 247 | const y = -headerHeight - heightPadding; 248 | this.screenMatrix = this.tablesContainer.getCTM(); 249 | const width = hadMembers ? `${tablesRect.width / this.screenMatrix.a + widthPadding * 2}` : `${fileWidth}`; 250 | const height = hadMembers ? `${tablesRect.height / this.screenMatrix.d + heightPadding * 2 + headerHeight}` : `${fileHeight}`; 251 | 252 | this.headerEl.setAttributeNS(null, "transform", `translate(${x}, ${y})`); 253 | this.headerEl.setAttributeNS(null, "width", width); 254 | 255 | this.backgroundEl.setAttributeNS(null, "transform", `translate(${x}, ${y})`); 256 | this.backgroundEl.setAttributeNS(null, "width", width); 257 | this.backgroundEl.setAttributeNS(null, "height", height); 258 | 259 | this.headerTextEl.setAttributeNS(null, "transform", `translate(${x + 4}, ${y + (headerHeight + headerFontSize) / 2})`); 260 | 261 | const wrapperRect = this.wrapperEl.getBoundingClientRect(); 262 | const offsetX = (tablesRect.left - wrapperRect.left) / this.screenMatrix.a + x; 263 | const offsetY = (tablesRect.top - wrapperRect.top) / this.screenMatrix.d + y; 264 | this.wrapperEl.setAttributeNS(null, "display", ""); 265 | this.wrapperEl.setAttributeNS(null, "transform", `translate(${offsetX}, ${offsetY})`); 266 | } 267 | 268 | render() { 269 | let { 270 | name, 271 | filename, 272 | members, 273 | exports, 274 | resolvedModules, 275 | onChangeView, 276 | rowIndex, 277 | fileWidth, 278 | fileHeight, 279 | valueDeclarationText, 280 | memberLayouts, 281 | exportLayouts, 282 | escapedName, 283 | exportMembers, 284 | originMembers, 285 | ...attributes 286 | } = this.props; 287 | const { config } = this.context; 288 | const justShowExport = config.showType === "export"; 289 | this.typeInOuts = []; 290 | const { headerFontSize, headerHeight } = this.context.config.fileStyle; 291 | 292 | if (exports) { 293 | exports = exports 294 | .map(exportDocEntry => this.getAllExportDocEntry(exportDocEntry)) 295 | .filter(docEntry => docEntry.name !== "default"); 296 | } 297 | const docEntries = justShowExport ? exports : members; 298 | this.docEntries = docEntries; 299 | 300 | const x = 0; 301 | const y = 0; 302 | 303 | return ( 304 | this.rootEl = rootEl} {...attributes}> 305 | this.wrapperEl = wrapperEl} display="none"> 306 | this.backgroundEl = backgroundEl} 316 | transform={`translate(${x}, ${y})`} 317 | /> 318 | this.headerEl = headerEl} 324 | fill="#000" 325 | /> 326 | this.headerTextEl = headerTextEl} 334 | > 335 | {filename} 336 | {/* {(filename && filename.length > 60) ? `${filename.slice(0, 60)}...` : filename} */} 337 | 338 | 339 | this.tablesContainer = tablesContainer}> 340 | {docEntries && docEntries.map((docEntry, index) => { 341 | const position = getTablePosition(filename, docEntry.name); 342 | return ( 343 | { 346 | this.handleChangeTableView(); 347 | setTablePosition(filename, docEntry.name, position); 348 | }} 349 | ref={table => this.tables[index] = table} 350 | key={index} 351 | transform={`translate(${position.x}, ${position.y})`} 352 | /> 353 | ); 354 | })} 355 | 356 | 357 | ); 358 | } 359 | } 360 | 361 | export default SingleFile; 362 | -------------------------------------------------------------------------------- /src/WebView/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import { addDragToSVGElement } from "../utils/addDragToSVGElement"; 4 | 5 | import { Config } from "./Board"; 6 | 7 | import { DocEntry } from "../../getFileDocEntries"; 8 | 9 | export interface DataProps extends DocEntry { 10 | onChangeView?: (tablePosition?: { x: number; y: number }) => void; 11 | } 12 | export interface TableProps extends DataProps, React.SVGAttributes {} 13 | 14 | export class Table extends React.Component { 15 | static contextTypes = { config: PropTypes.object }; 16 | context: { config: Config }; 17 | rootEl: SVGGElement; 18 | dragEl: SVGGElement; 19 | 20 | componentDidMount() { 21 | addDragToSVGElement(this.dragEl, this.rootEl, this.props.onChangeView); 22 | } 23 | 24 | render() { 25 | const { 26 | name, 27 | type, 28 | members, 29 | tableWidth, 30 | onChangeView, 31 | columnIndex, 32 | tableHeight, 33 | valueDeclarationText, 34 | escapedName, 35 | initializerText, 36 | ...attributes 37 | } = this.props; 38 | const { config } = this.context; 39 | const { 40 | theme, 41 | tableStyle 42 | } = config; 43 | const { 44 | itemHeight, 45 | itemPadding, 46 | headerFontSize, 47 | itemFontSize, 48 | headerHeight 49 | } = tableStyle; 50 | 51 | return ( 52 | this.rootEl = rootEl} 55 | > 56 | this.dragEl = dragEl} 58 | fill={theme.accent} 59 | fillOpacity={1} 60 | width={tableWidth} 61 | height={headerHeight} 62 | style={{ cursor: "move" }} 63 | /> 64 | 69 | 73 | {name} 74 | 75 | 81 | {type.slice(0, config.maxShowTypeLength)} 82 | 83 | 84 | 85 | {members && members.map((item, index) => { 86 | const { name, type, isRequired } = item; 87 | const middleY = (itemHeight - itemFontSize) / 2 + itemFontSize; 88 | return ( 89 | 90 | 95 | 96 | 100 | {name} 101 | 102 | 108 | {`${isRequired ? ":" : "?:" }${type.length > config.maxShowTypeLength ? type.slice(0, config.maxShowTypeLength) : type}`} 109 | 110 | 111 | 112 | ); 113 | })} 114 | 115 | 116 | ); 117 | } 118 | } 119 | 120 | export default Table; 121 | -------------------------------------------------------------------------------- /src/WebView/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | declare var acquireVsCodeApi; 5 | if (!window.vscode) { 6 | window.vscode = acquireVsCodeApi(); 7 | } 8 | import Board from "./components/Board"; 9 | 10 | const { render } = ReactDOM; 11 | const rootEl = document.getElementById("app"); 12 | 13 | const renderToDOM = (AppContainer?: new() => React.Component, AppComponent = Board) => { 14 | if (AppContainer) { 15 | render( 16 |
17 | 18 |
, 19 | rootEl 20 | ); 21 | } else { 22 | render(, rootEl); 23 | } 24 | }; 25 | 26 | renderToDOM(); -------------------------------------------------------------------------------- /src/WebView/utils/addDragToSVGElement.ts: -------------------------------------------------------------------------------- 1 | import { getTranslate } from "./getTransform"; 2 | 3 | export function addDragToSVGElement(targetEl: T, transformEl?: F, onChangeView?: (position?: { x?: number; y?: number; }) => void) { 4 | if (!targetEl) return; 5 | if (!transformEl) { 6 | transformEl = targetEl as any; 7 | } 8 | 9 | const mouseStatPosition: { 10 | x?: number; 11 | y?: number 12 | } = { x: 0, y: 0 }; 13 | const originTransform: { 14 | x?: number; 15 | y?: number; 16 | } = { x: 0, y: 0 }; 17 | const currTransform: { 18 | x?: number; 19 | y?: number; 20 | } = { x: 0, y: 0 }; 21 | let screenMatrix: SVGMatrix; 22 | 23 | targetEl.addEventListener("mousedown", handleMouseDown as any); 24 | targetEl.addEventListener("mouseup", handleMouseUp as any); 25 | 26 | function handleMouseDown (e: React.MouseEvent) { 27 | e.stopPropagation(); 28 | screenMatrix = transformEl.getScreenCTM(); 29 | mouseStatPosition.x = e.clientX / screenMatrix.a; 30 | mouseStatPosition.y = e.clientY / screenMatrix.d; 31 | const transform = transformEl.getAttributeNS(null, "transform"); 32 | const translate = getTranslate(transform); 33 | if (translate) { 34 | Object.assign(originTransform, translate); 35 | } 36 | 37 | document.documentElement.addEventListener("mousemove", handleMouseMove); 38 | document.documentElement.addEventListener("mouseup", handleMouseUp); 39 | } 40 | 41 | function handleMouseMove (e: MouseEvent) { 42 | const offsetX = e.clientX / screenMatrix.a - mouseStatPosition.x; 43 | const offsetY = e.clientY / screenMatrix.d - mouseStatPosition.y; 44 | const x = originTransform.x + offsetX; 45 | const y = originTransform.y + offsetY; 46 | currTransform.x = x; 47 | currTransform.y = y; 48 | transformEl.setAttributeNS(null, "transform", `translate(${x}, ${y})`); 49 | if (onChangeView) onChangeView({ x, y }); 50 | } 51 | 52 | function handleMouseUp (e: MouseEvent | React.MouseEvent) { 53 | if (onChangeView) onChangeView(currTransform); 54 | 55 | document.documentElement.removeEventListener("mousemove", handleMouseMove); 56 | document.documentElement.removeEventListener("mouseup", handleMouseUp); 57 | } 58 | } 59 | 60 | export default addDragToSVGElement; 61 | -------------------------------------------------------------------------------- /src/WebView/utils/getConnectPathString.ts: -------------------------------------------------------------------------------- 1 | const radius = 20; 2 | const offset = 10; 3 | 4 | export function getConnectPathString( 5 | startPosition: { x: number; y: number }, 6 | endPosition: { x: number; y: number }, 7 | useBezier = true 8 | ) { 9 | const ltr = endPosition.x > startPosition.x; 10 | let str = `M ${startPosition.x} ${startPosition.y} `; 11 | if (useBezier) { 12 | if (startPosition.x !== endPosition.x) { 13 | const offset = Math.abs(startPosition.x - endPosition.x) / 2; 14 | str += `C${startPosition.x + (ltr ? offset : -offset)},${startPosition.y} ${endPosition.x + (ltr ? -offset : offset)},${endPosition.y} ${endPosition.x},${endPosition.y}`; 15 | } else { 16 | const offset = startPosition.x - 100; 17 | str += `C${offset},${startPosition.y} ${offset},${endPosition.y} ${endPosition.x},${endPosition.y}`; 18 | } 19 | } else { 20 | const middleX = endPosition.x + (ltr ? -offset : offset); 21 | str += `L ${middleX} ${startPosition.y} `; 22 | str += `L ${middleX} ${endPosition.y} `; 23 | str += `L ${endPosition.x} ${endPosition.y} `; 24 | } 25 | return str; 26 | } 27 | 28 | export default getConnectPathString; 29 | -------------------------------------------------------------------------------- /src/WebView/utils/getTransform.ts: -------------------------------------------------------------------------------- 1 | export function getTranslate(str: string) { 2 | if (!str) { 3 | return { 4 | x: 0, 5 | y: 0 6 | }; 7 | } 8 | const reTranslate = /translate\s*\(([-\+\d\.\s,e]+)\)/i; 9 | const xyStr = str.match(reTranslate)[1]; 10 | if (xyStr) { 11 | const xyArr = xyStr.split(",").map(str => Number(str)); 12 | return { 13 | x: xyArr[0], 14 | y: xyArr[1] 15 | }; 16 | } else { 17 | return null; 18 | } 19 | } 20 | 21 | export function getScale(str: string) { 22 | if (!str) return 1; 23 | const reScale = /scale\((\d*\.*\d*)\)/i; 24 | const scaleStr = str.match(reScale)[1]; 25 | return scaleStr ? Number(scaleStr) : null; 26 | } 27 | -------------------------------------------------------------------------------- /src/WebView/utils/layout.ts: -------------------------------------------------------------------------------- 1 | import { DocEntry } from "../../getFileDocEntries"; 2 | import { Config, Position } from "../components/Board"; 3 | export { Position }; 4 | 5 | export interface BoardLayout { 6 | contentPosition?: Position; 7 | contentScale?: number; 8 | filePositions?: { 9 | [filename: string]: { 10 | position?: Position; 11 | memberPositions: { 12 | [tableName: string]: { 13 | position?: Position; 14 | } 15 | }; 16 | } 17 | }; 18 | } 19 | 20 | let layout: BoardLayout = {}; 21 | let cachedFiles: DocEntry[]; 22 | let cachedConfig: Config; 23 | 24 | export function resetDefaultLayout(files: DocEntry[], config: Config) { 25 | // get default layout 26 | if (!files && cachedFiles) files = cachedFiles; 27 | if (!config && cachedConfig) config = cachedConfig; 28 | if (!(config && files)) return; 29 | 30 | layout.contentPosition = { 31 | x: config.contentStyle.padding + config.fileStyle.widthPadding, 32 | y: config.contentStyle.padding + config.fileStyle.headerHeight + config.fileStyle.heightPadding 33 | }; 34 | layout.contentScale = 1; 35 | 36 | let prevLeft = 0; 37 | let prevTop = 0; 38 | layout.filePositions = {}; 39 | cachedFiles.forEach((file, index) => { 40 | const memberPositions = {}; 41 | layout.filePositions[file.filename] = { 42 | position: { x: 0, y: prevTop }, 43 | memberPositions 44 | }; 45 | const layouts = config.showType === "export" ? file.exportLayouts : file.memberLayouts; 46 | try { 47 | for (const memberName in layouts.memberPositions) { 48 | const position = layouts.memberPositions[memberName]; 49 | memberPositions[memberName] = {}; 50 | memberPositions[memberName].position = { x: position.x, y: position.y }; 51 | } 52 | } catch (e) {} 53 | prevLeft += file.fileWidth + config.fileStyle.fileOffset; 54 | prevTop += file.fileHeight + config.fileStyle.fileOffset; 55 | }); 56 | 57 | writeStorage(); 58 | } 59 | 60 | export function getLayout(files: DocEntry[], config: Config) { 61 | const docEntryEl: Element = document.getElementById("doc-layout"); 62 | const JSONStr = docEntryEl.innerHTML.trim(); 63 | let settedLayout = false; 64 | if (JSONStr) { 65 | const savedLayout = JSON.parse(JSONStr); 66 | if (savedLayout && savedLayout.filePositions && Object.keys(savedLayout.filePositions).length > 0) { 67 | layout = savedLayout; 68 | settedLayout = true; 69 | } 70 | } 71 | 72 | if (!settedLayout) { 73 | layout = {}; 74 | cachedFiles = files; 75 | cachedConfig = config; 76 | resetDefaultLayout(files, config); 77 | } 78 | 79 | return layout; 80 | } 81 | 82 | function writeStorage() { 83 | window.vscode.postMessage({ setLayout: layout }); 84 | } 85 | 86 | const originP = { x: 0, y: 0 }; 87 | 88 | export function getContentLayout() { 89 | return { position: layout.contentPosition, scale: layout.contentScale }; 90 | } 91 | 92 | export function setContentLayout(position?: Position, scale?: number) { 93 | if (position !== void 0) layout.contentPosition = position; 94 | if (scale !== void 0) layout.contentScale = scale; 95 | writeStorage(); 96 | } 97 | 98 | export function getContentPosition() { 99 | return layout.contentPosition; 100 | } 101 | 102 | export function setContentPosition(position: Position) { 103 | layout.contentPosition = position; 104 | writeStorage(); 105 | } 106 | 107 | export function getContentScale() { 108 | return layout.contentScale; 109 | } 110 | 111 | export function setContentScale(scale: number) { 112 | layout.contentScale = scale; 113 | writeStorage(); 114 | } 115 | 116 | export function getFilePosition(filename: string) { 117 | const file = layout.filePositions[filename]; 118 | return file ? file.position : originP; 119 | } 120 | 121 | export function setFilePosition(filename: string, position: Position) { 122 | try { 123 | layout.filePositions[filename].position = position; 124 | } catch (e) {} 125 | writeStorage(); 126 | } 127 | 128 | export function getTablePosition(filename: string, tableName: string) { 129 | let result = originP; 130 | try { 131 | result = layout.filePositions[filename].memberPositions[tableName].position; 132 | } catch (e) {} 133 | return result; 134 | } 135 | 136 | export function setTablePosition(filename: string, tableName: string, position: Position) { 137 | try { 138 | layout.filePositions[filename].memberPositions[tableName].position = position; 139 | } catch (e) {} 140 | writeStorage(); 141 | } 142 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | getAllLocalMembers?: boolean; 3 | showType?: "export" | "member"; 4 | sacle?: number; 5 | fileMaxDepth?: number; 6 | tableStyle?: { 7 | headerHeight?: number; 8 | itemHeight?: number; 9 | textPadding?: number; 10 | itemPadding?: number; 11 | headerFontSize?: number; 12 | itemFontSize?: number; 13 | }; 14 | connectPathStyle?: { 15 | color?: string; 16 | strokeDasharray?: string; 17 | arrowSize?: number; 18 | }; 19 | fileStyle?: { 20 | widthPadding?: number; 21 | heightPadding?: number; 22 | headerHeight?: number; 23 | headerFontSize?: number; 24 | tableOffset?: number; 25 | fileOffset?: number; 26 | }; 27 | contentStyle?: { 28 | background?: string; 29 | padding?: number; 30 | }; 31 | theme?: { 32 | accent: string; 33 | }; 34 | maxShowTypeLength?: number; 35 | } 36 | 37 | export const config: Config = { 38 | getAllLocalMembers: true, 39 | showType: "export", 40 | fileMaxDepth: 0, 41 | tableStyle: { 42 | itemPadding: 12, 43 | textPadding: 20, 44 | headerHeight: 36, 45 | itemHeight: 28, 46 | headerFontSize: 14, 47 | itemFontSize: 12 48 | }, 49 | fileStyle: { 50 | widthPadding: 8, 51 | heightPadding: 16, 52 | headerHeight: 24, 53 | headerFontSize: 12, 54 | tableOffset: 36, 55 | fileOffset: 48 56 | }, 57 | connectPathStyle: { 58 | color: "#333", 59 | strokeDasharray: "4 2", 60 | arrowSize: 6 61 | }, 62 | contentStyle: { 63 | background: "#e5e5e5", 64 | padding: 24 65 | }, 66 | theme: { 67 | accent: "#005aa0" 68 | }, 69 | maxShowTypeLength: 20 70 | } 71 | 72 | export default config; 73 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import * as fs from 'fs'; 4 | import * as md5 from "md5"; 5 | 6 | import getFileDocEntries, { DocEntry } from './getFileDocEntries' 7 | import config from './config' 8 | 9 | const entryFileName = path.join(__dirname, "./entryFile.json"); 10 | let title = "TypeScript UML"; 11 | 12 | let docEntries: DocEntry[]; 13 | 14 | let saveData = getSaveFileData(); 15 | let globalPanel: vscode.WebviewPanel; 16 | let globalExtensionPath: string; 17 | 18 | let prevMd5 = null; 19 | export function watchFile() { 20 | if (fs.existsSync(saveData.entryFile)) { 21 | if (!prevMd5) { 22 | prevMd5 = md5(fs.readFileSync(saveData.entryFile)); 23 | } 24 | fs.watchFile(saveData.entryFile, (eventType, filename) => { 25 | if (filename) { 26 | const currMd5 = md5(fs.readFileSync(saveData.entryFile)); 27 | if (currMd5 !== prevMd5) { 28 | UMLWebviewPanel.revive(globalPanel, globalExtensionPath); 29 | prevMd5 = currMd5; 30 | } 31 | } 32 | }); 33 | } 34 | } 35 | 36 | export function activate(context: vscode.ExtensionContext) { 37 | let newFile = ""; 38 | async function commandCallback(fileUri: any) { 39 | if (fileUri !== void 0) { 40 | if (typeof fileUri === "string") { 41 | newFile = fileUri; 42 | } else if (typeof fileUri === "object" && fileUri.path) { 43 | newFile = fileUri.path; 44 | } 45 | } else { 46 | newFile = ""; 47 | const { activeTextEditor } = vscode.window; 48 | if (activeTextEditor) { 49 | if (!newFile && activeTextEditor && activeTextEditor.document) { 50 | newFile = activeTextEditor.document.fileName; 51 | } 52 | } 53 | } 54 | newFile = path.normalize(newFile) 55 | if (process.platform === "win32" && newFile.startsWith("\\")) { 56 | newFile = newFile.slice(1); 57 | } 58 | if (newFile !== saveData.entryFile) { 59 | saveData.entryFile = newFile; 60 | saveData.layout = ""; 61 | } 62 | fs.unwatchFile(saveData.entryFile) 63 | watchFile(); 64 | 65 | UMLWebviewPanel.createOrShow(context.extensionPath); 66 | } 67 | context.subscriptions.push(vscode.commands.registerCommand('tsUML.showFile', commandCallback)); 68 | context.subscriptions.push(vscode.commands.registerCommand('tsUML.showFolder', commandCallback)); 69 | 70 | if (vscode.window.registerWebviewPanelSerializer) { 71 | // Make sure we register a serilizer in activation event 72 | vscode.window.registerWebviewPanelSerializer(UMLWebviewPanel.viewType, { 73 | async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) { 74 | // console.log(`Got state: ${JSON.stringify(state)}`); 75 | globalPanel = webviewPanel; 76 | globalExtensionPath = context.extensionPath; 77 | UMLWebviewPanel.revive(webviewPanel, context.extensionPath); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | class UMLWebviewPanel { 84 | /** 85 | * Track the currently panel. Only allow a single panel to exist at a time. 86 | */ 87 | public static currentPanel: UMLWebviewPanel | undefined; 88 | 89 | public static readonly viewType = "tsUML"; 90 | 91 | private readonly _panel: vscode.WebviewPanel; 92 | private readonly _extensionPath: string; 93 | private _disposables: vscode.Disposable[] = []; 94 | 95 | public static createOrShow(extensionPath: string) { 96 | const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; 97 | 98 | // If we already have a panel, show it. 99 | if (UMLWebviewPanel.currentPanel) { 100 | UMLWebviewPanel.currentPanel._panel.reveal(column); 101 | UMLWebviewPanel.currentPanel._update(); 102 | return; 103 | } 104 | 105 | // Otherwise, create a new panel. 106 | const panel = vscode.window.createWebviewPanel(UMLWebviewPanel.viewType, title, column || vscode.ViewColumn.One, { 107 | // Enable javascript in the webview 108 | enableScripts: true, 109 | 110 | // And restric the webview to only loading content from our extension's `media` directory. 111 | localResourceRoots: [ 112 | vscode.Uri.file(path.join(extensionPath, 'media')), 113 | vscode.Uri.file(path.join(extensionPath, 'build')), 114 | vscode.Uri.file(path.join(extensionPath, 'icons')) 115 | ] 116 | }); 117 | 118 | addDidReceiveMessage(panel); 119 | 120 | globalPanel = panel; 121 | globalExtensionPath = extensionPath; 122 | UMLWebviewPanel.currentPanel = new UMLWebviewPanel(panel, extensionPath); 123 | } 124 | 125 | public static revive(panel: vscode.WebviewPanel, extensionPath: string) { 126 | UMLWebviewPanel.currentPanel = new UMLWebviewPanel(panel, extensionPath); 127 | } 128 | 129 | private constructor( 130 | panel: vscode.WebviewPanel, 131 | extensionPath: string 132 | ) { 133 | this._panel = panel; 134 | this._extensionPath = extensionPath; 135 | 136 | // Set the webview's initial html content 137 | this._update(); 138 | 139 | // Listen for when the panel is disposed 140 | // This happens when the user closes the panel or when the panel is closed programatically 141 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables); 142 | 143 | // Update the content based on view changes 144 | this._panel.onDidChangeViewState(e => { 145 | if (this._panel.visible) { 146 | // this._update() 147 | } 148 | }, null, this._disposables); 149 | } 150 | 151 | public dispose() { 152 | UMLWebviewPanel.currentPanel = undefined; 153 | 154 | // Clean up our resources 155 | this._panel.dispose(); 156 | 157 | while (this._disposables.length) { 158 | const x = this._disposables.pop(); 159 | if (x) { 160 | x.dispose(); 161 | } 162 | } 163 | } 164 | 165 | public _update() { 166 | this._panel.webview.html = this._getHtmlForWebview(); 167 | } 168 | 169 | private _getHtmlForWebview() { 170 | const nonce = getNonce(); 171 | 172 | const favicon = vscode.Uri.file(path.join(this._extensionPath, 'icons/ts.png')); 173 | const faviconUri = favicon.with({ scheme: 'vscode-resource' }); 174 | const appScript = vscode.Uri.file(path.join(this._extensionPath, 'build/js/app.js')); 175 | const appUri = appScript.with({ scheme: 'vscode-resource' }); 176 | const vendorScript = vscode.Uri.file(path.join(this._extensionPath, 'build/js/vendor.js')); 177 | const vendorUri = vendorScript.with({ scheme: 'vscode-resource' }); 178 | 179 | if (saveData.entryFile) { 180 | docEntries = getFileDocEntries(saveData.entryFile, 0, config); 181 | } 182 | let layoutStr = JSON.stringify(saveData.layout); 183 | // console.log("layout: ", saveData.layout); 184 | saveDataToFile(); 185 | addDidReceiveMessage(this._panel); 186 | 187 | const htmlStr = ` 188 | 189 | 190 | 191 | 192 | ${title} 193 | 194 | 195 | 196 | 197 |
198 | 199 | 200 | 201 | `; 202 | return htmlStr; 203 | } 204 | } 205 | 206 | function addDidReceiveMessage(panel: vscode.WebviewPanel) { 207 | let saveDataTimer = null; 208 | panel.webview.onDidReceiveMessage( 209 | message => { 210 | // console.log("onDidReceiveMessage: ", message) 211 | if (message.boardWillMount) { 212 | panel.webview.postMessage({ docEntries }); 213 | } 214 | if (message.getLayout) { 215 | panel.webview.postMessage({ layout: saveData.layout }); 216 | } 217 | if (message.error) { 218 | console.error(message.error); 219 | } 220 | if (message.setLayout) { 221 | saveData.layout = message.setLayout; 222 | clearTimeout(saveDataTimer); 223 | saveDataTimer = setTimeout(saveDataToFile, 200); 224 | } 225 | }, 226 | ); 227 | } 228 | 229 | function getNonce() { 230 | let text = ""; 231 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 232 | for (let i = 0; i < 32; i++) { 233 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 234 | } 235 | return text; 236 | } 237 | 238 | function saveDataToFile() { 239 | // console.log("saveDataToFile: ", saveData); 240 | fs.writeFileSync(entryFileName, JSON.stringify(saveData, null, 2), { encoding: "utf8" }); 241 | } 242 | 243 | function getSaveFileData() { 244 | let data = { entryFile: "", layout: "" }; 245 | if (fs.existsSync(entryFileName)) { 246 | try { 247 | data = JSON.parse(fs.readFileSync(entryFileName, { encoding: "utf8" })); 248 | } catch (e) {} 249 | } 250 | return data; 251 | } 252 | -------------------------------------------------------------------------------- /src/getFileDocEntries.ts: -------------------------------------------------------------------------------- 1 | import Parser, { DocEntry as DefaultDocEntry } from "./Parser"; 2 | import { Config } from "./config"; 3 | export { Config }; 4 | 5 | export interface DocEntry extends DefaultDocEntry { 6 | rowIndex?: number; 7 | columnIndex?: number; 8 | isRef?: boolean; 9 | fileWidth?: number; 10 | fileHeight?: number; 11 | tableWidth?: number; 12 | tableHeight?: number; 13 | 14 | memberLayouts?: any; 15 | exportLayouts?: any; 16 | } 17 | 18 | const reType = /(\w+)(\[\])*/i; 19 | const findMaxLetter = (docEntry: DocEntry, config: Config) => { 20 | const headerCount = docEntry.name.length + (docEntry.type.length > config.maxShowTypeLength ? config.maxShowTypeLength : docEntry.type.length); 21 | if (docEntry.members && docEntry.members.length > 0) { 22 | const maxCount = docEntry.members.reduce((prev, current) => { 23 | const letterCount = current.name ? current.name.length : 0 + ( 24 | current.type ? (current.type.length > config.maxShowTypeLength ? config.maxShowTypeLength : current.type.length) : 0 25 | ); 26 | return letterCount > prev ? letterCount : prev; 27 | }, 0); 28 | return headerCount > maxCount ? headerCount : maxCount; 29 | } else { 30 | return headerCount; 31 | } 32 | }; 33 | 34 | const getTableWidth = (docEntry: DocEntry, config: Config) => { 35 | const { textPadding, itemPadding } = config.tableStyle; 36 | return findMaxLetter(docEntry, config) * 12; // + textPadding + itemPadding * 2; 37 | }; 38 | const getTableHeight = (docEntry: DocEntry, config: Config) => { 39 | const { headerHeight, itemHeight } = config.tableStyle; 40 | return headerHeight + itemHeight * (docEntry.members ? docEntry.members.length : 0); 41 | }; 42 | 43 | let allDocEntry: DocEntry[] = []; 44 | 45 | export function getFileDocEntries(currFileName: string, startRowIndex: number = 0, config: Config, isInit = true) { 46 | if (isInit) { 47 | allDocEntry = []; 48 | } 49 | 50 | const alreadyHadFile = allDocEntry.some(fileDocEntry => fileDocEntry.filename && fileDocEntry.filename.toLowerCase() === currFileName.toLowerCase()); 51 | if (alreadyHadFile) return; 52 | const _p = new Parser(); 53 | _p.getAllLocalMembers = Boolean(config.getAllLocalMembers); 54 | const fileDocEntry: DocEntry = _p.parse(currFileName); 55 | 56 | setLayouts(currFileName, startRowIndex, config, fileDocEntry); 57 | setLayouts(currFileName, startRowIndex, config, fileDocEntry, true); 58 | 59 | if (fileDocEntry.filename) { 60 | allDocEntry.push(fileDocEntry); 61 | } 62 | 63 | return allDocEntry; 64 | } 65 | 66 | export function setLayouts(currFileName: string, startRowIndex: number = 0, config: Config, fileDocEntry: DocEntry, setExportLayouts = false) { 67 | let currMembers = fileDocEntry.members ? [...fileDocEntry.members] : []; 68 | if (setExportLayouts) { 69 | currMembers = currMembers.filter(member => { 70 | return fileDocEntry.exportMembers.includes(member.name) 71 | }); 72 | } 73 | let prevFileTop = 0; 74 | const alreadyHadFile = allDocEntry.some(fileDocEntry => fileDocEntry.filename && fileDocEntry.filename.toLowerCase() === currFileName.toLowerCase()); 75 | if (alreadyHadFile && !setExportLayouts) return; 76 | 77 | const memberRowDepth = { 78 | maxDepth: 0, 79 | depths: {} as any 80 | }; 81 | function setDefaultMember(name: string) { 82 | if (!currMembers.some(member => member.name === name)) return; 83 | if (memberRowDepth.depths[name] === void 0) { 84 | memberRowDepth.depths[name] = 0; 85 | } 86 | } 87 | function setMemberRef(name: string, refKey: string) { 88 | if (!currMembers.some(member => member.name === refKey)) return; 89 | 90 | setDefaultMember(refKey); 91 | refKey = refKey; //`${refKey} + 1` 92 | if (memberRowDepth.depths[name] === void 0 || typeof memberRowDepth.depths[name] === "number") { 93 | memberRowDepth.depths[name] = refKey; 94 | } else if (typeof memberRowDepth.depths[name] === "string") { 95 | memberRowDepth.depths[name] = [memberRowDepth.depths[name], refKey]; 96 | } else if (Array.isArray(memberRowDepth.depths[name])) { 97 | memberRowDepth.depths[name].push(refKey); 98 | } 99 | } 100 | 101 | const { fileMaxDepth } = config; 102 | const getNewFileDocEntry = (newFileName: string) => { 103 | if (!newFileName) return; 104 | if ((typeof fileMaxDepth === "number" && startRowIndex < fileMaxDepth) || typeof fileMaxDepth !== "number") { 105 | if (!alreadyHadFile) { 106 | getFileDocEntries(newFileName, startRowIndex + 1, config, false); 107 | } 108 | } 109 | } 110 | 111 | fileDocEntry.rowIndex = startRowIndex; 112 | fileDocEntry.fileWidth = 0; 113 | fileDocEntry.fileHeight = 0; 114 | 115 | if (fileDocEntry.resolvedModules) { 116 | fileDocEntry.resolvedModules.forEach(({ resolvedFileName, isExternalLibraryImport }) => { 117 | if (resolvedFileName && !isExternalLibraryImport) { 118 | getNewFileDocEntry(resolvedFileName); 119 | } 120 | }) 121 | } 122 | 123 | if (currMembers) { 124 | currMembers.forEach((memberDocEntry: DocEntry, index) => { 125 | const { members, type } = memberDocEntry; 126 | 127 | setDefaultMember(memberDocEntry.name); 128 | // set columnIndex 129 | memberDocEntry.columnIndex = index; 130 | // set tableWidth 131 | memberDocEntry.tableWidth = getTableWidth(memberDocEntry, config); 132 | memberDocEntry.tableHeight = getTableHeight(memberDocEntry, config); 133 | 134 | if (memberDocEntry.extends) { 135 | (memberDocEntry.extends as DefaultDocEntry[]).forEach(member => { 136 | const { escapedName, name } = member; 137 | const typeName = escapedName || name; 138 | 139 | // Get ReactComponent Extends 140 | if (typeName === "Component") { 141 | const reComponent = /(?:React\.)?(?:Component)\?/im; 142 | const result = reComponent.exec(memberDocEntry.valueDeclarationText); 143 | if (result[1]) { 144 | const statePropsTypes = result[1].split(",").map(str => str.trim()); 145 | statePropsTypes.forEach(statePropsType => { 146 | setDefaultMember(statePropsType); 147 | setMemberRef(memberDocEntry.name, statePropsType); 148 | }); 149 | } 150 | } else { 151 | setMemberRef(memberDocEntry.name, typeName); 152 | } 153 | }); 154 | } 155 | 156 | // set local ref 157 | if (type === "Interface" || type === "Class") { 158 | if (!members) return; 159 | members.forEach((member: DocEntry, index) => { 160 | const escapedName = reType.test(member.type) ? member.type.match(reType)[1] : member.name; 161 | let localIndex = -1; 162 | if (escapedName) { 163 | for (const index in fileDocEntry.locals) { 164 | if (fileDocEntry.locals[index].name === escapedName) { 165 | localIndex = Number(index); 166 | 167 | setDefaultMember(escapedName); 168 | setMemberRef(memberDocEntry.name, escapedName); 169 | break; 170 | } 171 | } 172 | } 173 | if (localIndex > -1) { 174 | const { filename } = fileDocEntry.locals[localIndex]; 175 | member.filename = filename; 176 | member.isRef = true; 177 | getNewFileDocEntry(filename); 178 | } 179 | }); 180 | } 181 | }); 182 | 183 | const memberNames = Object.keys(memberRowDepth.depths); 184 | 185 | while(memberNames.some(key => typeof memberRowDepth.depths[key] !== "number")) { 186 | memberNames.forEach(key => { 187 | const refKey = memberRowDepth.depths[key]; 188 | if (refKey === 0) { 189 | } 190 | else if (typeof refKey === "string") { 191 | if (typeof memberRowDepth.depths[refKey] === "number") { 192 | memberRowDepth.depths[key] = memberRowDepth.depths[refKey] + 1; 193 | } 194 | } 195 | else if (Array.isArray(refKey)) { 196 | if (refKey.some(key => typeof key === "string")) { 197 | refKey.forEach((newKey, index) => { 198 | if (typeof newKey === "string") { 199 | if (typeof memberRowDepth.depths[newKey] === "number") { 200 | memberRowDepth.depths[key][index] = memberRowDepth.depths[newKey] + 1; 201 | } 202 | } 203 | }); 204 | } else { 205 | memberRowDepth.depths[key] = refKey.reduce((prev, current) => current > prev ? current : prev, 0) 206 | } 207 | } 208 | }); 209 | } 210 | // find row maxDepth 211 | memberRowDepth.maxDepth = memberNames.map(key => memberRowDepth[key]).reduce((prev, current) => current > prev ? current: prev, 0); 212 | 213 | // get layout 214 | function getTableRect(memberName: string) { 215 | let result = { tableWidth: 0, tableHeight: 0 }; 216 | if (fileDocEntry && currMembers) { 217 | for (const member of (currMembers as DocEntry[])) { 218 | if (member.name === memberName) { 219 | result = { tableWidth: member.tableWidth, tableHeight: member.tableHeight }; 220 | break; 221 | } 222 | } 223 | } 224 | return result; 225 | } 226 | const memberLayouts: { 227 | memberPositions?: { 228 | [key: string]: { x: number; y: number, rowIndex: number }; 229 | }, 230 | rows?: { 231 | heihgt?: number; 232 | maxWidth?: number; 233 | maxCloumn?: number; 234 | }[] 235 | } = { 236 | memberPositions: {}, 237 | rows: [] 238 | }; 239 | memberNames.forEach((memberName, index) => { 240 | const memberRowIndex = memberRowDepth.depths[memberName]; 241 | if (memberLayouts.rows[memberRowIndex] === void 0) { 242 | memberLayouts.rows[memberRowIndex] = { 243 | heihgt: -config.fileStyle.tableOffset, 244 | maxWidth: 0, 245 | maxCloumn: -1 246 | } 247 | } 248 | 249 | const rect = getTableRect(memberName); 250 | 251 | memberLayouts.memberPositions[memberName] = { x: 0, y: (prevFileTop + config.fileStyle.headerHeight + config.fileStyle.heightPadding) + (memberLayouts.rows[memberRowIndex].heihgt + config.fileStyle.tableOffset), rowIndex: memberRowIndex }; 252 | memberLayouts.rows[memberRowIndex].heihgt += rect.tableHeight + config.fileStyle.tableOffset; 253 | memberLayouts.rows[memberRowIndex].maxCloumn += 1; 254 | if (rect.tableWidth > memberLayouts.rows[memberRowIndex].maxWidth) { 255 | memberLayouts.rows[memberRowIndex].maxWidth = rect.tableWidth; 256 | } 257 | }); 258 | 259 | // set x position 260 | for (const memberName in memberLayouts.memberPositions) { 261 | const position = memberLayouts.memberPositions[memberName]; 262 | position.x = ( 263 | position.rowIndex === 0 ? 0 : ( 264 | memberLayouts.rows.slice(0, position.rowIndex).reduce((prev, current) => prev + current.maxWidth, 0) 265 | ) 266 | ) + position.rowIndex * config.fileStyle.tableOffset; 267 | } 268 | 269 | // calculator file padding 270 | fileDocEntry.fileHeight += memberLayouts.rows.reduce((prev, current) => prev < current.heihgt ? current.heihgt : prev, 0) + ( 271 | config.fileStyle.headerHeight + config.fileStyle.heightPadding * 2 272 | ); 273 | fileDocEntry.fileWidth += memberLayouts.rows.reduce((prev, current) => prev + current.maxWidth, 0) + ( 274 | config.fileStyle.tableOffset * (memberRowDepth.maxDepth) + config.fileStyle.widthPadding * 2 275 | ); 276 | 277 | 278 | if (setExportLayouts) { 279 | fileDocEntry.exportLayouts = memberLayouts; 280 | } else { 281 | fileDocEntry.memberLayouts = memberLayouts; 282 | } 283 | // fileDocEntry["memberTreeDepth"] = memberRowDepth; 284 | 285 | prevFileTop = fileDocEntry.fileHeight + config.fileStyle.fileOffset; 286 | } else { 287 | fileDocEntry.fileHeight = config.fileStyle.headerHeight; 288 | fileDocEntry.fileWidth = 500; 289 | } 290 | } 291 | 292 | export default getFileDocEntries; 293 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": false, 7 | "skipLibCheck": true, 8 | "target": "es2015", 9 | "sourceMap": false, 10 | "moduleResolution": "node", 11 | "allowJs": false, 12 | "declaration": false, 13 | "jsx": "react", 14 | "experimentalDecorators": true, 15 | "removeComments": true, 16 | "noImplicitAny": false, 17 | "rootDir": ".", 18 | "outDir": "out", 19 | "noLib": false, 20 | "resolveJsonModule": true, 21 | "lib": ["dom", "es5", "es2015", "es2016.array.include"], 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "filesGlob": [ 27 | "**/*.ts", 28 | "**/*.tsx" 29 | ], 30 | "include": [ 31 | "*.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | "server/**/*.ts", 35 | "__mocks__/**/*.ts", 36 | "__mocks__/**/*.tsx" 37 | ], 38 | "files": [ 39 | "./node_modules/react-uwp/index.d.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules", 43 | "build" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const __DEV__ = process.env.NODE_ENV !== 'production' 5 | const port = 8092 6 | 7 | module.exports = { 8 | entry: { 9 | app: [ 10 | ...(__DEV__ ? [ 11 | 'react-hot-loader/patch', 12 | `webpack-hot-middleware/client?path=/__webpack_hmr&hot=true&reload=false`, 13 | 'webpack/hot/only-dev-server' 14 | ] : []), 15 | './src/WebView/index.tsx' 16 | ] 17 | }, 18 | output: { 19 | path: path.resolve('./build'), 20 | filename: 'js/[name].js', 21 | chunkFilename: 'js/[name].js' 22 | }, 23 | resolve: { 24 | extensions: ['.webpack.js', '.js', '.jsx', '.ts', '.tsx'], 25 | modules: ['./node_modules', './src'], 26 | alias: { 27 | '@root': path.resolve(__dirname, './src') 28 | } 29 | }, 30 | resolveLoader: { 31 | moduleExtensions: ['-loader'] 32 | }, 33 | module: { 34 | rules: [{ 35 | test: /\.tsx?$/, 36 | use: { 37 | loader: 'awesome-typescript', 38 | query: { 39 | configFileName: path.resolve(__dirname, './tsconfig.json') 40 | } 41 | } 42 | }] 43 | }, 44 | devServer: { 45 | compress: true, 46 | hot: true, 47 | noInfo: true, 48 | port 49 | }, 50 | optimization: { 51 | splitChunks: { 52 | cacheGroups: { 53 | default: false, 54 | vendors: false, 55 | 56 | vendor: { 57 | name: 'vendor', 58 | chunks: 'all', 59 | test: /node_modules/, 60 | priority: 20 61 | }, 62 | 63 | common: { 64 | name: 'common', 65 | minChunks: 2, 66 | chunks: 'all', 67 | priority: 10, 68 | reuseExistingChunk: true, 69 | enforce: true 70 | } 71 | } 72 | } 73 | }, 74 | plugins: [ 75 | new webpack.optimize.OccurrenceOrderPlugin(), 76 | new webpack.HotModuleReplacementPlugin(), 77 | new webpack.DefinePlugin({ 78 | 'process.env': { 79 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) 80 | } 81 | }) 82 | ] 83 | } 84 | --------------------------------------------------------------------------------