├── .editorconfig ├── .eslintrc.json ├── .github ├── stale.yml └── workflows │ └── build.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── _config.yml ├── angular.json ├── angular.webpack.js ├── assets ├── 1.png ├── 2.png ├── 3.png └── 4.png ├── auto-update.ts ├── dev-app-update.yml ├── e2e ├── common-setup.ts ├── main.e2e.ts └── tsconfig.e2e.json ├── electron-builder.json ├── fixtures └── ng-11 │ ├── index.ts │ ├── package.json │ ├── services.ts │ ├── tsconfig.json │ └── yarn.lock ├── main.ts ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── quick-access │ │ │ ├── index.ts │ │ │ ├── quick-access-list │ │ │ │ ├── index.ts │ │ │ │ ├── quick-access-list.component.html │ │ │ │ ├── quick-access-list.component.scss │ │ │ │ ├── quick-access-list.component.ts │ │ │ │ └── quick-access-list.module.ts │ │ │ ├── quick-access.component.html │ │ │ ├── quick-access.component.scss │ │ │ ├── quick-access.component.ts │ │ │ └── quick-access.module.ts │ │ ├── state-navigation │ │ │ ├── index.ts │ │ │ ├── state-navigation.component.html │ │ │ ├── state-navigation.component.scss │ │ │ ├── state-navigation.component.ts │ │ │ └── state-navigation.module.ts │ │ └── visualizer │ │ │ ├── color-legend │ │ │ ├── color-legend.component.html │ │ │ ├── color-legend.component.scss │ │ │ ├── color-legend.component.ts │ │ │ ├── color-legend.module.ts │ │ │ ├── color-legend.ts │ │ │ └── index.ts │ │ │ ├── export-to-image.service.ts │ │ │ ├── index.ts │ │ │ ├── metadata │ │ │ ├── index.ts │ │ │ ├── metadata.component.html │ │ │ ├── metadata.component.scss │ │ │ ├── metadata.component.ts │ │ │ └── metadata.module.ts │ │ │ ├── network │ │ │ ├── index.ts │ │ │ ├── network.component.scss │ │ │ ├── network.component.ts │ │ │ ├── network.module.ts │ │ │ └── network.ts │ │ │ ├── visualizer.component.html │ │ │ ├── visualizer.component.scss │ │ │ ├── visualizer.component.ts │ │ │ └── visualizer.module.ts │ ├── home │ │ ├── file-dialog.service.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ ├── home.module.ts │ │ ├── home.ts │ │ └── index.ts │ ├── model │ │ ├── configuration.ts │ │ ├── ipc-bus.ts │ │ ├── project-proxy.ts │ │ └── state-manager.ts │ ├── shared │ │ ├── button │ │ │ ├── button.component.scss │ │ │ ├── button.component.ts │ │ │ ├── button.module.ts │ │ │ └── index.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ └── page-not-found │ │ │ │ ├── page-not-found.component.html │ │ │ │ ├── page-not-found.component.scss │ │ │ │ ├── page-not-found.component.spec.ts │ │ │ │ └── page-not-found.component.ts │ │ ├── directives │ │ │ ├── index.ts │ │ │ └── webview │ │ │ │ ├── webview.directive.spec.ts │ │ │ │ └── webview.directive.ts │ │ ├── shared.module.ts │ │ ├── spinner │ │ │ ├── index.ts │ │ │ ├── spinner.component.html │ │ │ ├── spinner.component.scss │ │ │ ├── spinner.component.ts │ │ │ └── spinner.module.ts │ │ └── utils.ts │ └── states │ │ └── state-proxy.ts ├── assets │ ├── .gitkeep │ ├── dark.theme.json │ ├── i18n │ │ └── en.json │ ├── icons │ │ ├── favicon.256x256.png │ │ ├── favicon.512x512.png │ │ ├── favicon.icns │ │ ├── favicon.ico │ │ └── favicon.png │ └── light.theme.json ├── electron │ ├── config.ts │ ├── formatters │ │ └── model-formatter.ts │ ├── helpers │ │ └── process.ts │ ├── menu │ │ ├── application_menu_template.ts │ │ └── dev_menu_template.ts │ ├── model │ │ ├── background-app.ts │ │ ├── project.ts │ │ └── symbol-index.ts │ ├── parser.ts │ ├── states │ │ ├── app-module.state.ts │ │ ├── app.state.ts │ │ ├── directive.state.ts │ │ ├── module-tree.state.ts │ │ ├── module.state.ts │ │ ├── pipe.state.ts │ │ ├── provider.state.ts │ │ ├── state.ts │ │ └── template.state.ts │ └── utils │ │ └── trie.ts ├── environments │ ├── environment.dev.ts │ ├── environment.prod.ts │ ├── environment.ts │ └── environment.web.ts ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills-test.ts ├── polyfills.ts ├── shared │ ├── data-format.ts │ ├── ipc-constants.ts │ └── themes │ │ └── color-map.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tsconfig.serve.json ├── utils.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "es2017": true 7 | }, 8 | "overrides": [ 9 | { 10 | "files": ["*.ts"], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 10, 20 | "project": [ 21 | "./tsconfig.serve.json", 22 | "./src/tsconfig.app.json", 23 | "./src/tsconfig.spec.json", 24 | "./e2e/tsconfig.e2e.json" 25 | ], 26 | "sourceType": "module", 27 | "ecmaFeatures": { 28 | "modules": true 29 | } 30 | }, 31 | "plugins": [ 32 | "@typescript-eslint", 33 | "@angular-eslint/eslint-plugin" 34 | ], 35 | "rules": { 36 | "@typescript-eslint/indent": [ 37 | "error", 2, { 38 | "SwitchCase": 1, 39 | "CallExpression": {"arguments": "first"}, 40 | "FunctionExpression": {"parameters": "first"}, 41 | "FunctionDeclaration": {"parameters": "first"} 42 | } 43 | ], 44 | "@typescript-eslint/no-empty-function": 0, 45 | "@typescript-eslint/no-explicit-any": 0, 46 | "@typescript-eslint/no-var-requires": 0, 47 | "@typescript-eslint/no-unsafe-call": 0, 48 | "@typescript-eslint/no-unsafe-member-access": 0, 49 | "@typescript-eslint/no-unsafe-assignment": 0, 50 | "@typescript-eslint/no-unsafe-return": 0, 51 | "@typescript-eslint/no-floating-promises": 0, 52 | "@typescript-eslint/semi": "error", 53 | "@angular-eslint/use-injectable-provided-in": "error", 54 | "@angular-eslint/no-attribute-decorator": "error" 55 | } 56 | }, 57 | { 58 | "files": ["*.component.html"], 59 | "parser": "@angular-eslint/template-parser", 60 | "plugins": ["@angular-eslint/template"], 61 | "rules": { 62 | "@angular-eslint/template/banana-in-box": "error", 63 | "@angular-eslint/template/no-negated-async": "error" 64 | } 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build/release 2 | on: push 3 | jobs: 4 | release: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [macos-latest, ubuntu-latest, windows-latest] 9 | steps: 10 | - name: Check out Git repository 11 | uses: actions/checkout@v1 12 | 13 | - name: Install Node.js, NPM and Yarn 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | 18 | - name: Install dependencies 19 | run: yarn && cd fixtures/ng-11 && yarn 20 | 21 | - name: gabrielbb/xvfb-action 22 | uses: GabrielBB/xvfb-action@v1 23 | with: 24 | working-directory: ./ 25 | run: yarn e2e 26 | 27 | - name: Build/release Electron app 28 | uses: samuelmeuli/action-electron-builder@v1 29 | with: 30 | # GitHub token, automatically provided to the action 31 | # (No need to define this secret in the repo settings) 32 | github_token: ${{ secrets.github_token }} 33 | 34 | # If the commit is tagged with a version (e.g. "v1.0.0"), 35 | # release the app after building 36 | release: ${{ startsWith(github.ref, 'refs/tags/v') }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /app-builds 8 | /release 9 | main.js 10 | auto-update.js 11 | utils.js 12 | src/**/*.js 13 | !src/karma.conf.js 14 | *.js.map 15 | 16 | 17 | # dependencies 18 | /node_modules 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | .vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # misc 37 | /.sass-cache 38 | /connect.lock 39 | /coverage 40 | /libpeerconnection.log 41 | npm-debug.log 42 | testem.log 43 | /typings 44 | package-lock.json 45 | 46 | # e2e 47 | /e2e/*.js 48 | !/e2e/protractor.conf.js 49 | /e2e/*.map 50 | 51 | # System Files 52 | .DS_Store 53 | Thumbs.db 54 | 55 | # direnv environment file 56 | .envrc 57 | 58 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | - windows 5 | language: node_js 6 | node_js: 7 | - 'lts/*' 8 | services: 9 | - xvfb 10 | before_script: 11 | - export DISPLAY=:99.0 12 | install: 13 | - npm set progress=false 14 | - npm install 15 | script: 16 | - ng lint 17 | - if [ "$TRAVIS_OS_NAME" != "windows" ]; then npm run test ; fi 18 | - if [ "$TRAVIS_OS_NAME" != "windows" ]; then npm run e2e ; fi 19 | - npm run build 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Electron Main Renderer", 9 | "type": "node", 10 | "request": "launch", 11 | "protocol": "inspector", 12 | // Prelaunch task compiles main.ts for Electron & starts Angular dev server. 13 | "preLaunchTask": "Build.All", 14 | "cwd": "${workspaceFolder}", 15 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 16 | "runtimeArgs": [ 17 | "--serve", 18 | ".", 19 | "--remote-debugging-port=9222" 20 | ], 21 | "windows": { 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 23 | } 24 | }, { 25 | "name": "Karma Attach Chrome", 26 | "type": "chrome", 27 | "request": "attach", 28 | "port": 9222, 29 | "webRoot": "${workspaceFolder}/", 30 | "sourceMaps": true, 31 | "timeout": 30000, 32 | "trace": true 33 | } 34 | 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build.All", 6 | "type": "shell", 7 | "command": "npm run electron:serve-tsc && ng serve", 8 | "isBackground": true, 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": { 14 | "owner": "typescript", 15 | "source": "ts", 16 | "applyTo": "closedDocuments", 17 | "fileLocation": ["relative", "${cwd}"], 18 | "pattern": "$tsc", 19 | "background": { 20 | "activeOnStart": true, 21 | "beginsPattern": "^.*", 22 | "endsPattern": "^.*Compiled successfully.*" 23 | } 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 - Maxime GRIS 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngrev 2 | 3 |

4 | ngrev 5 |

6 | 7 | Graphical tool for reverse engineering of Angular projects. It allows you to navigate in the structure of your application and observe the relationship between the different modules, providers, and directives. The tool performs **static code analysis** which means that you **don't have to run your application** in order to use it. 8 | 9 | **ngrev is not maintained by the Angular team. It's a side project developed by the open source community**. 10 | 11 | ## How to use? 12 | 13 | ### macOS 14 | 15 | 1. Go to the [releases page](https://github.com/mgechev/ngrev/releases). 16 | 2. Download the latest `*.dmg` file. 17 | 3. Install the application. 18 | 19 | The application is not signed, so you may have to explicitly allow your mac to run it in `System Preferences -> Security & Privacy -> General`. 20 | 21 | ### Linux 22 | 23 | 1. Go to the [releases page](https://github.com/mgechev/ngrev/releases). 24 | 2. Download the latest `*.AppImage` file. 25 | 3. Run the `*.AppImage` file (you may need to `chmod +x *.AppImage`). 26 | 27 | ### Windows 28 | 29 | 1. Go to the [releases page](https://github.com/mgechev/ngrev/releases). 30 | 2. Download the latest `*.exe` file. 31 | 3. Install the application. 32 | 33 | ## Creating a custom theme 34 | 35 | You can add your own theme by creating a `[theme-name].theme.json` file in Electron `[userData]/themes`. For a sample theme see [Dark](https://github.com/mgechev/ngrev/blob/master/src/assets/dark.theme.json). 36 | 37 | ### Application Requirements 38 | 39 | Your application needs to be compatible with Angular Ivy compiler. `ngrev` is not tested with versions older than v11. To stay up to date check the [update guide](https://angular.io/guide/updating) on angular.io. 40 | 41 | ### Using with Angular CLI 42 | 43 | 1. Open the Angular's application directory. 44 | 2. Make sure the dependencies are installed. 45 | 3. Open `ngrev`. 46 | 4. Click on `Select Project` and select `[YOUR_CLI_APP]/src/tsconfig.app.json`. 47 | 48 | ## Demo 49 | 50 | Demo [here](https://www.youtube.com/watch?v=sKdsxdeLWjM). 51 | 52 | Component template 53 | 54 | Themes 55 | 56 | Command + P 57 | 58 | Module Dependencies 59 | 60 | ## Release 61 | 62 | To release: 63 | 64 | 1. Update version in `package.json`. 65 | 2. `git commit -am vX.Y.Z && git tag vX.Y.Z` 66 | 3. `git push && git push --tags` 67 | 68 | ## Contributors 69 | 70 | [mgechev](https://github.com/mgechev) |[vik-13](https://github.com/vik-13) | 71 | :---: |:---: | 72 | [mgechev](https://github.com/mgechev) |[vik-13](https://github.com/vik-13) | 73 | 74 | # License 75 | 76 | MIT 77 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-electron": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-builders/custom-webpack:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets" 21 | ], 22 | "styles": [ 23 | "src/styles.scss" 24 | ], 25 | "scripts": [], 26 | "customWebpackConfig": { 27 | "path": "./angular.webpack.js" 28 | } 29 | }, 30 | "configurations": { 31 | "dev": { 32 | "optimization": false, 33 | "outputHashing": "all", 34 | "sourceMap": true, 35 | "extractCss": true, 36 | "namedChunks": false, 37 | "aot": false, 38 | "extractLicenses": true, 39 | "vendorChunk": false, 40 | "buildOptimizer": false, 41 | "fileReplacements": [ 42 | { 43 | "replace": "src/environments/environment.ts", 44 | "with": "src/environments/environment.dev.ts" 45 | } 46 | ] 47 | }, 48 | "web": { 49 | "optimization": false, 50 | "outputHashing": "all", 51 | "sourceMap": true, 52 | "extractCss": true, 53 | "namedChunks": false, 54 | "aot": false, 55 | "extractLicenses": true, 56 | "vendorChunk": false, 57 | "buildOptimizer": false, 58 | "fileReplacements": [ 59 | { 60 | "replace": "src/environments/environment.ts", 61 | "with": "src/environments/environment.web.ts" 62 | } 63 | ] 64 | }, 65 | "production": { 66 | "optimization": true, 67 | "outputHashing": "all", 68 | "sourceMap": false, 69 | "extractCss": true, 70 | "namedChunks": false, 71 | "aot": true, 72 | "extractLicenses": true, 73 | "vendorChunk": false, 74 | "buildOptimizer": true, 75 | "fileReplacements": [ 76 | { 77 | "replace": "src/environments/environment.ts", 78 | "with": "src/environments/environment.prod.ts" 79 | } 80 | ] 81 | } 82 | } 83 | }, 84 | "serve": { 85 | "builder": "@angular-builders/custom-webpack:dev-server", 86 | "options": { 87 | "browserTarget": "angular-electron:build" 88 | }, 89 | "configurations": { 90 | "dev": { 91 | "browserTarget": "angular-electron:build:dev" 92 | }, 93 | "web": { 94 | "browserTarget": "angular-electron:build:web" 95 | }, 96 | "production": { 97 | "browserTarget": "angular-electron:build:production" 98 | } 99 | } 100 | }, 101 | "extract-i18n": { 102 | "builder": "@angular-devkit/build-angular:extract-i18n", 103 | "options": { 104 | "browserTarget": "angular-electron:build" 105 | } 106 | }, 107 | "test": { 108 | "builder": "@angular-builders/custom-webpack:karma", 109 | "options": { 110 | "main": "src/test.ts", 111 | "polyfills": "src/polyfills-test.ts", 112 | "tsConfig": "src/tsconfig.spec.json", 113 | "karmaConfig": "src/karma.conf.js", 114 | "scripts": [], 115 | "styles": [ 116 | "src/styles.scss" 117 | ], 118 | "assets": [ 119 | "src/assets" 120 | ], 121 | "customWebpackConfig": { 122 | "path": "./angular.webpack.js" 123 | } 124 | } 125 | }, 126 | "lint": { 127 | "builder": "@angular-eslint/builder:lint", 128 | "options": { 129 | "eslintConfig": ".eslintrc.json", 130 | "lintFilePatterns": [ 131 | "src/**.ts", 132 | "main.ts" 133 | ] 134 | } 135 | } 136 | } 137 | }, 138 | "angular-electron-e2e": { 139 | "root": "e2e", 140 | "projectType": "application", 141 | "architect": { 142 | "lint": { 143 | "builder": "@angular-eslint/builder:lint", 144 | "options": { 145 | "eslintConfig": ".eslintrc.json", 146 | "lintFilePatterns": [ 147 | "e2e/**.ts" 148 | ] 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "defaultProject": "angular-electron", 155 | "schematics": { 156 | "@schematics/angular:component": { 157 | "prefix": "app", 158 | "style": "scss" 159 | }, 160 | "@schematics/angular:directive": { 161 | "prefix": "app" 162 | } 163 | }, 164 | "cli": { 165 | "analytics": "2e32b323-ebc1-4daf-8c94-ba87a23d0963" 166 | } 167 | } -------------------------------------------------------------------------------- /angular.webpack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom angular webpack configuration 3 | */ 4 | 5 | module.exports = (config, options) => { 6 | config.target = 'electron-renderer'; 7 | 8 | 9 | if (options.fileReplacements) { 10 | for(let fileReplacement of options.fileReplacements) { 11 | if (fileReplacement.replace !== 'src/environments/environment.ts') { 12 | continue; 13 | } 14 | 15 | let fileReplacementParts = fileReplacement['with'].split('.'); 16 | if (fileReplacementParts.length > 1 && ['web'].indexOf(fileReplacementParts[1]) >= 0) { 17 | config.target = 'web'; 18 | } 19 | break; 20 | } 21 | } 22 | 23 | return config; 24 | } 25 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/assets/3.png -------------------------------------------------------------------------------- /assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/assets/4.png -------------------------------------------------------------------------------- /auto-update.ts: -------------------------------------------------------------------------------- 1 | import { dialog } from 'electron'; 2 | import { autoUpdater } from 'electron-updater'; 3 | import { join } from 'path'; 4 | import { isDev } from './utils'; 5 | 6 | autoUpdater.autoDownload = false; 7 | 8 | autoUpdater.on('error', (error) => { 9 | dialog.showErrorBox( 10 | 'Error: ', 11 | error == null ? 'unknown' : (error.stack || error).toString() 12 | ); 13 | }); 14 | 15 | autoUpdater.on('update-available', () => { 16 | dialog 17 | .showMessageBox({ 18 | type: 'info', 19 | title: 'Found Updates', 20 | message: 'Found updates, do you want update now?', 21 | buttons: ['Sure', 'No'], 22 | }) 23 | .then((update) => { 24 | if (update) { 25 | autoUpdater.downloadUpdate(); 26 | } 27 | }); 28 | }); 29 | 30 | autoUpdater.on('update-not-available', () => { 31 | console.info('Current version is up to date'); 32 | }); 33 | 34 | autoUpdater.on('update-downloaded', () => { 35 | dialog 36 | .showMessageBox({ 37 | title: 'Install Updates', 38 | message: 'Updates downloaded, application will be quit for update...', 39 | }) 40 | .then(() => setImmediate(() => autoUpdater.quitAndInstall())); 41 | }); 42 | 43 | export const checkForUpdates = (): void => { 44 | if (isDev) { 45 | autoUpdater.updateConfigPath = join(__dirname, 'dev-app-update.yml'); 46 | } 47 | autoUpdater.checkForUpdates(); 48 | }; 49 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | # Keep up to date with electron-builder.yml 2 | # https://www.json2yaml.com/ 3 | --- 4 | productName: ngrev 5 | asar: false 6 | owner: mgechev 7 | repo: ngrev 8 | provider: github 9 | directories: 10 | output: release/ 11 | files: 12 | - "**/*" 13 | - "!**/*.ts" 14 | - "!*.code-workspace" 15 | - "!LICENSE.md" 16 | - "!package.json" 17 | - "!package-lock.json" 18 | - "!yarn.json" 19 | - "!src/" 20 | - src/assets/ 21 | - src/electron/**/*.js 22 | - src/shared/**/*.js 23 | - "!e2e/" 24 | - "!hooks/" 25 | - "!angular.json" 26 | - "!_config.yml" 27 | - "!karma.conf.js" 28 | - "!tsconfig.json" 29 | - "!tslint.json" 30 | win: 31 | icon: dist/assets/icons 32 | target: 33 | - portable 34 | mac: 35 | icon: dist/assets/icons 36 | target: 37 | - dmg 38 | linux: 39 | icon: dist/assets/icons 40 | target: 41 | - AppImage 42 | -------------------------------------------------------------------------------- /e2e/common-setup.ts: -------------------------------------------------------------------------------- 1 | const Application = require('spectron').Application; 2 | const electronPath = require('electron'); // Require Electron from the binaries included in node_modules. 3 | const path = require('path'); 4 | 5 | export default function setup(): void { 6 | beforeEach(async function () { 7 | this.app = new Application({ 8 | // Your electron path can be any binary 9 | // i.e for OSX an example path could be '/Applications/MyApp.app/Contents/MacOS/MyApp' 10 | // But for the sake of the example we fetch it from our node_modules. 11 | path: electronPath, 12 | env: { RUNNING_IN_SPECTRON: '1' }, 13 | 14 | // Assuming you have the following directory structure 15 | 16 | // |__ my project 17 | // |__ ... 18 | // |__ main.js 19 | // |__ package.json 20 | // |__ index.html 21 | // |__ ... 22 | // |__ test 23 | // |__ spec.js <- You are here! ~ Well you should be. 24 | 25 | // The following line tells spectron to look and use the main.js file 26 | // and the package.json located 1 level above. 27 | args: [path.join(__dirname, '..')], 28 | webdriverOptions: {} 29 | }); 30 | 31 | await this.app.start(); 32 | }); 33 | 34 | afterEach(async function () { 35 | if (this.app && this.app.isRunning()) { 36 | await this.app.stop(); 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /e2e/main.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { join } from 'path'; 3 | import { SpectronClient } from 'spectron'; 4 | 5 | import commonSetup from './common-setup'; 6 | 7 | describe('ngrev', function () { 8 | 9 | commonSetup.apply(this); 10 | 11 | let client: SpectronClient; 12 | 13 | beforeEach(function() { 14 | client = this.app.client; 15 | }); 16 | 17 | it('creates initial windows', async function () { 18 | const count = await client.getWindowCount(); 19 | expect(count).to.equal(1); 20 | }); 21 | 22 | it('should display a button saying "Select Project"', async function () { 23 | const elem = await client.$('ngrev-home button'); 24 | const text = await elem.getText(); 25 | expect(text).to.equal('Select Project'); 26 | }); 27 | 28 | it('should parse the project',async function() { 29 | const project = join(__dirname, '..', 'fixtures', 'ng-11', 'tsconfig.json'); 30 | await this.app.electron.clipboard.writeText(project); 31 | await (await client.$('ngrev-home button')).click(); 32 | const breadcrumbLabel = await client.$('ngrev-app h2'); 33 | expect(await breadcrumbLabel.getText()).to.equal('History'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "types": [ 7 | "mocha", 8 | "node" 9 | ] 10 | }, 11 | "include": [ 12 | "**.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "ngrev", 3 | "asar": false, 4 | "directories": { 5 | "output": "release/" 6 | }, 7 | "files": [ 8 | "**/*", 9 | "!**/*.ts", 10 | "!*.code-workspace", 11 | "!LICENSE.md", 12 | "!package.json", 13 | "!package-lock.json", 14 | "!yarn.json", 15 | "!src/", 16 | "src/assets/", 17 | "src/electron/**/*.js", 18 | "src/shared/**/*.js", 19 | "!e2e/", 20 | "!hooks/", 21 | "!angular.json", 22 | "!_config.yml", 23 | "!karma.conf.js", 24 | "!tsconfig.json", 25 | "!tslint.json" 26 | ], 27 | "win": { 28 | "icon": "dist/assets/icons", 29 | "target": ["portable", "zip"] 30 | }, 31 | "mac": { 32 | "icon": "dist/assets/icons", 33 | "target": ["dmg", "zip"] 34 | }, 35 | "linux": { 36 | "icon": "dist/assets/icons", 37 | "target": ["AppImage", "zip"] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fixtures/ng-11/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Component, Inject, Injectable, InjectionToken, Directive, Pipe, PipeTransform } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { CommonModule } from '@angular/common'; 5 | import { MatExpansionModule } from '@angular/material/expansion'; 6 | import { BasicProvider, TOKEN } from './services'; 7 | 8 | 9 | @Component({ 10 | selector: 'main-component', 11 | template: '
Hello world
', 12 | providers: [{ provide: TOKEN, useValue: true }], 13 | }) 14 | export class MainComponent { 15 | visible: boolean; 16 | constructor( 17 | public p: BasicProvider, 18 | @Inject('primitive') public primitive, 19 | @Inject(TOKEN) public isTrue, 20 | ) {} 21 | } 22 | 23 | @Directive({ 24 | selector: '[main]', 25 | providers: [{ provide: TOKEN, useValue: false }] 26 | }) 27 | export class MainDirective { 28 | constructor(public p: BasicProvider) {} 29 | } 30 | 31 | @Injectable() 32 | export class CompositeProvider { 33 | constructor( 34 | public p: BasicProvider, 35 | @Inject('primitive') public primitive: string, 36 | ) {} 37 | } 38 | 39 | @Pipe({ name: 'main' }) 40 | export class MainPipe implements PipeTransform { 41 | constructor(public p: BasicProvider) {} 42 | transform(value: any) { 43 | return value; 44 | } 45 | } 46 | 47 | @NgModule({ 48 | imports: [CommonModule, BrowserModule, MatExpansionModule, BrowserAnimationsModule], 49 | exports: [MainComponent], 50 | declarations: [MainComponent, MainDirective, MainPipe], 51 | bootstrap: [MainComponent], 52 | providers: [ 53 | CompositeProvider, 54 | BasicProvider, 55 | { provide: 'primitive', useValue: '42' }, 56 | ] 57 | }) 58 | export class AppModule {} 59 | -------------------------------------------------------------------------------- /fixtures/ng-11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-11", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "", 7 | "license": "MIT", 8 | "scripts": { 9 | "postinstall": "ngcc" 10 | }, 11 | "dependencies": { 12 | "@angular/animations": "11.0.3", 13 | "@angular/cdk": "11.0.3", 14 | "@angular/common": "11.0.3", 15 | "@angular/core": "11.0.3", 16 | "@angular/forms": "11.0.3", 17 | "@angular/material": "11.0.3", 18 | "@angular/platform-browser": "11.0.3", 19 | "rxjs": "6.6.3", 20 | "zone.js": "0.10.3" 21 | }, 22 | "devDependencies": { 23 | "@angular/compiler": "11.0.9", 24 | "@angular/compiler-cli": "11.0.9" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fixtures/ng-11/services.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, InjectionToken } from "@angular/core"; 2 | 3 | @Injectable() 4 | export class BasicProvider {} 5 | 6 | export const TOKEN = new InjectionToken('token'); 7 | -------------------------------------------------------------------------------- /fixtures/ng-11/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node" 8 | }, 9 | "files": [ 10 | "index.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, screen, Menu } from 'electron'; 2 | import { getConfig, setConfigProps } from './src/electron/config'; 3 | import { devMenuTemplate } from './src/electron/menu/dev_menu_template'; 4 | import { applicationMenuTemplate } from './src/electron/menu/application_menu_template'; 5 | import { BackgroundApp } from './src/electron/model/background-app'; 6 | // import { checkForUpdates } from './auto-update'; 7 | import { isDev } from './utils'; 8 | import * as path from 'path'; 9 | import * as url from 'url'; 10 | 11 | let win: BrowserWindow | null = null; 12 | 13 | // Save userData in separate folders for each environment. 14 | // Thanks to this you can use production and development versions of the app 15 | // on same machine like those are two separate apps. 16 | if (isDev) { 17 | const userDataPath = app.getPath('userData').toString(); 18 | app.setPath('userData', userDataPath + ' (development)'); 19 | } 20 | 21 | const themeChange = (theme: string) => setConfigProps({ theme }); 22 | 23 | const libsToggle = () => { 24 | const config = getConfig(); 25 | const showLibs = !config.showLibs; 26 | setConfigProps({ showLibs }); 27 | }; 28 | 29 | const modulesOnlyToggle = () => { 30 | const config = getConfig(); 31 | const showModules = !config.showModules; 32 | setConfigProps({ showModules }); 33 | }; 34 | 35 | const menuItems = [ 36 | applicationMenuTemplate( 37 | themeChange, 38 | libsToggle, 39 | modulesOnlyToggle 40 | ) 41 | ]; 42 | if (isDev) { 43 | menuItems.push(devMenuTemplate()); 44 | } 45 | 46 | export const menus = Menu.buildFromTemplate(menuItems); 47 | 48 | function createWindow(): BrowserWindow { 49 | Menu.setApplicationMenu(menus); 50 | 51 | const size = screen.getPrimaryDisplay().workAreaSize; 52 | 53 | // Create the browser window. 54 | win = new BrowserWindow({ 55 | x: 0, 56 | y: 0, 57 | width: size.width, 58 | height: size.height, 59 | webPreferences: { 60 | nodeIntegration: true, 61 | allowRunningInsecureContent: !!isDev, 62 | contextIsolation: false, // false if you want to run 2e2 test with Spectron 63 | enableRemoteModule : true // true if you want to run 2e2 test with Spectron or use remote module in renderer context (ie. Angular) 64 | }, 65 | }); 66 | 67 | win.setTitle(require('./package.json').name); 68 | 69 | if (isDev) { 70 | 71 | win.webContents.openDevTools(); 72 | 73 | require('electron-reload')(__dirname, { 74 | electron: require(`${__dirname}/node_modules/electron`) 75 | }); 76 | win.loadURL('http://localhost:4200'); 77 | 78 | } else { 79 | win.loadURL(url.format({ 80 | pathname: path.join(__dirname, 'dist/index.html'), 81 | protocol: 'file:', 82 | slashes: true 83 | })); 84 | } 85 | 86 | // Emitted when the window is closed. 87 | win.on('closed', () => { 88 | // Dereference the window object, usually you would store window 89 | // in an array if your app supports multi windows, this is the time 90 | // when you should delete the corresponding element. 91 | win = null; 92 | }); 93 | 94 | // Enable auto updates 95 | // win.on('show', checkForUpdates); 96 | 97 | const backgroundApp = new BackgroundApp(); 98 | backgroundApp.init(getConfig()); 99 | 100 | return win; 101 | } 102 | 103 | try { 104 | // This method will be called when Electron has finished 105 | // initialization and is ready to create browser windows. 106 | // Some APIs can only be used after this event occurs. 107 | // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947 108 | app.on('ready', () => setTimeout(createWindow, 400)); 109 | 110 | // Quit when all windows are closed. 111 | app.on('window-all-closed', () => { 112 | // On OS X it is common for applications and their menu bar 113 | // to stay active until the user quits explicitly with Cmd + Q 114 | if (process.platform !== 'darwin') { 115 | app.quit(); 116 | } 117 | }); 118 | 119 | app.on('activate', () => { 120 | // On OS X it's common to re-create a window in the app when the 121 | // dock icon is clicked and there are no other windows open. 122 | if (win === null) { 123 | createWindow(); 124 | } 125 | }); 126 | 127 | } catch (e) { 128 | // Catch Error 129 | // throw e; 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrev", 3 | "version": "0.0.36", 4 | "description": "Reverse engineering of Angular apps", 5 | "homepage": "https://github.com/mgechev/ngrev", 6 | "author": { 7 | "name": "Minko Gechev", 8 | "email": "mgechev@gmail.com" 9 | }, 10 | "keywords": [ 11 | "angular", 12 | "angular 11", 13 | "electron", 14 | "nodejs", 15 | "typescript", 16 | "spectron", 17 | "eslint", 18 | "sass", 19 | "windows", 20 | "mac", 21 | "linux" 22 | ], 23 | "main": "main.js", 24 | "private": true, 25 | "scripts": { 26 | "postinstall": "electron-builder install-app-deps", 27 | "ng": "ng", 28 | "start": "npm-run-all -p electron:serve ng:serve", 29 | "build": "npm run build:app -- -c production", 30 | "build:app": "npm run electron:serve-tsc && ng build --base-href ./", 31 | "build:dev": "npm run build:app -- -c dev", 32 | "ng:serve": "ng serve -c web -o", 33 | "electron:serve-tsc": "tsc -p tsconfig.serve.json", 34 | "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve", 35 | "electron:local": "npm run build && npx electron .", 36 | "electron:build": "CSC_IDENTITY_AUTO_DISCOVERY=false npm run build && electron-builder build -mwl", 37 | "test": "ng test --watch=false", 38 | "test:watch": "ng test", 39 | "e2e": "npm run build && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts", 40 | "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", 41 | "lint": "ng lint" 42 | }, 43 | "dependencies": { 44 | "@angular/cdk": "11.0.3", 45 | "@angular/compiler": "11.0.3", 46 | "@angular/compiler-cli": "11.0.3", 47 | "@angular/core": "11.0.3", 48 | "@types/vis": "^4.17.3", 49 | "electron-updater": "4.3.5", 50 | "fs-jetpack": "^0.10.2", 51 | "fuse.js": "^2.6.2", 52 | "ngast": "0.6.2", 53 | "rxjs": "6.6.3", 54 | "sanitize-filename": "^1.6.1", 55 | "tslib": "2.0.3", 56 | "typescript": "4.0.5", 57 | "vis": "^4.18.1", 58 | "zone.js": "0.10.3" 59 | }, 60 | "devDependencies": { 61 | "@angular-builders/custom-webpack": "10.0.1", 62 | "@angular-devkit/build-angular": "0.1100.3", 63 | "@angular-eslint/builder": "0.8.0-beta.3", 64 | "@angular-eslint/eslint-plugin": "0.8.0-beta.3", 65 | "@angular-eslint/eslint-plugin-template": "0.8.0-beta.3", 66 | "@angular-eslint/schematics": "0.8.0-beta.3", 67 | "@angular-eslint/template-parser": "0.8.0-beta.3", 68 | "@angular/cli": "11.0.3", 69 | "@angular/common": "11.0.3", 70 | "@angular/forms": "11.0.3", 71 | "@angular/language-service": "11.0.3", 72 | "@angular/platform-browser": "11.0.3", 73 | "@angular/platform-browser-dynamic": "11.0.3", 74 | "@angular/router": "11.0.3", 75 | "@ngx-translate/core": "13.0.0", 76 | "@ngx-translate/http-loader": "6.0.0", 77 | "@types/chai": "4.2.14", 78 | "@types/jasmine": "3.6.2", 79 | "@types/jasminewd2": "2.0.8", 80 | "@types/mocha": "8.0.4", 81 | "@types/node": "12.12.6", 82 | "@typescript-eslint/eslint-plugin": "4.7.0", 83 | "@typescript-eslint/eslint-plugin-tslint": "4.7.0", 84 | "@typescript-eslint/parser": "4.7.0", 85 | "chai": "4.2.0", 86 | "conventional-changelog-cli": "2.1.1", 87 | "core-js": "3.6.5", 88 | "cross-env": "7.0.2", 89 | "electron": "11.1.0", 90 | "electron-builder": "22.9.1", 91 | "electron-reload": "1.5.0", 92 | "eslint": "7.13.0", 93 | "eslint-plugin-import": "2.22.1", 94 | "eslint-plugin-jsdoc": "30.7.8", 95 | "eslint-plugin-prefer-arrow": "1.2.2", 96 | "jasmine-core": "3.6.0", 97 | "jasmine-spec-reporter": "6.0.0", 98 | "karma": "~5.1.0", 99 | "karma-coverage-istanbul-reporter": "3.0.3", 100 | "karma-electron": "6.3.1", 101 | "karma-jasmine": "4.0.1", 102 | "karma-jasmine-html-reporter": "1.5.4", 103 | "mocha": "8.2.1", 104 | "npm-run-all": "4.1.5", 105 | "spectron": "13.0.0", 106 | "ts-node": "9.1.0", 107 | "wait-on": "5.0.1", 108 | "webdriver-manager": "12.1.7" 109 | }, 110 | "engines": { 111 | "node": ">=10.13.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | height: 100%; 4 | display: block; 5 | } 6 | 7 | button { 8 | position: absolute; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | NgZone, 7 | OnDestroy, 8 | ViewChild 9 | } from '@angular/core'; 10 | import { ProjectProxy } from './model/project-proxy'; 11 | import { Config, IdentifiedStaticSymbol, VisualizationConfig } from '../shared/data-format'; 12 | import { formatError } from './shared/utils'; 13 | import { Memento, StateManager } from './model/state-manager'; 14 | import { Theme } from '../shared/themes/color-map'; 15 | import { IPCBus } from './model/ipc-bus'; 16 | import { Message } from '../shared/ipc-constants'; 17 | import { fromEvent, Observable, Subscription } from 'rxjs'; 18 | import { debounceTime, filter, map, startWith, tap } from 'rxjs/operators'; 19 | import { Configuration } from './model/configuration'; 20 | import { ProjectLoadEvent } from './home'; 21 | import { BACKSPACE } from '@angular/cdk/keycodes'; 22 | import { QuickAccessComponent } from './components/quick-access'; 23 | import { KeyValue } from '@angular/common'; 24 | 25 | @Component({ 26 | selector: 'ngrev-app', 27 | templateUrl: './app.component.html', 28 | styleUrls: ['./app.component.scss'], 29 | changeDetection: ChangeDetectionStrategy.OnPush 30 | }) 31 | export class AppComponent implements AfterViewInit, OnDestroy { 32 | projectSet = false; 33 | loading = false; 34 | queryList: KeyValue[] = []; 35 | queryObject: string[] = ['value.name', 'value.filePath']; 36 | theme!: Theme; 37 | themes!: { [name: string]: Theme }; 38 | showLibs = false; 39 | showModules = true; 40 | 41 | maxWidth$: Observable; 42 | 43 | @ViewChild(QuickAccessComponent) quickAccess?: QuickAccessComponent; 44 | 45 | private _stopLoading = () => { 46 | this.loading = false; 47 | this._cd.markForCheck(); 48 | }; 49 | private _startLoading = () => { 50 | this.loading = true; 51 | this._cd.markForCheck(); 52 | }; 53 | 54 | private _keyDownSubscription: Subscription; 55 | 56 | constructor( 57 | public manager: StateManager, 58 | private _cd: ChangeDetectorRef, 59 | private _ngZone: NgZone, 60 | private _project: ProjectProxy, 61 | private _ipcBus: IPCBus, 62 | private _configuration: Configuration 63 | ) { 64 | this._configuration.getConfig().then((config: Config) => { 65 | this.themes = config.themes; 66 | this.theme = config.themes[config.theme]; 67 | this.showLibs = config.showLibs; 68 | this.showModules = config.showModules; 69 | this._cd.markForCheck(); 70 | }); 71 | 72 | this.maxWidth$ = fromEvent(window, 'resize').pipe( 73 | debounceTime(100), 74 | map(() => window.innerWidth), 75 | startWith(window.innerWidth) 76 | ); 77 | 78 | this._keyDownSubscription = fromEvent(document, 'keydown').pipe( 79 | filter((event: KeyboardEvent): boolean => !!(event.keyCode === BACKSPACE && this.quickAccess?.hidden)), 80 | tap({ 81 | next: () => { 82 | this.prevState(); 83 | } 84 | }) 85 | ).subscribe(); 86 | } 87 | 88 | ngAfterViewInit(): void { 89 | this._ipcBus.on(Message.ChangeTheme, (_: any, theme: string) => { 90 | this._ngZone.run(() => { 91 | this.theme = this.themes[theme]; 92 | this._cd.markForCheck(); 93 | }); 94 | }); 95 | this._ipcBus.on(Message.ToggleLibsMenuAction, () => { 96 | this._ngZone.run(() => { 97 | this.manager.toggleLibs().then(() => { 98 | this.manager.reloadAppState(); 99 | this._cd.markForCheck(); 100 | }); 101 | }); 102 | }); 103 | this._ipcBus.on(Message.ToggleModulesMenuAction, () => { 104 | this._ngZone.run(() => { 105 | this.manager.toggleModules().then(() => { 106 | this.manager.reloadAppState(); 107 | this._cd.markForCheck(); 108 | }); 109 | }); 110 | }); 111 | } 112 | 113 | ngOnDestroy(): void { 114 | this._keyDownSubscription.unsubscribe(); 115 | } 116 | 117 | onProject({ tsconfig }: ProjectLoadEvent): void { 118 | this.projectSet = true; 119 | this._startLoading(); 120 | this.manager 121 | .loadProject(tsconfig, this.showLibs, this.showModules) 122 | .then(() => this._project.getSymbols()) 123 | .then((symbols: IdentifiedStaticSymbol[]): KeyValue[] => { 124 | return this.queryList = symbols.map((symbol: IdentifiedStaticSymbol) => ({ key: symbol.name, value: symbol })); 125 | }) 126 | .then(this._stopLoading) 127 | .catch((error: any) => { 128 | window.require('electron').remote.dialog.showErrorBox( 129 | 'Error while parsing project', 130 | `Cannot parse your project. Make sure it's compatible with 131 | the Angular's AoT compiler. Error during parsing:\n\n${formatError(error)}` 132 | ); 133 | this._stopLoading(); 134 | }); 135 | } 136 | 137 | tryChangeState(id: string): void { 138 | this._ngZone.run(() => { 139 | this._startLoading(); 140 | this.manager 141 | .tryChangeState(id) 142 | .then(() => { 143 | this._stopLoading(); 144 | }) 145 | .catch(() => { 146 | this._stopLoading(); 147 | }); 148 | }); 149 | } 150 | 151 | selectSymbol(symbolPair: KeyValue): void { 152 | if (symbolPair && symbolPair.value) { 153 | this.tryChangeState(symbolPair.value.id); 154 | } 155 | } 156 | 157 | restoreMemento(memento: Memento): void { 158 | this._ngZone.run(() => { 159 | this.manager 160 | .restoreMemento(memento) 161 | .then(this._stopLoading) 162 | .catch(this._stopLoading); 163 | }); 164 | } 165 | 166 | get initialized(): VisualizationConfig | null { 167 | return this.manager.getCurrentState(() => { 168 | this._cd.markForCheck(); 169 | }); 170 | } 171 | 172 | prevState(): void { 173 | const mementos = this.manager.getHistory(); 174 | if (mementos.length > 1) { 175 | this.restoreMemento(mementos[mementos.length - 2]); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { VisualizerModule } from './components/visualizer/visualizer.module'; 6 | import { QuickAccessModule } from './components/quick-access'; 7 | import { SpinnerModule } from './shared/spinner'; 8 | import { HomeModule } from './home'; 9 | import { ButtonModule } from './shared/button'; 10 | import { StateNavigationModule } from './components/state-navigation'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | BrowserModule, 15 | VisualizerModule, 16 | QuickAccessModule, 17 | SpinnerModule, 18 | HomeModule, 19 | ButtonModule, 20 | StateNavigationModule 21 | ], 22 | declarations: [ 23 | AppComponent 24 | ], 25 | bootstrap: [ 26 | AppComponent 27 | ] 28 | }) 29 | export class AppModule {} 30 | -------------------------------------------------------------------------------- /src/app/components/quick-access/index.ts: -------------------------------------------------------------------------------- 1 | export * from './quick-access.module'; 2 | export * from './quick-access.component'; 3 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from './quick-access-list.module'; 2 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access-list/quick-access-list.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 10 |
  • 11 |
12 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access-list/quick-access-list.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | background: white; 3 | display: block; 4 | margin-top: 3px; 5 | width: 100%; 6 | max-height: calc(100% - 70px); 7 | border-top: 1px solid #ccc; 8 | overflow: auto; 9 | } 10 | ul { 11 | width: 100%; 12 | padding: 0; 13 | margin-top: 0; 14 | margin-bottom: 0; 15 | } 16 | li { 17 | list-style: none; 18 | padding: 16px; 19 | border-bottom: 1px solid #ccc; 20 | border-left: 1px solid #ccc; 21 | border-right: 1px solid #ccc; 22 | cursor: pointer; 23 | } 24 | .selected { 25 | background: #efefef; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access-list/quick-access-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Output, 4 | EventEmitter, 5 | ViewChildren, 6 | ElementRef, 7 | Renderer2, 8 | QueryList, 9 | Input, 10 | ChangeDetectionStrategy 11 | } from '@angular/core'; 12 | 13 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 14 | import { Theme } from '../../../../shared/themes/color-map'; 15 | import { DOWN_ARROW, ENTER, UP_ARROW } from '@angular/cdk/keycodes'; 16 | import { KeyValue } from '@angular/common'; 17 | import { IdentifiedStaticSymbol } from '../../../../shared/data-format'; 18 | 19 | const ensureVisible = (item: ElementRef) => { 20 | const domNode = item.nativeElement as HTMLLIElement; 21 | const directParent = domNode.parentNode as HTMLUListElement; 22 | const scrollParent = directParent.parentNode as HTMLElement; 23 | const nodeTop = domNode.offsetTop; 24 | const nodeBottom = nodeTop + domNode.offsetHeight; 25 | const visibleBottom = scrollParent.scrollTop + scrollParent.offsetHeight; 26 | if (nodeTop - domNode.offsetHeight < scrollParent.scrollTop) { 27 | scrollParent.scrollTop = nodeTop - domNode.offsetHeight; 28 | } 29 | if (nodeBottom > visibleBottom) { 30 | scrollParent.scrollTop = nodeBottom - scrollParent.offsetHeight - domNode.offsetHeight; 31 | } 32 | } 33 | 34 | const formatText = (text: string, highlight: string): string => { 35 | const map: {[key: string]: boolean} = {}; 36 | (highlight || '').split('').forEach((c: string) => (map[c.toLowerCase()] = true)); 37 | const textChars = text.split(''); 38 | return textChars.reduce((a: string, c: string) => { 39 | return a + (map[c.toLowerCase()] ? `${c}` : c); 40 | }, 41 | '' 42 | ); 43 | } 44 | 45 | @Component({ 46 | selector: 'ngrev-quick-access-list', 47 | templateUrl: './quick-access-list.component.html', 48 | host: { 49 | '(document:keydown)': 'onKeyDown($event)' 50 | }, 51 | styleUrls: ['./quick-access-list.component.scss'], 52 | changeDetection: ChangeDetectionStrategy.OnPush 53 | }) 54 | export class QuickAccessListComponent { 55 | 56 | @Input() theme!: Theme; 57 | @Input() highlight!: string; 58 | @Input() 59 | get data(): KeyValue[] { return this._data; } 60 | set data(value: KeyValue[]) { 61 | this._data = value; 62 | if (this.selection >= value.length) { 63 | this.highlightItem(0); 64 | } 65 | } 66 | private _data: KeyValue[] = []; 67 | 68 | @Output() select: EventEmitter> = new EventEmitter>(); 69 | @ViewChildren('items') items?: QueryList; 70 | 71 | selection = 0; 72 | 73 | constructor(private _renderer: Renderer2, private _sanitizer: DomSanitizer) {} 74 | 75 | selectItem(event: Event, element: KeyValue) { 76 | this.select.emit(element); 77 | event.stopImmediatePropagation(); 78 | } 79 | 80 | onKeyDown(event: KeyboardEvent) { 81 | let nextIdx = this.selection; 82 | if (event.keyCode === UP_ARROW) { 83 | nextIdx = this.selection - 1; 84 | if (nextIdx < 0) { 85 | nextIdx = this.data.length - 1; 86 | } 87 | } 88 | if (event.keyCode === DOWN_ARROW) { 89 | nextIdx = (this.selection + 1) % this.data.length; 90 | } 91 | if (event.keyCode === ENTER && this.data[this.selection]) { 92 | this.select.emit(this.data[this.selection]); 93 | } 94 | this.highlightItem(nextIdx); 95 | } 96 | 97 | formatText(text: string): SafeHtml { 98 | return this._sanitizer.bypassSecurityTrustHtml(formatText(text, this.highlight)); 99 | } 100 | 101 | private highlightItem(idx: number) { 102 | if (!this.items) return; 103 | this.unHighlightItem(this.selection); 104 | const elements = this.items.toArray(); 105 | const item = elements[idx]; 106 | if (item) { 107 | this._renderer.addClass(item.nativeElement, 'selected'); 108 | ensureVisible(item); 109 | } 110 | this.selection = idx; 111 | } 112 | 113 | private unHighlightItem(idx: number) { 114 | if (!this.items) return; 115 | const elements = this.items.toArray(); 116 | const item = elements[idx]; 117 | if (item) { 118 | this._renderer.removeClass(item.nativeElement, 'selected'); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access-list/quick-access-list.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { QuickAccessListComponent } from './quick-access-list.component'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | QuickAccessListComponent 8 | ], 9 | imports: [ 10 | CommonModule 11 | ], 12 | exports: [ 13 | QuickAccessListComponent 14 | ] 15 | }) 16 | export class QuickAccessListModule {} 17 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access.component.html: -------------------------------------------------------------------------------- 1 |
7 | 12 | 13 | 20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | margin: auto; 3 | margin-top: 45px; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | bottom: 0; 8 | right: 0; 9 | width: 70%; 10 | max-width: 600px; 11 | height: calc(100% - 45px); 12 | z-index: 15; 13 | 14 | &.hidden { 15 | display: none; 16 | } 17 | } 18 | .fuzzy-box { 19 | padding: 5px; 20 | width: 100%; 21 | max-height: 80%; 22 | background: #fff; 23 | display: flex; 24 | flex-direction: column; 25 | } 26 | .fuzzy-box input { 27 | max-height: 60px; 28 | font-size: 35px; 29 | outline: none; 30 | padding: 7px; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, ElementRef, 5 | EventEmitter, 6 | HostBinding, 7 | Input, OnDestroy, 8 | Output, 9 | ViewChild 10 | } from '@angular/core'; 11 | import { Theme } from '../../../shared/themes/color-map'; 12 | import { CONTROL, DOWN_ARROW, ESCAPE, META, P, UP_ARROW } from '@angular/cdk/keycodes'; 13 | import { fromEvent, Observable, Subject } from 'rxjs'; 14 | import { filter, map, takeUntil, tap } from 'rxjs/operators'; 15 | import { KeyValue } from '@angular/common'; 16 | import { IdentifiedStaticSymbol } from '../../../shared/data-format'; 17 | 18 | declare const require: any; 19 | const Fuse = require('fuse.js'); 20 | 21 | const MetaKeyCodes = [META, CONTROL]; 22 | 23 | @Component({ 24 | selector: 'ngrev-quick-access', 25 | templateUrl: './quick-access.component.html', 26 | styleUrls: ['./quick-access.component.scss'], 27 | changeDetection: ChangeDetectionStrategy.OnPush 28 | }) 29 | export class QuickAccessComponent implements OnDestroy { 30 | @Input() theme!: Theme; 31 | 32 | @Input() 33 | set queryObject(query: string[]) { 34 | let list = []; 35 | if (this.fuse) { 36 | list = this.fuse.list; 37 | } 38 | this.fuse = new Fuse(list, { keys: query }); 39 | } 40 | 41 | @Input() 42 | set queryList(symbols: KeyValue[]) { 43 | this.fuse.set(symbols); 44 | } 45 | 46 | @Output() select: EventEmitter> = new EventEmitter>(); 47 | 48 | @HostBinding('class.hidden') hidden: boolean = true; 49 | 50 | @ViewChild('input', {static: false}) 51 | set input(value: ElementRef) { 52 | value?.nativeElement.focus(); 53 | } 54 | 55 | searchQuery$: Subject = new Subject(); 56 | searchResult$: Observable; 57 | 58 | private metaKeyDown = 0; 59 | private fuse: typeof Fuse = new Fuse([], { keys: ['name', 'filePath'] }); 60 | private _unsubscribe: Subject = new Subject(); 61 | 62 | constructor(private _changeDetectorRef: ChangeDetectorRef) { 63 | fromEvent(document, 'click').pipe( 64 | filter(() => !this.hidden), 65 | tap({ 66 | next: () => { 67 | this.hide(); 68 | } 69 | }), 70 | takeUntil(this._unsubscribe) 71 | ).subscribe(); 72 | 73 | fromEvent(document, 'keydown').pipe( 74 | tap((event: KeyboardEvent) => { 75 | if (MetaKeyCodes.indexOf(event.keyCode) >= 0) { 76 | this.metaKeyDown = event.keyCode; 77 | } 78 | if (event.keyCode === P && this.metaKeyDown) { 79 | this.show(); 80 | } 81 | if (event.keyCode === ESCAPE) { 82 | this.hide(); 83 | } 84 | if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { 85 | event.preventDefault(); 86 | } 87 | }), 88 | takeUntil(this._unsubscribe) 89 | ).subscribe(); 90 | 91 | fromEvent(document, 'keyup').pipe( 92 | tap((event: KeyboardEvent) => { 93 | if (MetaKeyCodes.indexOf(event.keyCode) >= 0 && this.metaKeyDown === event.keyCode) { 94 | this.metaKeyDown = 0; 95 | } 96 | }), 97 | takeUntil(this._unsubscribe) 98 | ).subscribe(); 99 | 100 | this.searchResult$ = this.searchQuery$.pipe( 101 | map((searchQuery: string) => { 102 | return this.fuse.search(searchQuery); 103 | }) 104 | ); 105 | } 106 | 107 | ngOnDestroy(): void { 108 | this._unsubscribe.next(); 109 | this._unsubscribe.complete(); 110 | } 111 | 112 | updateKeyword(searchText: string) { 113 | this.searchQuery$.next(searchText); 114 | } 115 | 116 | get boxShadow() { 117 | return `0 0 11px 3px ${this.theme.fuzzySearch.shadowColor}`; 118 | } 119 | 120 | hide() { 121 | this.hidden = true; 122 | this._changeDetectorRef.markForCheck(); 123 | } 124 | 125 | private show() { 126 | this.hidden = false; 127 | this._changeDetectorRef.markForCheck(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/app/components/quick-access/quick-access.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { QuickAccessComponent } from './quick-access.component'; 3 | import { QuickAccessListModule } from './quick-access-list'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | QuickAccessListModule, 10 | CommonModule, 11 | FormsModule 12 | ], 13 | declarations: [ 14 | QuickAccessComponent 15 | ], 16 | exports: [ 17 | QuickAccessComponent 18 | ] 19 | }) 20 | export class QuickAccessModule {} 21 | -------------------------------------------------------------------------------- /src/app/components/state-navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state-navigation.module'; 2 | -------------------------------------------------------------------------------- /src/app/components/state-navigation/state-navigation.component.html: -------------------------------------------------------------------------------- 1 |

History

2 |
    3 |
  • 12 | 13 | 16 | {{ memento.state.title }} 17 | 18 |
  • 19 |
20 | -------------------------------------------------------------------------------- /src/app/components/state-navigation/state-navigation.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: absolute; 3 | top: 30px; 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | z-index: 10; 8 | } 9 | 10 | h2 { 11 | text-align: center; 12 | display: block; 13 | width: 100%; 14 | padding: 0; 15 | margin: 0; 16 | margin-bottom: 13px; 17 | font-size: 13px; 18 | font-weight: 400; 19 | } 20 | 21 | ul { 22 | list-style: none; 23 | margin: auto; 24 | padding: 0; 25 | display: block; 26 | } 27 | 28 | li.state { 29 | width: 20px; 30 | height: 20px; 31 | border-radius: 50%; 32 | display: inline-block; 33 | margin-left: 2px; 34 | position: relative; 35 | cursor: pointer; 36 | border-width: 1px; 37 | border-style: solid; 38 | } 39 | 40 | .tooltip { 41 | display: none; 42 | position: absolute; 43 | text-align: center; 44 | background: rgba(0, 0, 0, 0.8); 45 | color: #fff; 46 | padding: 5px; 47 | border-radius: 3px; 48 | top: 24px; 49 | } 50 | 51 | .visible { 52 | display: block; 53 | font-size: 10px; 54 | } 55 | 56 | li.meta { 57 | width: 30px; 58 | height: 20px; 59 | display: inline-block; 60 | position: relative; 61 | } 62 | 63 | .label { 64 | position: absolute; 65 | top: -2px; 66 | left: 13px; 67 | font-size: 16px; 68 | display: block; 69 | } 70 | -------------------------------------------------------------------------------- /src/app/components/state-navigation/state-navigation.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { VisualizationConfig } from '../../../shared/data-format'; 3 | import { BoxTheme, DefaultColor, Theme } from '../../../shared/themes/color-map'; 4 | import { Memento } from '../../model/state-manager'; 5 | import { coerceNumberProperty } from '@angular/cdk/coercion'; 6 | 7 | const BoxWidth = 40; 8 | 9 | const dummyConfig: VisualizationConfig = { 10 | title: '...', 11 | graph: { 12 | nodes: [], 13 | edges: [] 14 | } 15 | }; 16 | 17 | const MetaMemento = new Memento(dummyConfig); 18 | 19 | @Component({ 20 | selector: 'ngrev-state-navigation', 21 | templateUrl: './state-navigation.component.html', 22 | styleUrls: ['./state-navigation.component.scss'], 23 | changeDetection: ChangeDetectionStrategy.OnPush 24 | }) 25 | export class StateNavigationComponent { 26 | @Input() states: Memento[] = []; 27 | 28 | @Input() 29 | get maxWidth(): number { return this._maxWidth; } 30 | set maxWidth(value: number) { 31 | this._maxWidth = coerceNumberProperty(value); 32 | } 33 | private _maxWidth!: number; 34 | 35 | @Input() theme!: Theme; 36 | 37 | @Output() select: EventEmitter = new EventEmitter(); 38 | 39 | visibleTooltip = -1; 40 | 41 | showTooltip(idx: number) { 42 | this.visibleTooltip = idx; 43 | } 44 | 45 | hideTooltip(idx: number) { 46 | this.visibleTooltip = -1; 47 | } 48 | 49 | changeState(state: Memento) { 50 | this.select.next(state); 51 | } 52 | 53 | isMetaState(state: Memento) { 54 | return state === MetaMemento; 55 | } 56 | 57 | getBackgroundColor(memento: Memento) { 58 | if (this.isMetaState(memento)) { 59 | return 'transparent'; 60 | } else { 61 | const nodes = memento.state.graph.nodes; 62 | const first = nodes[0]; 63 | if (first && first.type) { 64 | const config: BoxTheme = this.theme[(first.type.type as keyof Theme)] as BoxTheme; 65 | if (config) { 66 | return config.color.background; 67 | } 68 | } 69 | return DefaultColor.color.background; 70 | } 71 | } 72 | 73 | getRenderableItems() { 74 | const width = this.maxWidth; 75 | if (!width) { 76 | return []; 77 | } else { 78 | const maxBoxes = Math.floor(width / BoxWidth); 79 | if (maxBoxes === 0) { 80 | return []; 81 | } 82 | if (maxBoxes >= this.states.length) { 83 | return this.states; 84 | } 85 | const firstHalf = Math.floor(maxBoxes / 2); 86 | // -1 because of the meta box 87 | const secondHalf = maxBoxes - firstHalf - 1; 88 | return this.states 89 | .slice(0, firstHalf) 90 | .concat(MetaMemento) 91 | .concat(this.states.slice(this.states.length - secondHalf, this.states.length)); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/components/state-navigation/state-navigation.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StateNavigationComponent } from './state-navigation.component'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | StateNavigationComponent 8 | ], 9 | imports: [ 10 | CommonModule 11 | ], 12 | exports: [ 13 | StateNavigationComponent 14 | ] 15 | }) 16 | export class StateNavigationModule {} 17 | -------------------------------------------------------------------------------- /src/app/components/visualizer/color-legend/color-legend.component.html: -------------------------------------------------------------------------------- 1 |
8 |

Legend

9 |
10 |
11 |
{{ color.label }}
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/components/visualizer/color-legend/color-legend.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: relative; 4 | } 5 | h1 { 6 | font-size: 13px; 7 | margin: 0; 8 | margin-bottom: 5px; 9 | color: #555; 10 | } 11 | section { 12 | position: absolute; 13 | left: 10px; 14 | bottom: 10px; 15 | padding: 5px; 16 | border: 1px solid #999; 17 | padding-left: 10px; 18 | padding-right: 10px; 19 | background-color: rgba(255, 255, 255, 0.8); 20 | transition: 0.2s opacity; 21 | opacity: 1; 22 | } 23 | .color { 24 | width: 18px; 25 | height: 6px; 26 | margin-top: 3px; 27 | margin-right: 10px; 28 | border: 1px solid #999; 29 | } 30 | .color-label { 31 | font-size: 11px; 32 | } 33 | .colors-wrapper { 34 | display: flex; 35 | } 36 | .hidden { 37 | opacity: 0; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/components/visualizer/color-legend/color-legend.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { Theme } from '../../../../shared/themes/color-map'; 3 | import { ColorLegend } from './color-legend'; 4 | 5 | @Component({ 6 | selector: 'ngrev-color-legend', 7 | templateUrl: './color-legend.component.html', 8 | styleUrls: ['./color-legend.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ColorLegendComponent { 12 | @Input() theme!: Theme; 13 | 14 | @Input() 15 | get colors() { return this._colors || []; } 16 | set colors(value: ColorLegend) { 17 | this._colors = value; 18 | } 19 | private _colors: ColorLegend = []; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/visualizer/color-legend/color-legend.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ColorLegendComponent } from './color-legend.component'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | ColorLegendComponent 8 | ], 9 | imports: [ 10 | CommonModule 11 | ], 12 | exports: [ 13 | ColorLegendComponent 14 | ] 15 | }) 16 | export class ColorLegendModule {} 17 | -------------------------------------------------------------------------------- /src/app/components/visualizer/color-legend/color-legend.ts: -------------------------------------------------------------------------------- 1 | export type Color = { color: string; label: string }; 2 | export type ColorLegend = Color[]; 3 | -------------------------------------------------------------------------------- /src/app/components/visualizer/color-legend/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color-legend.module'; 2 | export * from './color-legend'; 3 | -------------------------------------------------------------------------------- /src/app/components/visualizer/export-to-image.service.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "../../../shared/ipc-constants"; 2 | import { IPCBus } from "../../model/ipc-bus"; 3 | import { Injectable } from "@angular/core"; 4 | declare const require: any; 5 | const sanitizeFilename = require("sanitize-filename"); 6 | 7 | interface SaveDialogType { 8 | canceled: boolean; 9 | filePath?: string; 10 | bookmark?: string; 11 | } 12 | 13 | const arrayBufferToBuffer = (ab: ArrayBuffer): Buffer => { 14 | const buffer = new window.Buffer(ab.byteLength); 15 | const view = new Uint8Array(ab); 16 | for (let i = 0; i < buffer.length; ++i) { 17 | buffer[i] = view[i]; 18 | } 19 | return buffer; 20 | } 21 | 22 | const blobCallback = (title: string): (b: any) => void => { 23 | return (b: any) => { 24 | const fileReader = new FileReader(); 25 | fileReader.onloadend = () => { 26 | const data = { 27 | name: sanitizeFilename( 28 | title.toLowerCase().replace(/\s/g, "-") 29 | ), 30 | image: fileReader.result as ArrayBuffer, 31 | format: 'png', 32 | }; 33 | window.require('electron').remote.dialog 34 | .showSaveDialog(window.require('electron').remote.BrowserWindow.getAllWindows()[0], { 35 | title: 'Export to Image', 36 | defaultPath: sanitizeFilename(data.name + "." + data.format), 37 | }) 38 | .then((result: SaveDialogType) => { 39 | const fs = window.require('fs'); 40 | if (data.image && result.filePath) { 41 | fs.writeFileSync(result.filePath, arrayBufferToBuffer(data.image)); 42 | } 43 | }); 44 | }; 45 | fileReader.readAsArrayBuffer(b); 46 | } 47 | } 48 | 49 | export interface VisualizerState { 50 | canvas: HTMLCanvasElement; 51 | title: string; 52 | } 53 | 54 | @Injectable() 55 | export class ExportToImage { 56 | private ipcCallback?: () => void; 57 | private visState?: VisualizerState; 58 | 59 | constructor(private ipcBus: IPCBus) {} 60 | 61 | enable(state: VisualizerState) { 62 | this.disable(); 63 | this.visState = state; 64 | this.init(); 65 | this.ipcBus.send(Message.EnableExport).catch(); 66 | } 67 | 68 | disable() { 69 | this.ipcCallback && this.ipcCallback(); 70 | this.ipcBus.send(Message.DisableExport).catch(); 71 | } 72 | 73 | private init() { 74 | this.ipcCallback = this.ipcBus.on(Message.SaveImage, () => { 75 | this.visState?.canvas.toBlob(blobCallback(this.visState.title), 'image/png'); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/components/visualizer/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/app/components/visualizer/index.ts -------------------------------------------------------------------------------- /src/app/components/visualizer/metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metadata.module'; 2 | -------------------------------------------------------------------------------- /src/app/components/visualizer/metadata/metadata.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
NameValue
{{pair.key}}{{pair.value}}
13 | -------------------------------------------------------------------------------- /src/app/components/visualizer/metadata/metadata.component.scss: -------------------------------------------------------------------------------- 1 | table { 2 | bottom: 7px; 3 | right: 3px; 4 | position: absolute; 5 | background-color: rgba(255, 255, 255, 0.8); 6 | font-size: 13px; 7 | transition: 0.2s opacity; 8 | opacity: 1; 9 | } 10 | td, th { 11 | border: 1px solid #ccc; 12 | padding: 8px; 13 | } 14 | thead { 15 | font-weight: bold; 16 | } 17 | .hidden { 18 | opacity: 0; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/visualizer/metadata/metadata.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { Metadata } from '../../../../shared/data-format'; 3 | import { Theme } from '../../../../shared/themes/color-map'; 4 | 5 | @Component({ 6 | selector: 'ngrev-metadata-view', 7 | templateUrl: './metadata.component.html', 8 | styleUrls: ['./metadata.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class MetadataComponent { 12 | @Input() theme!: Theme; 13 | @Input() metadata!: Metadata | null; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/visualizer/metadata/metadata.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MetadataComponent } from './metadata.component'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | MetadataComponent 8 | ], 9 | imports: [ 10 | CommonModule 11 | ], 12 | exports: [ 13 | MetadataComponent 14 | ] 15 | }) 16 | export class MetadataModule {} 17 | -------------------------------------------------------------------------------- /src/app/components/visualizer/network/index.ts: -------------------------------------------------------------------------------- 1 | export * from './network.module'; 2 | -------------------------------------------------------------------------------- /src/app/components/visualizer/network/network.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | height: 100%; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/components/visualizer/network/network.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | ElementRef, 5 | EventEmitter, 6 | Input, 7 | NgZone, 8 | OnDestroy, 9 | Output 10 | } from '@angular/core'; 11 | import { NetworkConfig } from './network'; 12 | import { Network } from 'vis'; 13 | import { ExportToImage } from '../export-to-image.service'; 14 | import { IPCBus } from '../../../model/ipc-bus'; 15 | import { Message } from '../../../../shared/ipc-constants'; 16 | 17 | @Component({ 18 | selector: 'ngrev-network', 19 | template: '', 20 | styleUrls: ['./network.component.scss'], 21 | changeDetection: ChangeDetectionStrategy.OnPush 22 | }) 23 | export class NetworkComponent implements OnDestroy { 24 | @Input() 25 | get network(): NetworkConfig | undefined { return this._network; } 26 | set network(value: NetworkConfig | undefined) { 27 | if (!value) { 28 | return; 29 | } 30 | this._ngZone.runOutsideAngular(() => { 31 | const scale: number = this._instance.getScale(); 32 | const position: {x: number, y: number} = this._instance.getViewPosition(); 33 | 34 | this._instance.setData({ 35 | nodes: value.nodes, edges: value.edges 36 | }); 37 | 38 | this._instance.setOptions(value.options); 39 | 40 | if (this._network?.title === value.title) { 41 | this._instance.moveTo({ 42 | scale, 43 | position 44 | }); 45 | } else { 46 | this._instance.fit(); 47 | } 48 | }); 49 | 50 | this._exportToImage.enable({ 51 | title: value.title, 52 | canvas: this._elementRef.nativeElement.querySelector('canvas') 53 | }); 54 | 55 | this._network = value; 56 | } 57 | 58 | @Output() select: EventEmitter = new EventEmitter(); 59 | @Output() highlight: EventEmitter = new EventEmitter(); 60 | @Output() contextMenu: EventEmitter = new EventEmitter(); 61 | 62 | private _network?: NetworkConfig; 63 | private _instance!: Network; 64 | private _clickTimeout: any; 65 | private _fitViewListener!: () => void; 66 | 67 | constructor(private _elementRef: ElementRef, 68 | private _exportToImage: ExportToImage, 69 | private _ipcBus: IPCBus, 70 | private _ngZone: NgZone) { 71 | this._ngZone.runOutsideAngular(() => { 72 | this._instance = new Network(this._elementRef.nativeElement, {}); 73 | 74 | this._instance.on('doubleClick', this.selectNode.bind(this)); 75 | this._instance.on('click', this.highlightNode.bind(this)); 76 | this._instance.on('oncontext', this.nodeContext.bind(this)); 77 | 78 | this._fitViewListener = this._ipcBus.on(Message.FitView, () => { 79 | this._instance.fit(); 80 | }); 81 | }); 82 | } 83 | 84 | ngOnDestroy(): void { 85 | if (this._instance) { 86 | this._instance.destroy(); 87 | this._exportToImage.disable(); 88 | this._fitViewListener(); 89 | } 90 | } 91 | 92 | selectNode(e: any): void { 93 | clearTimeout(this._clickTimeout); 94 | e.event.preventDefault(); 95 | if (e.nodes && e.nodes[0]) { 96 | this._ngZone.run(() => { 97 | this.select.emit(e.nodes[0]); 98 | }); 99 | } 100 | } 101 | 102 | highlightNode(e: any): void { 103 | this._clickTimeout = setTimeout(() => { 104 | if (e.nodes && e.nodes[0]) { 105 | this._ngZone.run(() => { 106 | this.highlight.emit(e.nodes[0]); 107 | }); 108 | } 109 | }, 200) as any; 110 | } 111 | 112 | nodeContext(e: any): void { 113 | const node = this._instance.getNodeAt({ 114 | x: e.event.layerX, 115 | y: e.event.layerY 116 | }) as string; 117 | this._ngZone.run(() => { 118 | this.contextMenu.emit(node); 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/app/components/visualizer/network/network.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NetworkComponent } from './network.component'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | NetworkComponent 7 | ], 8 | exports: [ 9 | NetworkComponent 10 | ] 11 | }) 12 | export class NetworkModule {} 13 | -------------------------------------------------------------------------------- /src/app/components/visualizer/network/network.ts: -------------------------------------------------------------------------------- 1 | import { DataSet, Edge, Node, Options } from 'vis'; 2 | 3 | export interface NetworkConfig { 4 | title: string; 5 | nodes: Node[] | DataSet; 6 | edges: Edge[] | DataSet; 7 | options: Options; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/components/visualizer/visualizer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/components/visualizer/visualizer.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | height: 100%; 4 | display: block; 5 | position: relative; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/visualizer/visualizer.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { DataSet } from 'vis'; 3 | 4 | import { Direction, Layout, Metadata, SymbolTypes, VisualizationConfig, Node } from '../../../shared/data-format'; 5 | import { BoxColor, BoxTheme, DefaultColor, Theme } from '../../../shared/themes/color-map'; 6 | import { Color, ColorLegend } from './color-legend'; 7 | import { StateManager } from '../../model/state-manager'; 8 | import { combineLatest, ReplaySubject } from 'rxjs'; 9 | import { map, tap } from 'rxjs/operators'; 10 | import { NetworkConfig } from './network/network'; 11 | 12 | export const TypeToNameMap: { [key: string]: string } = { 13 | [SymbolTypes.Component]: 'Component', 14 | [SymbolTypes.Directive]: 'Directive', 15 | [SymbolTypes.ComponentWithDirective]: 'Component with Directive', 16 | [SymbolTypes.HtmlElement]: 'HTML element', 17 | [SymbolTypes.HtmlElementWithDirective]: 'HTML element with Directive', 18 | [SymbolTypes.ComponentOrDirective]: 'Component or Directive', 19 | [SymbolTypes.Meta]: 'Meta', 20 | [SymbolTypes.Pipe]: 'Pipe', 21 | [SymbolTypes.Module]: 'Module', 22 | [SymbolTypes.LazyModule]: 'Lazy Module', 23 | [SymbolTypes.Provider]: 'Provider' 24 | }; 25 | 26 | @Component({ 27 | selector: 'ngrev-visualizer', 28 | templateUrl: './visualizer.component.html', 29 | styleUrls: ['./visualizer.component.scss'], 30 | changeDetection: ChangeDetectionStrategy.OnPush 31 | }) 32 | export class VisualizerComponent { 33 | @Input() 34 | set data(value: VisualizationConfig) { 35 | this._data.next(value); 36 | } 37 | 38 | @Input() 39 | set theme(value: Theme) { 40 | this.theme$.next(value); 41 | } 42 | 43 | @Output() select: EventEmitter = new EventEmitter(); 44 | @Output() highlight: EventEmitter = new EventEmitter(); 45 | 46 | usedColors?: ColorLegend; 47 | metadata: Metadata | null = null; 48 | networkConfig?: NetworkConfig; 49 | theme$: ReplaySubject = new ReplaySubject(1); 50 | 51 | private _data: ReplaySubject> = new ReplaySubject>(); 52 | 53 | constructor(private _manager: StateManager, private _changeDetectorRef: ChangeDetectorRef) { 54 | combineLatest([ 55 | this._data, 56 | this.theme$ 57 | ]).pipe( 58 | map(([data, theme]: [VisualizationConfig, Theme]) => { 59 | const graph = data.graph; 60 | const colors = new Map(); 61 | const nodes = new DataSet( 62 | graph.nodes.map((node: Node): Node => { 63 | const type = (node.type || { type: SymbolTypes.Unknown }).type; 64 | const styles: BoxTheme = (theme[type as keyof Theme] as BoxTheme)|| DefaultColor; 65 | const color = styles.color.background; 66 | const label = TypeToNameMap[type as string] || 'Unknown'; 67 | colors.set(type, { color, label }); 68 | return Object.assign({}, node, styles); 69 | }) 70 | ); 71 | 72 | const edges = new DataSet( 73 | graph.edges.map(e => { 74 | const copy = Object.assign({}, e); 75 | if (e.direction === Direction.To) { 76 | (e as any).arrows = 'to'; 77 | } else if (e.direction === Direction.From) { 78 | (e as any).arrows = 'from'; 79 | } else if (e.direction === Direction.Both) { 80 | (e as any).arrows = 'from, to'; 81 | } 82 | (e as any).color = theme.arrow; 83 | (e as any).labelHighlightBold = false; 84 | (e as any).selectionWidth = 0.5; 85 | return e; 86 | }) 87 | ); 88 | let layout: any = { 89 | hierarchical: { 90 | sortMethod: 'directed', 91 | enabled: true, 92 | direction: 'LR', 93 | edgeMinimization: true, 94 | parentCentralization: true, 95 | nodeSpacing: 50 96 | } 97 | }; 98 | if (data.layout === Layout.Regular) { 99 | layout = { 100 | hierarchical: { 101 | enabled: false 102 | }, 103 | improvedLayout: true, 104 | randomSeed: 2 105 | }; 106 | } 107 | if (data.layout === Layout.HierarchicalUDDirected) { 108 | layout.improvedLayout = true; 109 | layout.hierarchical.direction = 'UD'; 110 | layout.hierarchical.nodeSpacing = 200; 111 | layout.hierarchical.sortMethod = 'directed'; 112 | } 113 | 114 | return { 115 | title: data.title, 116 | layout, 117 | nodes, 118 | edges, 119 | colors 120 | } 121 | }), 122 | tap(({title, layout, nodes, edges, colors}) => { 123 | this.usedColors = []; 124 | colors.forEach(val => this.usedColors!.push(val)); 125 | 126 | this.networkConfig = { 127 | title, 128 | nodes, 129 | edges, 130 | options: { 131 | interaction: { 132 | dragNodes: true 133 | }, 134 | layout, 135 | physics: { 136 | enabled: false 137 | }, 138 | nodes: { 139 | shape: 'box', 140 | shapeProperties: { 141 | borderRadius: 1, 142 | interpolation: true, 143 | borderDashes: false, 144 | useImageSize: false, 145 | useBorderWithImage: false 146 | } 147 | } 148 | } 149 | }; 150 | }) 151 | ).subscribe(); 152 | } 153 | 154 | nodeContext(node: string) { 155 | this._manager.getMetadata(node).then((metadata: Metadata) => this._showContextMenu(node, metadata)).catch(() => {}); 156 | } 157 | 158 | selectNode(node: string) { 159 | this.select.next(node); 160 | this.metadata = null; 161 | } 162 | 163 | highlightNode(node: string) { 164 | this.highlight.next(node); 165 | this.metadata = null; 166 | this._manager.getMetadata(node).then((metadata: Metadata) => { 167 | this.metadata = metadata; 168 | this._changeDetectorRef.markForCheck(); 169 | }).catch(() => {}); 170 | } 171 | 172 | private _showContextMenu(id: string, metadata: Metadata) { 173 | const { Menu, MenuItem } = window.require('electron').remote; 174 | const menu = new Menu(); 175 | const self = this; 176 | if (metadata && metadata.filePath) { 177 | menu.append( 178 | new MenuItem({ 179 | label: 'Open File', 180 | click() { 181 | if (metadata && metadata.filePath) { 182 | window.require('electron').shell.openPath(metadata.filePath); 183 | } 184 | } 185 | }) 186 | ); 187 | menu.append( 188 | new MenuItem({ 189 | type: 'separator' 190 | }) 191 | ); 192 | } 193 | menu.append( 194 | new MenuItem({ 195 | label: 'Select', 196 | click() { 197 | self.select.next(id); 198 | } 199 | }) 200 | ); 201 | if (metadata) { 202 | menu.append( 203 | new MenuItem({ 204 | label: 'View Metadata', 205 | click() { 206 | self.metadata = metadata; 207 | self._changeDetectorRef.markForCheck(); 208 | } 209 | }) 210 | ); 211 | } 212 | menu.popup({ window: window.require('electron').remote.getCurrentWindow() }); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/app/components/visualizer/visualizer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { VisualizerComponent } from './visualizer.component'; 4 | import { ExportToImage } from './export-to-image.service'; 5 | import { NetworkModule } from './network'; 6 | import { ColorLegendModule } from './color-legend'; 7 | import { MetadataModule } from './metadata'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | NetworkModule, 13 | ColorLegendModule, 14 | MetadataModule 15 | ], 16 | declarations: [ 17 | VisualizerComponent 18 | ], 19 | exports: [ 20 | VisualizerComponent 21 | ], 22 | providers: [ 23 | ExportToImage 24 | ] 25 | }) 26 | export class VisualizerModule {} 27 | -------------------------------------------------------------------------------- /src/app/home/file-dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { OpenDialogOptions } from 'electron'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class FileDialogService { 8 | open(options: OpenDialogOptions): Promise<{ filePaths: string[] }> { 9 | if (process.env.RUNNING_IN_SPECTRON) { 10 | const filePaths: string[] = [window.require('electron').clipboard.readText()]; 11 | return Promise.resolve({ filePaths }); 12 | } 13 | return window.require('electron').remote.dialog.showOpenDialog(options); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .wrapper { 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { ProjectLoadEvent } from './home'; 3 | import { FileDialogService } from './file-dialog.service'; 4 | import { Theme } from '../../shared/themes/color-map'; 5 | 6 | @Component({ 7 | selector: 'ngrev-home', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class HomeComponent { 13 | @Input() theme!: Theme; 14 | @Output() project: EventEmitter = new EventEmitter(); 15 | 16 | constructor(private _dialog: FileDialogService) {} 17 | 18 | loadProject(): void { 19 | this._dialog.open({ properties: ['openFile', 'multiSelections'] }) 20 | .then(({filePaths}: {filePaths: string[]}) => { 21 | if (filePaths && filePaths[0]) { 22 | this.project.emit({ tsconfig: filePaths[0] }); 23 | } 24 | }); 25 | } 26 | 27 | get backgroundColor(): string | undefined { 28 | if (!this.theme) { 29 | return; 30 | } 31 | return this.theme.background; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { HomeComponent } from './home.component'; 3 | import { ButtonModule } from '../shared/button'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | HomeComponent 8 | ], 9 | imports: [ 10 | ButtonModule 11 | ], 12 | exports: [ 13 | HomeComponent 14 | ] 15 | }) 16 | export class HomeModule {} 17 | -------------------------------------------------------------------------------- /src/app/home/home.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectLoadEvent { 2 | tsconfig: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.module'; 2 | export * from './home'; 3 | -------------------------------------------------------------------------------- /src/app/model/configuration.ts: -------------------------------------------------------------------------------- 1 | import { IPCBus } from './ipc-bus'; 2 | import { Message } from '../../shared/ipc-constants'; 3 | import { Injectable } from '@angular/core'; 4 | import { Config } from '../../shared/data-format'; 5 | import { DefaultTheme } from '../../shared/themes/color-map'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class Configuration { 11 | constructor(private ipcBus: IPCBus) {} 12 | 13 | getConfig(): Promise { 14 | return this.ipcBus.send(Message.Config).then((config: Config) => { 15 | config.themes = Object.assign(config.themes || {}); 16 | config.theme = config.theme || DefaultTheme; 17 | return config; 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/model/ipc-bus.ts: -------------------------------------------------------------------------------- 1 | import { Message, Status } from "../../shared/ipc-constants"; 2 | import { Injectable } from '@angular/core'; 3 | 4 | const NonBlocking: Message[] = [Message.EnableExport, Message.DisableExport]; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class IPCBus { 10 | private blocked = false; 11 | 12 | constructor() {} 13 | 14 | send(method: Message, data?: R): Promise { 15 | if (this.pending && !NonBlocking.includes(method)) { 16 | console.log("Trying to send request", method); 17 | return Promise.reject("Pending requests"); 18 | } 19 | this.blocked = true; 20 | console.log("Sending method call", method); 21 | return new Promise((resolve, reject) => { 22 | const { ipcRenderer } = window.require("electron"); 23 | ipcRenderer.once(method, (e: Message, code: Status, payload: T) => { 24 | console.log("Got response of type", method); 25 | if (code === Status.Success) { 26 | resolve(payload); 27 | } else { 28 | reject(payload); 29 | } 30 | this.blocked = false; 31 | }); 32 | ipcRenderer.send(method, data); 33 | }); 34 | } 35 | 36 | get pending() { 37 | return this.blocked; 38 | } 39 | 40 | on(event: string, cb: any): () => void { 41 | const { ipcRenderer } = window.require("electron"); 42 | ipcRenderer.addListener(event, cb); 43 | return () => ipcRenderer.removeListener(event, cb); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/model/project-proxy.ts: -------------------------------------------------------------------------------- 1 | import { IPCBus } from './ipc-bus'; 2 | import { Message } from '../../shared/ipc-constants'; 3 | import { Injectable } from '@angular/core'; 4 | import { IdentifiedStaticSymbol } from '../../shared/data-format'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ProjectProxy { 10 | constructor(private ipcBus: IPCBus) {} 11 | 12 | load(tsconfig: string, showLibs: boolean, showModules: boolean) { 13 | return this.ipcBus.send(Message.LoadProject, { tsconfig, showLibs, showModules }); 14 | } 15 | 16 | getSymbols(): Promise { 17 | return this.ipcBus.send(Message.GetSymbols); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/model/state-manager.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-async-promise-executor */ 2 | import { IPCBus } from './ipc-bus'; 3 | import { Injectable } from '@angular/core'; 4 | import { VisualizationConfig } from '../../shared/data-format'; 5 | import { ProjectProxy } from './project-proxy'; 6 | import { StateProxy } from '../states/state-proxy'; 7 | import { Message } from '../../shared/ipc-constants'; 8 | 9 | export class Memento { 10 | constructor(public state: VisualizationConfig, public dirty = false) {} 11 | } 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class StateManager { 17 | private state?: StateProxy; 18 | private history: Memento[] = []; 19 | private transitionInProgress: string | null = null; 20 | private transitionResolveQueue: { resolve: (value?: unknown) => void; reject: (value?: unknown) => void }[] = []; 21 | 22 | constructor(private project: ProjectProxy, private bus: IPCBus) {} 23 | 24 | getHistory() { 25 | return this.history; 26 | } 27 | 28 | loadProject(tsconfig: string, showLibs: boolean, showModules: boolean) { 29 | return this.project 30 | .load(tsconfig, showLibs, showModules) 31 | .then(() => (this.state = new StateProxy())) 32 | .then((proxy: StateProxy) => proxy.getData()) 33 | .then((data: VisualizationConfig) => (this.history = [...this.history, new Memento(data)])); 34 | } 35 | 36 | toggleLibs() { 37 | return this.bus.send(Message.ToggleLibs); 38 | } 39 | 40 | toggleModules() { 41 | return this.bus.send(Message.ToggleModules); 42 | } 43 | 44 | tryChangeState(id: string) { 45 | if (this.transitionInProgress && this.transitionInProgress !== id) { 46 | return Promise.reject(new Error('Cannot change state while transition is pending.')); 47 | } else if (this.transitionInProgress === id) { 48 | return new Promise((resolve, reject) => this.transitionResolveQueue.push({ resolve, reject })); 49 | } 50 | if (!this.transitionInProgress) { 51 | this.transitionInProgress = id; 52 | } 53 | if (!this.state) { 54 | return Promise.reject(new Error('Project is not loaded yet.')); 55 | } 56 | return this.state! 57 | .directStateTransfer(id) 58 | .then(() => this.state!.getData()) 59 | .then(data => { 60 | this.pushState(data); 61 | while (this.transitionResolveQueue.length) { 62 | const res = this.transitionResolveQueue.pop(); 63 | if (res) { 64 | res.resolve(data); 65 | } 66 | } 67 | this.transitionInProgress = null; 68 | return data; 69 | }) 70 | .catch(e => { 71 | while (this.transitionResolveQueue.length) { 72 | const res = this.transitionResolveQueue.pop(); 73 | if (res) { 74 | res.reject(e); 75 | } 76 | } 77 | this.transitionInProgress = null; 78 | return Promise.reject(e); 79 | }); 80 | } 81 | 82 | reloadAppState() { 83 | this.history[0].dirty = true; 84 | } 85 | 86 | getCurrentState(refreshOnReady?: () => void): VisualizationConfig | null { 87 | const last = this.history[this.history.length - 1]; 88 | if (last) { 89 | if (last.dirty) { 90 | last.dirty = false; 91 | this.state!.reload().then(state => { 92 | this.history.pop(); 93 | this.history = [...this.history, new Memento(state)]; 94 | refreshOnReady && refreshOnReady(); 95 | return state; 96 | }); 97 | } 98 | return last.state; 99 | } else { 100 | return null; 101 | } 102 | } 103 | 104 | getMetadata(nodeId: string) { 105 | return this.state!.getMetadata(nodeId); 106 | } 107 | 108 | async restoreMemento(memento: Memento) { 109 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 110 | // eslint-disable-next-line no-async-promise-executor 111 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 112 | return new Promise(async (resolve, reject) => { 113 | try { 114 | while (this.history.length) { 115 | const last = this.history[this.history.length - 1]; 116 | if (last === memento) { 117 | break; 118 | } else { 119 | await this.popState(); 120 | } 121 | } 122 | resolve(true); 123 | } catch (e) { 124 | reject(e); 125 | } 126 | }); 127 | } 128 | 129 | private pushState(data: VisualizationConfig) { 130 | this.history = [...this.history, new Memento(data)]; 131 | } 132 | 133 | private popState() { 134 | return this.state!.prevState().then(() => { 135 | this.history.pop(); 136 | this.history = [...this.history]; 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/app/shared/button/button.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | &.type-regular { 3 | width: 185px; 4 | display: block; 5 | margin: auto; 6 | cursor: pointer; 7 | background: #2196f3; 8 | padding: 13px; 9 | color: #fff; 10 | font-size: 18px; 11 | border: 2px solid #1976d2; 12 | border-radius: 25px; 13 | outline: none; 14 | 15 | &:active { 16 | background: #1976d2; 17 | } 18 | } 19 | 20 | &.type-back { 21 | z-index: 1; 22 | width: 60px; 23 | height: 30px; 24 | border: none; 25 | outline: none; 26 | border-bottom-right-radius: 7px; 27 | background: #eee; 28 | transition: 0.2s opacity; 29 | 30 | &:active { 31 | background: #ccc; 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/app/shared/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, ElementRef, Input, Renderer2 } from '@angular/core'; 2 | 3 | type ButtonType = 'regular' | 'back'; 4 | 5 | @Component({ 6 | selector: 'button[ngrev-button], a[ngrev-button]', 7 | template: '', 8 | styleUrls: ['./button.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ButtonComponent { 12 | @Input() 13 | get buttonType(): ButtonType { return this._buttonType; } 14 | set buttonType(value: ButtonType) { 15 | this._renderer.removeClass(this._elementRef.nativeElement, `type-${this._buttonType}`); 16 | this._renderer.addClass(this._elementRef.nativeElement, `type-${value}`); 17 | this._buttonType = value; 18 | } 19 | 20 | private _buttonType: ButtonType = 'regular'; 21 | 22 | constructor(private _renderer: Renderer2, private _elementRef: ElementRef) { 23 | this._renderer.addClass(this._elementRef.nativeElement, `type-${this.buttonType}`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/button/button.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ButtonComponent } from './button.component'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | ButtonComponent 7 | ], 8 | exports: [ 9 | ButtonComponent 10 | ] 11 | }) 12 | export class ButtonModule {} 13 | -------------------------------------------------------------------------------- /src/app/shared/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button.module'; 2 | -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page-not-found/page-not-found.component'; 2 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |

2 | page-not-found works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/app/shared/components/page-not-found/page-not-found.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { PageNotFoundComponent } from './page-not-found.component'; 4 | 5 | describe('PageNotFoundComponent', () => { 6 | let component: PageNotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PageNotFoundComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PageNotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './webview/webview.directive'; 2 | -------------------------------------------------------------------------------- /src/app/shared/directives/webview/webview.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { WebviewDirective } from './webview.directive'; 2 | 3 | describe('WebviewDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new WebviewDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/webview/webview.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'webview' 5 | }) 6 | export class WebviewDirective { 7 | constructor() { } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | 6 | import { PageNotFoundComponent } from './components/'; 7 | import { WebviewDirective } from './directives/'; 8 | import { FormsModule } from '@angular/forms'; 9 | 10 | @NgModule({ 11 | declarations: [PageNotFoundComponent, WebviewDirective], 12 | imports: [CommonModule, TranslateModule, FormsModule], 13 | exports: [TranslateModule, WebviewDirective, FormsModule] 14 | }) 15 | export class SharedModule {} 16 | -------------------------------------------------------------------------------- /src/app/shared/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spinner.module'; 2 | -------------------------------------------------------------------------------- /src/app/shared/spinner/spinner.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Loading...
3 |
4 | -------------------------------------------------------------------------------- /src/app/shared/spinner/spinner.component.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .loader { 13 | font-size: 36px; 14 | z-index: 3; 15 | color: #555; 16 | text-indent: -9999em; 17 | overflow: hidden; 18 | width: 1em; 19 | height: 1em; 20 | border-radius: 50%; 21 | -webkit-transform: translateZ(0); 22 | -ms-transform: translateZ(0); 23 | transform: translateZ(0); 24 | -webkit-animation: load6 1.7s infinite ease, round 1.7s infinite ease; 25 | animation: load6 1.7s infinite ease, round 1.7s infinite ease; 26 | } 27 | 28 | @-webkit-keyframes load6 { 29 | 0% { 30 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 31 | } 32 | 5%, 33 | 95% { 34 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 35 | } 36 | 10%, 37 | 59% { 38 | box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em; 39 | } 40 | 20% { 41 | box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em; 42 | } 43 | 38% { 44 | box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em; 45 | } 46 | 100% { 47 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 48 | } 49 | } 50 | 51 | @keyframes load6 { 52 | 0% { 53 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 54 | } 55 | 5%, 56 | 95% { 57 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 58 | } 59 | 10%, 60 | 59% { 61 | box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em; 62 | } 63 | 20% { 64 | box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em; 65 | } 66 | 38% { 67 | box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em; 68 | } 69 | 100% { 70 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 71 | } 72 | } 73 | 74 | @-webkit-keyframes round { 75 | 0% { 76 | -webkit-transform: rotate(0deg); 77 | transform: rotate(0deg); 78 | } 79 | 100% { 80 | -webkit-transform: rotate(360deg); 81 | transform: rotate(360deg); 82 | } 83 | } 84 | 85 | @keyframes round { 86 | 0% { 87 | -webkit-transform: rotate(0deg); 88 | transform: rotate(0deg); 89 | } 90 | 100% { 91 | -webkit-transform: rotate(360deg); 92 | transform: rotate(360deg); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/shared/spinner/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { Theme } from '../../../shared/themes/color-map'; 3 | 4 | @Component({ 5 | selector: 'ngrev-spinner', 6 | templateUrl: './spinner.component.html', 7 | styleUrls: ['./spinner.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class SpinnerComponent { 11 | @Input() theme!: Theme; 12 | 13 | get backgroundColor(): string | undefined { 14 | if (!this.theme) { 15 | return; 16 | } 17 | return this.theme.background; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/spinner/spinner.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SpinnerComponent } from './spinner.component'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | SpinnerComponent 7 | ], 8 | exports: [ 9 | SpinnerComponent 10 | ] 11 | }) 12 | export class SpinnerModule {} 13 | -------------------------------------------------------------------------------- /src/app/shared/utils.ts: -------------------------------------------------------------------------------- 1 | export const formatError = (error: unknown): string => { 2 | if (typeof error === 'string') { 3 | return error; 4 | } else { 5 | try { 6 | return JSON.stringify(error); 7 | } catch (e) { 8 | console.log('Cannot serialize the error', e); 9 | } 10 | } 11 | return ''; 12 | }; 13 | 14 | export const isMetaNodeId = (id: string): boolean => { 15 | return id.split('#').length !== 2; 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/states/state-proxy.ts: -------------------------------------------------------------------------------- 1 | import { IPCBus } from '../model/ipc-bus'; 2 | import { Message } from '../../shared/ipc-constants'; 3 | import { Metadata, VisualizationConfig } from '../../shared/data-format'; 4 | 5 | export class StateProxy { 6 | private ipcBus: IPCBus = new IPCBus(); 7 | private currentData?: VisualizationConfig; 8 | private dataDirty = true; 9 | private _active: boolean = false; 10 | 11 | get active(): boolean { 12 | return this._active; 13 | } 14 | 15 | getData(): Promise> { 16 | this._active = true; 17 | if (this.dataDirty) { 18 | return this.ipcBus.send>(Message.GetData).then((data: VisualizationConfig) => { 19 | this.dataDirty = false; 20 | this.currentData = data; 21 | return data; 22 | }); 23 | } else { 24 | return this.currentData ? Promise.resolve(this.currentData) : Promise.reject(); 25 | } 26 | } 27 | 28 | reload(): Promise> { 29 | this.dataDirty = true; 30 | return this.getData(); 31 | } 32 | 33 | nextState(id: string): Promise { 34 | return this.ipcBus.send(Message.NextState, id).then((state: any) => { 35 | this.dataDirty = true; 36 | return state; 37 | }); 38 | } 39 | 40 | getMetadata(id: string): Promise { 41 | return this.ipcBus.send(Message.GetMetadata, id); 42 | } 43 | 44 | prevState(): Promise { 45 | return this.ipcBus.send(Message.PrevState).then(state => { 46 | this.dataDirty = true; 47 | return state; 48 | }); 49 | } 50 | 51 | directStateTransfer(id: string): Promise { 52 | return this.ipcBus.send(Message.DirectStateTransition, id).then(state => { 53 | this.dataDirty = true; 54 | return state; 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/dark.theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dark", 3 | "background": "#2E3440", 4 | "historyLabel": "#D8DEE9", 5 | "fuzzySearch": { 6 | "font": "#D8DEE9", 7 | "background": "#2E3440", 8 | "border": "1px solid #999", 9 | "shadowColor": "#D8DEE9", 10 | "selected": "#4C5669" 11 | }, 12 | "legend": { 13 | "background": "#3D4455", 14 | "font": "#7EB7BB", 15 | "title": "#7EB7BB", 16 | "border": "1px solid #999" 17 | }, 18 | "backButton": { 19 | "border": "none", 20 | "background": "#4C5669", 21 | "font": "#D8DEE9" 22 | }, 23 | "arrow": { 24 | "color": "#D8DEE9", 25 | "highlight": "#E5E9F0" 26 | }, 27 | "component": { 28 | "margin": 10, 29 | "color": { 30 | "background": "#BE616B", 31 | "border": "#914A52", 32 | "highlight": { 33 | "background": "#BE616B", 34 | "border": "#914A52" 35 | } 36 | }, 37 | "labelHighlightBold": false, 38 | "font": { 39 | "color": "#FFFFFF" 40 | } 41 | }, 42 | "directive": { 43 | "margin": 10, 44 | "color": { 45 | "background": "#CF8772", 46 | "border": "#A16959", 47 | "highlight": { 48 | "background": "#CF8772", 49 | "border": "#A16959" 50 | } 51 | }, 52 | "labelHighlightBold": false, 53 | "font": { 54 | "color": "#FFFFFF" 55 | } 56 | }, 57 | "component-or-directive": { 58 | "margin": 10, 59 | "color": { 60 | "background": "#CF8772", 61 | "border": "#A16959", 62 | "highlight": { 63 | "background": "#CF8772", 64 | "border": "#A16959" 65 | } 66 | }, 67 | "labelHighlightBold": false, 68 | "font": { 69 | "color": "#FFFFFF" 70 | } 71 | }, 72 | "component-with-directive": { 73 | "margin": 10, 74 | "color": { 75 | "background": "#EACA8F", 76 | "border": "#B89F70", 77 | "highlight": { 78 | "background": "#EACA8F", 79 | "border": "#B89F70" 80 | } 81 | }, 82 | "labelHighlightBold": false, 83 | "font": { 84 | "color": "#FFFFFF" 85 | } 86 | }, 87 | "html-element": { 88 | "margin": 10, 89 | "color": { 90 | "background": "#A4BD8E", 91 | "border": "#849872", 92 | "highlight": { 93 | "background": "#A4BD8E", 94 | "border": "#849872" 95 | } 96 | }, 97 | "labelHighlightBold": false, 98 | "font": { 99 | "color": "#FFFFFF" 100 | } 101 | }, 102 | "html-element-with-directive": { 103 | "margin": 10, 104 | "color": { 105 | "background": "#B38FAC", 106 | "border": "#8B6F86", 107 | "highlight": { 108 | "background": "#B38FAC", 109 | "border": "#8B6F86" 110 | } 111 | }, 112 | "labelHighlightBold": false, 113 | "font": { 114 | "color": "#FFFFFF" 115 | } 116 | }, 117 | "module": { 118 | "margin": 10, 119 | "color": { 120 | "background": "#5F82AA", 121 | "border": "#496483", 122 | "highlight": { 123 | "background": "#5F82AA", 124 | "border": "#496483" 125 | } 126 | }, 127 | "labelHighlightBold": false, 128 | "font": { 129 | "color": "#FFFFFF" 130 | } 131 | }, 132 | "lazy-module": { 133 | "margin": 10, 134 | "color": { 135 | "background": "#90BCBB", 136 | "border": "#749897", 137 | "highlight": { 138 | "background": "#90BCBB", 139 | "border": "#749897" 140 | } 141 | }, 142 | "labelHighlightBold": false, 143 | "font": { 144 | "color": "#FFFFFF" 145 | } 146 | }, 147 | "provider": { 148 | "margin": 10, 149 | "color": { 150 | "background": "#8AC0CF", 151 | "border": "#6B95A1", 152 | "highlight": { 153 | "background": "#8AC0CF", 154 | "border": "#6B95A1" 155 | } 156 | }, 157 | "labelHighlightBold": false, 158 | "font": { 159 | "color": "#FFFFFF" 160 | } 161 | }, 162 | "pipe": { 163 | "margin": 10, 164 | "color": { 165 | "background": "#82A1C0", 166 | "border": "#637B92", 167 | "highlight": { 168 | "background": "#82A1C0", 169 | "border": "#637B92" 170 | } 171 | }, 172 | "labelHighlightBold": false, 173 | "font": { 174 | "color": "#FFFFFF" 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "PAGES": { 3 | "HOME": { 4 | "TITLE": "App works !", 5 | "GO_TO_DETAIL": "Go to Detail" 6 | }, 7 | "DETAIL": { 8 | "TITLE": "Detail page !", 9 | "BACK_TO_HOME": "Back to Home" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/favicon.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/assets/icons/favicon.256x256.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/assets/icons/favicon.512x512.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/assets/icons/favicon.icns -------------------------------------------------------------------------------- /src/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/assets/icons/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgechev/ngrev/edd1ee316a7f2a0d9ddbf94ae91bb0b624997edb/src/assets/icons/favicon.png -------------------------------------------------------------------------------- /src/assets/light.theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Light", 3 | "background": "#ffffff", 4 | "fuzzySearch": { 5 | "font": "#000", 6 | "background": "#fff", 7 | "border": "#ccc", 8 | "shadowColor": "#ccc", 9 | "selected": "#ccc" 10 | }, 11 | "historyLabel": "#000", 12 | "legend": { 13 | "background": "rgba(255, 255, 255, 0.8)", 14 | "font": "#000", 15 | "title": "#555", 16 | "border": "1px solid #999" 17 | }, 18 | "backButton": { 19 | "border": "none", 20 | "background": "#eee", 21 | "font": "#000" 22 | }, 23 | "arrow": { 24 | "color": "#555555", 25 | "highlight": "#333333" 26 | }, 27 | "component": { 28 | "margin": 10, 29 | "color": { 30 | "background": "#2196F3", 31 | "border": "#1E88E5", 32 | "highlight": { 33 | "background": "#2196F3", 34 | "border": "#1E88E5" 35 | } 36 | }, 37 | "labelHighlightBold": false, 38 | "font": { 39 | "color": "#FFFFFF" 40 | } 41 | }, 42 | "directive": { 43 | "margin": 10, 44 | "color": { 45 | "background": "#FF5722", 46 | "border": "#E64A19", 47 | "highlight": { 48 | "background": "#FF5722", 49 | "border": "#E64A19" 50 | } 51 | }, 52 | "labelHighlightBold": false, 53 | "font": { 54 | "color": "#FFFFFF" 55 | } 56 | }, 57 | "component-or-directive": { 58 | "margin": 10, 59 | "color": { 60 | "background": "#FF5722", 61 | "border": "#E64A19", 62 | "highlight": { 63 | "background": "#FF5722", 64 | "border": "#E64A19" 65 | } 66 | }, 67 | "labelHighlightBold": false, 68 | "font": { 69 | "color": "#FFFFFF" 70 | } 71 | }, 72 | "component-with-directive": { 73 | "margin": 10, 74 | "color": { 75 | "background": "#03A9F4", 76 | "border": "#039BE5", 77 | "highlight": { 78 | "background": "#03A9F4", 79 | "border": "#039BE5" 80 | } 81 | }, 82 | "labelHighlightBold": false, 83 | "font": { 84 | "color": "#FFFFFF" 85 | } 86 | }, 87 | "html-element": { 88 | "margin": 10, 89 | "color": { 90 | "background": "#00BCD4", 91 | "border": "#00ACC1", 92 | "highlight": { 93 | "background": "#00BCD4", 94 | "border": "#00ACC1" 95 | } 96 | }, 97 | "labelHighlightBold": false, 98 | "font": { 99 | "color": "#FFFFFF" 100 | } 101 | }, 102 | "html-element-with-directive": { 103 | "margin": 10, 104 | "color": { 105 | "background": "#009688", 106 | "border": "#00897B", 107 | "highlight": { 108 | "background": "#009688", 109 | "border": "#00897B" 110 | } 111 | }, 112 | "labelHighlightBold": false, 113 | "font": { 114 | "color": "#FFFFFF" 115 | } 116 | }, 117 | "module": { 118 | "margin": 10, 119 | "color": { 120 | "background": "#8BC34A", 121 | "border": "#7CB342", 122 | "highlight": { 123 | "background": "#8BC34A", 124 | "border": "#7CB342" 125 | } 126 | }, 127 | "labelHighlightBold": false, 128 | "font": { 129 | "color": "#FFFFFF" 130 | } 131 | }, 132 | "lazy-module": { 133 | "margin": 10, 134 | "color": { 135 | "background": "#009688", 136 | "border": "#7FCAC3", 137 | "highlight": { 138 | "background": "#009688", 139 | "border": "#7FCAC3" 140 | } 141 | }, 142 | "labelHighlightBold": false, 143 | "font": { 144 | "color": "#FFFFFF" 145 | } 146 | }, 147 | "provider": { 148 | "margin": 10, 149 | "color": { 150 | "background": "#CDDC39", 151 | "border": "#9E9D24", 152 | "highlight": { 153 | "background": "#CDDC39", 154 | "border": "#9E9D24" 155 | } 156 | }, 157 | "labelHighlightBold": false, 158 | "font": { 159 | "color": "#FFFFFF" 160 | } 161 | }, 162 | "pipe": { 163 | "margin": 10, 164 | "color": { 165 | "background": "#FF9800", 166 | "border": "#F57C00", 167 | "highlight": { 168 | "background": "#FF9800", 169 | "border": "#F57C00" 170 | } 171 | }, 172 | "labelHighlightBold": false, 173 | "font": { 174 | "color": "#FFFFFF" 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/electron/config.ts: -------------------------------------------------------------------------------- 1 | import { Theme, DefaultTheme } from '../shared/themes/color-map'; 2 | import { Config } from '../shared/data-format'; 3 | import { readFileSync, readdirSync, writeFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | import { app } from 'electron'; 6 | 7 | // Handle the case when the theme is not there 8 | const builtInThemesMap = readdirSync(join(__dirname, '..', 'assets')) 9 | .filter(f => f.endsWith('.theme.json')) 10 | .map(f => JSON.parse(readFileSync(join(__dirname, '..', 'assets', f)).toString())) 11 | .reduce((a: Config['themes'], theme: Theme) => { 12 | a[theme.name] = theme; 13 | return a; 14 | }, {}); 15 | 16 | export const getConfig = (): Config => { 17 | const path = app.getPath('userData'); 18 | console.log('Looking for config file in', path); 19 | let config = null; 20 | let themes: Theme[] = []; 21 | try { 22 | config = JSON.parse(readFileSync(join(path, 'config.json')).toString()); 23 | console.log('Found config file'); 24 | } catch (_) { 25 | console.log('Config file not found'); 26 | return { showLibs: false, showModules: false, theme: DefaultTheme, themes: builtInThemesMap }; 27 | } 28 | try { 29 | themes = readdirSync(join(path, 'themes')) 30 | .filter(f => f.endsWith('.json')) 31 | .map(f => JSON.parse(readFileSync(join(path, 'themes', f)).toString())); 32 | console.log('Found themes'); 33 | } catch (_) { 34 | console.log('Themes not found', _); 35 | return { 36 | showLibs: config.showLibs, 37 | showModules: config.showModules, 38 | theme: config.theme, 39 | themes: builtInThemesMap 40 | }; 41 | } 42 | return { 43 | showLibs: config.showLibs, 44 | showModules: config.showModules, 45 | theme: config.theme, 46 | themes: Object.assign( 47 | themes.reduce((a: Config['themes'], t: Theme) => { 48 | a[t.name] = t; 49 | return a; 50 | }, {}), 51 | builtInThemesMap 52 | ) 53 | }; 54 | }; 55 | 56 | export const setConfigProps = (config: Partial): void => { 57 | const path = app.getPath('userData'); 58 | let storedConfig: Partial = {}; 59 | try { 60 | storedConfig = JSON.parse(readFileSync(join(path, 'config.json')).toString()); 61 | } catch (e) { 62 | console.error(e); 63 | } 64 | try { 65 | Object.assign(storedConfig, config); 66 | writeFileSync(join(path, 'config.json'), JSON.stringify(storedConfig, null, 2)); 67 | } catch (e) { 68 | console.error(e); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/electron/formatters/model-formatter.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy } from '@angular/core'; 2 | import { 3 | ComponentSymbol, 4 | DirectiveSymbol, 5 | InjectableSymbol, 6 | NgModuleSymbol, 7 | PipeSymbol, 8 | TemplateNode, 9 | } from 'ngast'; 10 | import { Metadata } from '../../shared/data-format'; 11 | 12 | const _changeDetectionToString = ( 13 | cd: ChangeDetectionStrategy | undefined 14 | ): string | null => { 15 | switch (cd) { 16 | case ChangeDetectionStrategy.Default: 17 | return 'Default'; 18 | case ChangeDetectionStrategy.OnPush: 19 | return 'OnPush'; 20 | } 21 | return null; 22 | }; 23 | 24 | export const getInjectableMetadata = (injectable: InjectableSymbol): Metadata => { 25 | const deps = injectable.getDependencies(); 26 | return { 27 | filePath: injectable.path, 28 | properties: [ 29 | { key: 'Name', value: injectable.name }, 30 | // { key: 'Multiprovider', value: (meta.multi === true).toString() }, 31 | { key: 'Dependencies', value: deps.map((dep) => dep.name).join(', ') }, 32 | ], 33 | }; 34 | }; 35 | 36 | export const getPipeMetadata = (pipe: PipeSymbol): Metadata => { 37 | return { 38 | filePath: pipe.path, 39 | properties: [ 40 | { key: 'Name', value: pipe.name }, 41 | // { key: 'Pure', value: ((meta || { pure: true }).pure === true).toString() } 42 | ], 43 | }; 44 | }; 45 | 46 | export const getDirectiveMetadata = ( 47 | dir: DirectiveSymbol | ComponentSymbol 48 | ): Metadata | null => { 49 | const meta = dir.metadata; 50 | if (!meta) { 51 | return null; 52 | } 53 | const getChangeDetection = () => { 54 | if (dir instanceof ComponentSymbol) { 55 | // TODO: ngast doesn't export ComponentMetadata 56 | return (meta as ComponentSymbol['metadata'])?.changeDetection; 57 | } 58 | return undefined; 59 | }; 60 | return { 61 | filePath: dir.path, 62 | properties: [ 63 | { key: 'Selector', value: meta.selector }, 64 | { key: 'Component', value: (dir.annotation === 'Component').toString() }, 65 | { 66 | key: 'Change Detection', 67 | value: _changeDetectionToString(getChangeDetection()), 68 | }, 69 | { key: 'Export', value: (meta.exportAs || []).join(', ') }, 70 | ], 71 | }; 72 | }; 73 | 74 | export const getElementMetadata = (el: TemplateNode): Metadata => { 75 | return { 76 | properties: [ 77 | { key: 'Name', value: el.name }, 78 | { key: 'Directives', value: el.directives.map(d => d.name).join(', ') }, 79 | { key: 'Attributes', value: el.attributes.map(a => `[${a}]`).join(', ') }, 80 | { key: 'References', value: el.references.map(r => `[${r}]`).join(', ') }, 81 | ], 82 | }; 83 | }; 84 | 85 | export const getModuleMetadata = (node: NgModuleSymbol): Metadata => { 86 | return { 87 | filePath: node.path, 88 | properties: [{ key: 'Name', value: node.name }], 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /src/electron/helpers/process.ts: -------------------------------------------------------------------------------- 1 | import { Metadata, VisualizationConfig, Config, IdentifiedStaticSymbol } from '../../shared/data-format'; 2 | import { fork, ChildProcess } from 'child_process'; 3 | import { EventEmitter } from 'events'; 4 | import { Message } from '../../shared/ipc-constants'; 5 | 6 | export interface LoadProjectRequest { 7 | showLibs: boolean; 8 | showModules: boolean; 9 | topic: Message.LoadProject; 10 | tsconfig: string; 11 | } 12 | 13 | export interface LoadProjectResponse { 14 | topic: Message.LoadProject; 15 | err: string | null; 16 | } 17 | 18 | export interface PrevStateRequest { 19 | topic: Message.PrevState; 20 | } 21 | 22 | export interface PrevStateResponse { 23 | topic: Message.PrevState; 24 | available: boolean; 25 | } 26 | 27 | export interface DirectStateTransitionRequest { 28 | topic: Message.DirectStateTransition; 29 | id: string; 30 | } 31 | 32 | export interface DirectStateTransitionResponse { 33 | topic: Message.DirectStateTransition; 34 | available: boolean; 35 | } 36 | 37 | export interface GetSymbolsRequest { 38 | topic: Message.GetSymbols; 39 | } 40 | 41 | export interface GetSymbolsResponse { 42 | topic: Message.GetSymbols; 43 | symbols: IdentifiedStaticSymbol[]; 44 | } 45 | 46 | export interface GetMetadataRequest { 47 | topic: Message.GetMetadata; 48 | id: string; 49 | } 50 | 51 | export interface GetMetadataResponse { 52 | topic: Message.GetMetadata; 53 | data: Metadata | null; 54 | } 55 | 56 | export interface GetDataRequest { 57 | topic: Message.GetData; 58 | } 59 | 60 | export interface GetDataResponse { 61 | topic: Message.GetData; 62 | data: VisualizationConfig | null; 63 | } 64 | 65 | export interface ConfigRequest { 66 | topic: Message.Config; 67 | } 68 | 69 | export interface ConfigResponse { 70 | topic: Message.Config; 71 | data: Config; 72 | } 73 | 74 | export interface ToggleLibsResponse { 75 | topic: Message.ToggleLibs; 76 | } 77 | 78 | export interface ToggleLibsRequest { 79 | topic: Message.ToggleLibs; 80 | } 81 | 82 | export interface ToggleModulesResponse { 83 | topic: Message.ToggleModules; 84 | } 85 | 86 | export interface ToggleModulesRequest { 87 | topic: Message.ToggleModules; 88 | } 89 | 90 | 91 | export type IPCRequest = 92 | | LoadProjectRequest 93 | | PrevStateRequest 94 | | DirectStateTransitionRequest 95 | | GetSymbolsRequest 96 | | GetMetadataRequest 97 | | GetDataRequest 98 | | ConfigRequest 99 | | ToggleLibsRequest 100 | | ToggleModulesRequest; 101 | 102 | export type IPCResponse = 103 | | LoadProjectResponse 104 | | PrevStateResponse 105 | | DirectStateTransitionResponse 106 | | GetSymbolsResponse 107 | | GetMetadataResponse 108 | | GetDataResponse 109 | | ConfigResponse 110 | | ToggleLibsResponse 111 | | ToggleModulesResponse; 112 | 113 | export interface Responder { 114 | (data: R): void; 115 | } 116 | 117 | export interface RequestHandler { 118 | (request: T, responder: Responder): void; 119 | } 120 | 121 | export class ParentProcess { 122 | private emitter = new EventEmitter(); 123 | 124 | constructor() { 125 | process.on('message' as any, (request: IPCRequest) => { 126 | console.log('Got message from the parent process with topic:', request.topic); 127 | this.emitter.emit(request.topic, request, ((response: IPCResponse) => { 128 | console.log('Sending response for message:', request.topic); 129 | (process as any).send(response); 130 | }) as Responder); 131 | }); 132 | } 133 | 134 | on(topic: Message, cb: RequestHandler): void { 135 | this.emitter.on(topic, (request: T, responder: Responder) => { 136 | try { 137 | cb(request, responder); 138 | } catch (e) { 139 | console.log('Error while responding to a message', e); 140 | responder({ 141 | topic: topic 142 | } as any); 143 | } 144 | }); 145 | } 146 | } 147 | 148 | export class SlaveProcess { 149 | private emitter = new EventEmitter(); 150 | 151 | constructor(private process: ChildProcess, private moduleUrl: string, private initArgs: string[]) {} 152 | 153 | static create(moduleUrl: string, ...args: string[]): SlaveProcess { 154 | const slaveProcess = fork(moduleUrl, args); 155 | const result = new SlaveProcess(slaveProcess, moduleUrl, args); 156 | slaveProcess.on('error', err => { 157 | console.error(err); 158 | }); 159 | return result; 160 | } 161 | 162 | get connected(): boolean { 163 | return this.process.connected; 164 | } 165 | 166 | onReady(cb: () => void): void { 167 | this.emitter.on('ready', cb); 168 | } 169 | 170 | send(request: IPCRequest): Promise { 171 | return new Promise((resolve) => { 172 | this.process.once('message', (data: R) => { 173 | console.log('Got message with topic', data.topic); 174 | resolve(data); 175 | }); 176 | this.process.send(request); 177 | }); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/electron/menu/application_menu_template.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, dialog, MenuItem, MenuItemConstructorOptions, MessageBoxReturnValue } from "electron"; 2 | import { Message } from "../../shared/ipc-constants"; 3 | import { getConfig } from "../config"; 4 | 5 | export enum MenuIndex { 6 | Ngrev 7 | } 8 | 9 | export enum SubmenuIndex { 10 | Themes = 0, 11 | ShowLibs = 1, 12 | ShowModulesOnly = 2, 13 | Export = 4, 14 | FitView = 6, 15 | Reset = 7, 16 | Quit = 8 17 | } 18 | 19 | export const applicationMenuTemplate = ( 20 | themeChange: (name: string) => void, 21 | libsToggle: () => void, 22 | modulesOnlyToggle: () => void 23 | ): MenuItemConstructorOptions | MenuItem => { 24 | return { 25 | label: "ngrev", 26 | submenu: [ 27 | { 28 | label: "Themes", 29 | submenu: Object.keys(getConfig().themes || []).map((label: string) => { 30 | return { 31 | label, 32 | type: "radio", 33 | checked: label === getConfig().theme, 34 | click() { 35 | const window = BrowserWindow.getAllWindows()[0]; 36 | themeChange(label); 37 | window.webContents.send(Message.ChangeTheme, label); 38 | }, 39 | }; 40 | }), 41 | }, 42 | { 43 | label: "Show libs", 44 | type: "checkbox", 45 | checked: getConfig().showLibs, 46 | accelerator: "CmdOrCtrl+L", 47 | click() { 48 | const window = BrowserWindow.getAllWindows()[0]; 49 | libsToggle(); 50 | window.webContents.send(Message.ToggleLibsMenuAction); 51 | }, 52 | }, 53 | { 54 | label: "Show modules only", 55 | type: "checkbox", 56 | checked: getConfig().showModules, 57 | accelerator: "CmdOrCtrl+M", 58 | click() { 59 | const window = BrowserWindow.getAllWindows()[0]; 60 | modulesOnlyToggle(); 61 | window.webContents.send(Message.ToggleModulesMenuAction); 62 | }, 63 | }, 64 | { 65 | type: "separator" 66 | }, 67 | { 68 | label: "Export", 69 | accelerator: "CmdOrCtrl+E", 70 | enabled: false, 71 | click() { 72 | const window = BrowserWindow.getAllWindows()[0]; 73 | window.webContents.send(Message.SaveImage); 74 | }, 75 | }, 76 | { 77 | type: "separator" 78 | }, 79 | { 80 | label: "Fit view", 81 | accelerator: "CmdOrCtrl+F", 82 | click() { 83 | const window = BrowserWindow.getAllWindows()[0]; 84 | window.webContents.send(Message.FitView); 85 | }, 86 | }, 87 | { 88 | label: "Reset", 89 | accelerator: "CmdOrCtrl+R", 90 | click() { 91 | const focusedWindow: BrowserWindow | null = BrowserWindow.getFocusedWindow(); 92 | if (!focusedWindow) { 93 | return; 94 | } 95 | dialog 96 | .showMessageBox(focusedWindow, { 97 | type: "warning", 98 | buttons: ["OK", "Cancel"], 99 | title: "Are you sure?", 100 | message: 101 | "Your progress will be lost. Are you sure you want to refresh and select a new project?", 102 | }) 103 | .then((message: MessageBoxReturnValue) => { 104 | if (!message.response) { 105 | BrowserWindow.getAllWindows().forEach((w: BrowserWindow) => 106 | w.webContents.reloadIgnoringCache() 107 | ); 108 | } 109 | }); 110 | }, 111 | }, 112 | { 113 | label: "Quit", 114 | accelerator: "CmdOrCtrl+Q", 115 | click() { 116 | app.quit(); 117 | }, 118 | }, 119 | ], 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /src/electron/menu/dev_menu_template.ts: -------------------------------------------------------------------------------- 1 | import { app, MenuItem, MenuItemConstructorOptions } from 'electron'; 2 | 3 | export const devMenuTemplate = (): MenuItem | MenuItemConstructorOptions => { 4 | return { 5 | label: 'Development', 6 | submenu: [ 7 | { 8 | label: 'Quit', 9 | accelerator: 'CmdOrCtrl+Q', 10 | click() { 11 | app.quit(); 12 | } 13 | } 14 | ] 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/electron/model/background-app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DirectStateTransitionResponse, GetDataResponse, GetMetadataResponse, GetSymbolsResponse, 3 | LoadProjectResponse, 4 | PrevStateResponse, 5 | SlaveProcess, ToggleLibsResponse, ToggleModulesResponse 6 | } from '../helpers/process'; 7 | import { ipcMain, IpcMainEvent, WebContents } from 'electron'; 8 | import { Message, Status } from '../../shared/ipc-constants'; 9 | import { Config } from '../../shared/data-format'; 10 | import { menus } from '../../../main'; 11 | import { join } from 'path'; 12 | import { MenuIndex, SubmenuIndex } from '../menu/application_menu_template'; 13 | 14 | const success = (sender: WebContents, msg: Message, payload: any) => { 15 | sender.send(msg, Status.Success, payload); 16 | }; 17 | 18 | const error = (sender: WebContents, msg: Message, payload: any) => { 19 | sender.send(msg, Status.Failure, payload); 20 | }; 21 | 22 | interface Task { 23 | (): Promise; 24 | } 25 | 26 | class TaskQueue { 27 | private queue: Task[] = []; 28 | 29 | push(task: Task) { 30 | this.queue.unshift(task); 31 | if (this.queue.length === 1) { 32 | this.next(); 33 | } 34 | } 35 | 36 | private next() { 37 | const task = this.queue.pop(); 38 | if (task) { 39 | task().then(() => this.next(), () => this.next()); 40 | } 41 | } 42 | } 43 | 44 | export class BackgroundApp { 45 | private slaveProcess!: SlaveProcess; 46 | private taskQueue!: TaskQueue; 47 | 48 | init(config: Partial): void { 49 | this.slaveProcess = SlaveProcess.create(join(__dirname, '..', 'parser.js')); 50 | this.taskQueue = new TaskQueue(); 51 | 52 | ipcMain.on(Message.Config, (event: IpcMainEvent) => { 53 | success(event.sender, Message.Config, config); 54 | }); 55 | 56 | ipcMain.on(Message.LoadProject, (event: IpcMainEvent, { tsconfig, showLibs, showModules }: { tsconfig: string; showLibs: boolean; showModules: boolean }) => { 57 | if (!this.slaveProcess.connected) { 58 | console.log('The slave process is not ready yet'); 59 | return; 60 | } else { 61 | console.log('The slave process is connected'); 62 | } 63 | console.log('Loading project. Forwarding message to the background process.'); 64 | this.taskQueue.push(() => { 65 | return this.slaveProcess 66 | .send({ 67 | topic: Message.LoadProject, 68 | tsconfig, 69 | showLibs, 70 | showModules 71 | }) 72 | .then((data: LoadProjectResponse) => { 73 | if (data.err) { 74 | console.log('Got error message while loading the project: ', data.err); 75 | error(event.sender, Message.LoadProject, data.err); 76 | } else { 77 | console.log('The project was successfully loaded'); 78 | success(event.sender, Message.LoadProject, null); 79 | } 80 | }); 81 | }); 82 | }); 83 | 84 | ipcMain.on(Message.PrevState, (event: IpcMainEvent) => { 85 | console.log('Requesting previous state'); 86 | this.taskQueue.push(() => { 87 | return this.slaveProcess 88 | .send({ 89 | topic: Message.PrevState 90 | }) 91 | .then((data: PrevStateResponse) => { 92 | console.log('Got previous state'); 93 | if (data.available) { 94 | success(event.sender, Message.PrevState, data.available); 95 | } else { 96 | error(event.sender, Message.PrevState, data.available); 97 | } 98 | }); 99 | }); 100 | }); 101 | 102 | ipcMain.on(Message.DirectStateTransition, (event: IpcMainEvent, id: string) => { 103 | console.log('Requesting direct state transition'); 104 | this.taskQueue.push(() => { 105 | return this.slaveProcess 106 | .send({ 107 | topic: Message.DirectStateTransition, 108 | id 109 | }) 110 | .then((data: DirectStateTransitionResponse) => { 111 | console.log('Got response for direct state transition', id, data.available); 112 | if (data.available) { 113 | success(event.sender, Message.DirectStateTransition, data.available); 114 | } else { 115 | error(event.sender, Message.DirectStateTransition, data.available); 116 | } 117 | }); 118 | }); 119 | }); 120 | 121 | ipcMain.on(Message.GetSymbols, (event: IpcMainEvent) => { 122 | console.log('Requesting symbols...'); 123 | this.taskQueue.push(() => { 124 | return this.slaveProcess 125 | .send({ 126 | topic: Message.GetSymbols 127 | }) 128 | .then((data: GetSymbolsResponse) => { 129 | console.log('Got symbols response'); 130 | if (data.symbols) { 131 | success(event.sender, Message.GetSymbols, data.symbols); 132 | } 133 | }); 134 | }); 135 | }); 136 | 137 | ipcMain.on(Message.GetMetadata, (event: IpcMainEvent, id: string) => { 138 | console.log('Requesting metadata...'); 139 | this.taskQueue.push(() => { 140 | return this.slaveProcess 141 | .send({ 142 | topic: Message.GetMetadata, 143 | id 144 | }) 145 | .then((response: GetMetadataResponse) => { 146 | console.log('Got metadata from the child process'); 147 | if (response.data) { 148 | success(event.sender, Message.GetMetadata, response.data); 149 | } else { 150 | error(event.sender, Message.GetMetadata, null); 151 | } 152 | }); 153 | }); 154 | }); 155 | 156 | ipcMain.on(Message.GetData, (event: IpcMainEvent) => { 157 | console.log('Requesting data'); 158 | this.taskQueue.push(() => { 159 | return this.slaveProcess 160 | .send({ 161 | topic: Message.GetData 162 | }) 163 | .then((response: GetDataResponse) => { 164 | console.log('Got data response', response.data); 165 | if (response.data) { 166 | success(event.sender, Message.GetData, response.data); 167 | } else { 168 | error(event.sender, Message.GetData, null); 169 | } 170 | }); 171 | }); 172 | }); 173 | 174 | ipcMain.on(Message.ToggleLibs, (event: IpcMainEvent) => { 175 | console.log('Toggle libs!'); 176 | this.taskQueue.push(() => { 177 | return this.slaveProcess 178 | .send({ 179 | topic: Message.ToggleLibs 180 | }) 181 | .then(() => { 182 | console.log('The slave process toggled the libs'); 183 | success(event.sender, Message.ToggleLibs, true); 184 | }); 185 | }); 186 | }); 187 | 188 | ipcMain.on(Message.ToggleModules, (event: IpcMainEvent) => { 189 | console.log('Toggle modules!'); 190 | this.taskQueue.push(() => { 191 | return this.slaveProcess 192 | .send({ 193 | topic: Message.ToggleModules 194 | }) 195 | .then(() => { 196 | console.log('The slave process toggled the modules'); 197 | success(event.sender, Message.ToggleModules, true); 198 | }); 199 | }); 200 | }); 201 | 202 | ipcMain.on(Message.DisableExport, (event: IpcMainEvent) => { 203 | const exportMenuItem = menus.items[MenuIndex.Ngrev].submenu; 204 | if (exportMenuItem) { 205 | exportMenuItem.items[SubmenuIndex.Export].enabled = false; 206 | success(event.sender, Message.DisableExport, true); 207 | } 208 | }); 209 | 210 | ipcMain.on(Message.EnableExport, (event: IpcMainEvent) => { 211 | const exportMenuItem = menus.items[MenuIndex.Ngrev].submenu; 212 | if (exportMenuItem) { 213 | menus.items[MenuIndex.Ngrev].submenu!.items[SubmenuIndex.Export].enabled = true; 214 | } 215 | success(event.sender, Message.EnableExport, true); 216 | }); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/electron/model/project.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceSymbols } from 'ngast'; 2 | 3 | export class Project { 4 | projectSymbols?: WorkspaceSymbols; 5 | 6 | load(tsconfig: string): Promise { 7 | this.projectSymbols = new WorkspaceSymbols( 8 | tsconfig, 9 | ); 10 | return Promise.resolve(this.projectSymbols); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/electron/model/symbol-index.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../states/state'; 2 | import { Symbol, WorkspaceSymbols, AnnotationNames } from 'ngast'; 3 | import { getId } from '../../shared/data-format'; 4 | import { PipeState } from '../states/pipe.state'; 5 | import { DirectiveState } from '../states/directive.state'; 6 | import { ModuleTreeState } from '../states/module-tree.state'; 7 | import { ProviderState } from '../states/provider.state'; 8 | 9 | export interface StateFactory { 10 | (): State; 11 | } 12 | 13 | export interface SymbolData { 14 | stateFactory: StateFactory; 15 | symbol: Symbol; 16 | } 17 | 18 | export type Index = Map>; 19 | 20 | export interface ISymbolIndex { 21 | getIndex(context: WorkspaceSymbols): Index; 22 | clear(): void; 23 | } 24 | 25 | class SymbolIndexImpl { 26 | private symbolsIndex: Index = new Map>(); 27 | 28 | getIndex(context: WorkspaceSymbols) { 29 | if (this.symbolsIndex && this.symbolsIndex.size) { 30 | return this.symbolsIndex; 31 | } 32 | this.symbolsIndex.clear(); 33 | context.getAllInjectable().forEach(symbol => { 34 | this.symbolsIndex.set(getId(symbol), { 35 | symbol, 36 | stateFactory() { 37 | return new ProviderState(context, symbol); 38 | } 39 | }); 40 | }); 41 | context.getAllPipes().forEach(symbol => 42 | this.symbolsIndex.set(getId(symbol), { 43 | symbol, 44 | stateFactory() { 45 | return new PipeState(context, symbol); 46 | } 47 | }) 48 | ); 49 | context.getAllModules().forEach(symbol => 50 | this.symbolsIndex.set(getId(symbol), { 51 | symbol, 52 | stateFactory() { 53 | return new ModuleTreeState(context, symbol); 54 | } 55 | }) 56 | ); 57 | context.getAllDirectives().forEach(symbol => 58 | this.symbolsIndex.set(getId(symbol), { 59 | symbol, 60 | stateFactory() { 61 | return new DirectiveState(context, symbol); 62 | } 63 | }) 64 | ); 65 | context.getAllComponents().forEach(symbol => 66 | this.symbolsIndex.set(getId(symbol), { 67 | symbol, 68 | stateFactory() { 69 | return new DirectiveState(context, symbol); 70 | } 71 | }) 72 | ); 73 | return this.symbolsIndex; 74 | } 75 | 76 | clear() { 77 | this.symbolsIndex.clear(); 78 | } 79 | } 80 | 81 | export const SymbolIndex: ISymbolIndex = new SymbolIndexImpl(); 82 | -------------------------------------------------------------------------------- /src/electron/parser.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from './states/app.state'; 2 | import { SymbolIndex, SymbolData } from './model/symbol-index'; 3 | import { Message } from '../shared/ipc-constants'; 4 | import { State } from './states/state'; 5 | import { Project } from './model/project'; 6 | import { AnnotationNames, Symbol } from 'ngast'; 7 | import { 8 | ParentProcess, 9 | LoadProjectRequest, 10 | Responder, 11 | PrevStateRequest, 12 | DirectStateTransitionRequest, 13 | GetSymbolsRequest, 14 | GetMetadataRequest, 15 | GetDataRequest, 16 | LoadProjectResponse, 17 | PrevStateResponse, 18 | DirectStateTransitionResponse, 19 | GetSymbolsResponse, 20 | GetMetadataResponse, 21 | GetDataResponse, 22 | ToggleLibsRequest, 23 | ToggleLibsResponse, 24 | ToggleModulesRequest, 25 | ToggleModulesResponse 26 | } from './helpers/process'; 27 | import { IdentifiedStaticSymbol } from '../shared/data-format'; 28 | 29 | export class BackgroundApp { 30 | private project?: Project; 31 | private states: State[] = []; 32 | private parentProcess = new ParentProcess(); 33 | 34 | init(): void { 35 | this.parentProcess.on( 36 | Message.LoadProject, 37 | (data: LoadProjectRequest, responder: Responder) => { 38 | this.states.forEach(s => s.destroy()); 39 | SymbolIndex.clear(); 40 | 41 | this.states = []; 42 | console.log(`Loading project: "${data.tsconfig}"`); 43 | this.project = new Project(); 44 | let parseError: any = null; 45 | try { 46 | this.project.load(data.tsconfig); 47 | const allModules = this.project.projectSymbols!.getAllModules(); 48 | const bootstrapModule = allModules.filter(m => { 49 | const bootstrap = m.getBootstap(); 50 | return bootstrap && bootstrap.length > 0; 51 | }).pop(); 52 | if (!parseError) { 53 | const module = bootstrapModule ?? allModules[0]; 54 | if (module) { 55 | console.log('Project loaded'); 56 | this.states.push(new AppState(this.project.projectSymbols!, data.showLibs, data.showModules)); 57 | console.log('Initial state created'); 58 | responder({ 59 | topic: Message.LoadProject, 60 | err: null 61 | }); 62 | } else { 63 | responder({ 64 | topic: Message.LoadProject, 65 | err: 'Cannot find the root module of your project.' 66 | }); 67 | return; 68 | } 69 | } else { 70 | console.log(parseError); 71 | responder({ 72 | topic: Message.LoadProject, 73 | err: (parseError as Error).message 74 | }); 75 | return; 76 | } 77 | } catch (exception) { 78 | console.log(exception); 79 | let message = exception.message; 80 | if (parseError) { 81 | if ((parseError as any) instanceof Error) { 82 | parseError = (parseError as Error).message; 83 | } 84 | message = parseError; 85 | } 86 | responder({ 87 | topic: Message.LoadProject, 88 | err: message 89 | }); 90 | } 91 | } 92 | ); 93 | 94 | this.parentProcess.on( 95 | Message.PrevState, 96 | (data: PrevStateRequest, responder: Responder) => { 97 | console.log('Going to previous state'); 98 | if (this.states.length > 1) { 99 | this.states.pop(); 100 | console.log('Successfully moved to previous state'); 101 | responder({ 102 | topic: Message.PrevState, 103 | available: true 104 | }); 105 | } else { 106 | console.log('Unsuccessfully moved to previous state'); 107 | responder({ 108 | topic: Message.PrevState, 109 | available: false 110 | }); 111 | } 112 | } 113 | ); 114 | 115 | this.parentProcess.on( 116 | Message.DirectStateTransition, 117 | (data: DirectStateTransitionRequest, responder: Responder) => { 118 | console.log('Direct state transition', data.id); 119 | if (!this.project?.projectSymbols) { 120 | console.log('Project is not loaded yet'); 121 | responder({ 122 | topic: Message.DirectStateTransition, 123 | available: false 124 | }); 125 | return; 126 | } 127 | const index = SymbolIndex.getIndex(this.project.projectSymbols); 128 | const lastState = this.states[this.states.length - 1]; 129 | const nextSymbol = index.get(data.id); 130 | let nextState: State | null; 131 | if (nextSymbol) { 132 | nextState = nextSymbol.stateFactory(); 133 | if (lastState instanceof nextState.constructor && lastState.stateSymbolId === nextState.stateSymbolId) { 134 | nextState = lastState.nextState(data.id); 135 | } 136 | } else { 137 | // Used for templates 138 | nextState = lastState.nextState(data.id); 139 | } 140 | if (nextState) { 141 | this.states.push(nextState); 142 | console.log('Found next state'); 143 | responder({ 144 | topic: Message.DirectStateTransition, 145 | available: true 146 | }); 147 | return; 148 | } 149 | console.log('No next state'); 150 | responder({ 151 | topic: Message.DirectStateTransition, 152 | available: false 153 | }); 154 | } 155 | ); 156 | 157 | this.parentProcess.on( 158 | Message.GetSymbols, 159 | (data: GetSymbolsRequest, responder: Responder) => { 160 | console.log('Get symbols'); 161 | if (!this.project?.projectSymbols) { 162 | console.log('Project is not loaded yet'); 163 | return; 164 | } 165 | const res: IdentifiedStaticSymbol[] = []; 166 | try { 167 | const map = SymbolIndex.getIndex(this.project.projectSymbols); 168 | map.forEach((data: SymbolData, id: string) => { 169 | if (data.symbol instanceof Symbol) { 170 | res.push({ id, name: data.symbol.name, annotation: data.symbol.annotation, path: data.symbol.path }); 171 | } 172 | }); 173 | } catch (e) { 174 | console.error(e); 175 | } 176 | responder({ 177 | topic: Message.GetSymbols, 178 | symbols: res 179 | }); 180 | } 181 | ); 182 | 183 | this.parentProcess.on( 184 | Message.GetMetadata, 185 | (data: GetMetadataRequest, responder: Responder) => { 186 | console.log('Getting metadata', data.id); 187 | if (this.state) { 188 | responder({ 189 | topic: Message.GetMetadata, 190 | data: this.state.getMetadata(data.id) 191 | }); 192 | } else { 193 | responder({ 194 | topic: Message.GetMetadata, 195 | data: null 196 | }); 197 | } 198 | } 199 | ); 200 | 201 | this.parentProcess.on( 202 | Message.GetData, 203 | (_: GetDataRequest, responder: Responder) => { 204 | console.log('Getting data'); 205 | if (this.state) { 206 | const data = this.state.getData(); 207 | console.log('Getting data from state:', this.state.constructor.name, 'Got', data.graph.nodes.length, 'items'); 208 | responder({ 209 | topic: Message.GetData, 210 | data 211 | }); 212 | } else { 213 | console.log('No state to get data from'); 214 | responder({ 215 | topic: Message.GetData, 216 | data: null 217 | }); 218 | } 219 | } 220 | ); 221 | 222 | this.parentProcess.on( 223 | Message.ToggleLibs, 224 | (_: ToggleLibsRequest, responder: Responder) => { 225 | console.log('Toggle libraries'); 226 | if (!this.project?.projectSymbols) { 227 | console.log('Project is not loaded yet'); 228 | return; 229 | } 230 | const state = this.states.shift() as AppState; 231 | const newState = new AppState(this.project.projectSymbols, !state.showLibs, state.showModules); 232 | this.states.unshift(newState); 233 | responder({ 234 | topic: Message.ToggleLibs 235 | }); 236 | } 237 | ); 238 | 239 | this.parentProcess.on( 240 | Message.ToggleModules, 241 | (_: ToggleModulesRequest, responder: Responder) => { 242 | console.log('Toggle modules'); 243 | if (!this.project?.projectSymbols) { 244 | console.log('Project is not loaded yet'); 245 | return; 246 | } 247 | const state = this.states.shift() as AppState; 248 | const newState = new AppState(this.project.projectSymbols, state.showLibs, !state.showModules); 249 | this.states.unshift(newState); 250 | responder({ 251 | topic: Message.ToggleModules 252 | }); 253 | } 254 | ); 255 | } 256 | 257 | get state(): State | undefined { 258 | return this.states[this.states.length - 1]; 259 | } 260 | } 261 | 262 | new BackgroundApp().init(); 263 | -------------------------------------------------------------------------------- /src/electron/states/app-module.state.ts: -------------------------------------------------------------------------------- 1 | import { State } from './state'; 2 | import { DirectiveState } from './directive.state'; 3 | import { 4 | Node, 5 | Edge, 6 | Metadata, 7 | getId, 8 | SymbolTypes, 9 | isAngularSymbol, 10 | VisualizationConfig, 11 | isThirdParty 12 | } from '../../shared/data-format'; 13 | import { ComponentSymbol, DirectiveSymbol, InjectableSymbol, NgModuleSymbol, PipeSymbol, WorkspaceSymbols } from 'ngast'; 14 | import { getDirectiveMetadata, getInjectableMetadata, getPipeMetadata } from '../formatters/model-formatter'; 15 | import { ProviderState } from './provider.state'; 16 | import { PipeState } from './pipe.state'; 17 | 18 | interface DataType { 19 | symbol: InjectableSymbol | PipeSymbol | DirectiveSymbol | ComponentSymbol | NgModuleSymbol; 20 | metadata: any; 21 | } 22 | 23 | interface NodeMap { 24 | [id: string]: Node; 25 | } 26 | 27 | export class AppModuleState extends State { 28 | private symbols: NodeMap = {}; 29 | 30 | constructor(context: WorkspaceSymbols, protected module: NgModuleSymbol) { 31 | super(getId(module), context); 32 | } 33 | 34 | getMetadata(id: string): Metadata | null { 35 | const symbol = this.symbols[id]; 36 | if (!symbol) { 37 | return null; 38 | } 39 | const data = symbol.data; 40 | if (!data) { 41 | return null; 42 | } 43 | if (data.symbol instanceof DirectiveSymbol || data.symbol instanceof ComponentSymbol) { 44 | return getDirectiveMetadata(data.symbol); 45 | } else if (data.symbol instanceof InjectableSymbol) { 46 | return getInjectableMetadata(data.symbol); 47 | } else if (data.symbol instanceof PipeSymbol) { 48 | return getPipeMetadata(data.symbol); 49 | } 50 | return null; 51 | } 52 | 53 | nextState(nodeId: string): State | null { 54 | if (nodeId === this.symbolId) { 55 | return null; 56 | } 57 | const data = this.symbols[nodeId].data; 58 | if (!data || !data.symbol) { 59 | return null; 60 | } 61 | // ngtsc does not allow us to resolve many of the properties 62 | // we need for third-party symbols so we don't allow the navigation. 63 | if (isThirdParty(data.symbol)) { 64 | return null; 65 | } 66 | if (data.symbol instanceof DirectiveSymbol || data.symbol instanceof ComponentSymbol) { 67 | return new DirectiveState(this.context, data.symbol); 68 | } else if (data.symbol instanceof InjectableSymbol) { 69 | return new ProviderState(this.context, data.symbol); 70 | } else if (data.symbol instanceof PipeSymbol) { 71 | return new PipeState(this.context, data.symbol); 72 | } 73 | return null; 74 | } 75 | 76 | getData(): VisualizationConfig { 77 | const currentModuleId = getId(this.module); 78 | const nodes: NodeMap = { 79 | [currentModuleId]: { 80 | id: currentModuleId, 81 | label: this.module.name, 82 | data: { 83 | symbol: this.module, 84 | metadata: null 85 | }, 86 | type: { 87 | angular: isAngularSymbol(this.module), 88 | type: SymbolTypes.Module 89 | } 90 | } 91 | }; 92 | const edges: Edge[] = []; 93 | 94 | (this.module.getDeclarations() || []).forEach(node => { 95 | if (node instanceof PipeSymbol) { 96 | this._appendSet(currentModuleId, node, nodes, SymbolTypes.Pipe, edges); 97 | } else if (node instanceof DirectiveSymbol) { 98 | this._appendSet(currentModuleId, node, nodes, SymbolTypes.Directive, edges); 99 | } else { 100 | this._appendSet(currentModuleId, node, nodes, SymbolTypes.Component, edges); 101 | } 102 | }); 103 | 104 | (this.module.getExports() || []).forEach(node => { 105 | if (node instanceof PipeSymbol) { 106 | this._appendSet(currentModuleId, node, nodes, SymbolTypes.Pipe, edges); 107 | } else if (node instanceof DirectiveSymbol) { 108 | this._appendSet(currentModuleId, node, nodes, SymbolTypes.Directive, edges); 109 | } else if (node instanceof ComponentSymbol) { 110 | this._appendSet(currentModuleId, node, nodes, SymbolTypes.Component, edges); 111 | } 112 | }); 113 | 114 | (this.module.getProviders() || []).forEach(provider => { 115 | if (!(provider instanceof InjectableSymbol)) { 116 | return; 117 | } 118 | this._appendSet(currentModuleId, provider, nodes, SymbolTypes.Provider, edges); 119 | }); 120 | 121 | this.symbols = nodes; 122 | return { 123 | title: this.module.name, 124 | graph: { 125 | nodes: Object.keys(nodes).map((id: string) => { 126 | const node = nodes[id]; 127 | return { 128 | id, 129 | type: node.type, 130 | label: node.label 131 | }; 132 | }), 133 | edges: edges 134 | } 135 | }; 136 | } 137 | 138 | private _appendSet( 139 | parentSet: string, 140 | node: InjectableSymbol | PipeSymbol | DirectiveSymbol | ComponentSymbol, 141 | nodes: NodeMap, 142 | symbolType: SymbolTypes, 143 | edges: Edge[] 144 | ): void { 145 | const id = getId(node); 146 | const name = node.name; 147 | nodes[id] = { 148 | id, 149 | label: name, 150 | data: { 151 | symbol: node, 152 | metadata: null 153 | }, 154 | type: { 155 | angular: isAngularSymbol(node), 156 | type: symbolType 157 | } 158 | }; 159 | edges.push({ 160 | from: parentSet, 161 | to: id 162 | }); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/electron/states/app.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentSymbol, 3 | DirectiveSymbol, 4 | InjectableSymbol, 5 | NgModuleSymbol, 6 | PipeSymbol, 7 | WorkspaceSymbols 8 | } from 'ngast'; 9 | import { State } from './state'; 10 | import { Metadata, VisualizationConfig, Layout, Node, Edge } from '../../shared/data-format'; 11 | import { ModuleTreeState } from './module-tree.state'; 12 | import { AppModuleState } from './app-module.state'; 13 | import { DirectiveState } from './directive.state'; 14 | import { ProviderState } from './provider.state'; 15 | import { PipeState } from './pipe.state'; 16 | 17 | const CompositeStateID = '$$$composite-state$$$'; 18 | const Title = 'Application View'; 19 | 20 | export class AppState extends State { 21 | private states: State[] = []; 22 | 23 | constructor(context: WorkspaceSymbols, private _showLibs: boolean, private _showModules: boolean) { 24 | super(CompositeStateID, context); 25 | this.init(); 26 | } 27 | 28 | get showLibs(): boolean { 29 | return this._showLibs; 30 | } 31 | 32 | get showModules(): boolean { 33 | return this._showModules; 34 | } 35 | 36 | getMetadata(id: string): Metadata | null { 37 | return this.states.reduce((c: Metadata | null, s: State) => { 38 | if (c !== null) return c; 39 | return s.getMetadata(id); 40 | }, null); 41 | } 42 | 43 | nextState(): null { 44 | return null; 45 | } 46 | 47 | getData(): VisualizationConfig { 48 | const data: VisualizationConfig = { 49 | layout: Layout.HierarchicalUDDirected, 50 | title: Title, 51 | graph: { 52 | nodes: [], 53 | edges: [] 54 | } 55 | }; 56 | const existingNodes = new Set(); 57 | const existingEdges = new Set(); 58 | this.states.forEach((state: State) => { 59 | const { graph } = state.getData(); 60 | graph.nodes.forEach((node: Node) => { 61 | if (!existingNodes.has(node.id) && this.showSymbol(node.id)) { 62 | data.graph.nodes.push(node); 63 | existingNodes.add(node.id); 64 | } 65 | }); 66 | graph.edges.forEach((edge: Edge) => { 67 | const edgeKey = `${edge.from}->${edge.to}`; 68 | if (!existingEdges.has(edgeKey) && this.showSymbol(edgeKey)) { 69 | data.graph.edges.push(edge); 70 | existingEdges.add(edgeKey); 71 | } 72 | }); 73 | }); 74 | return data; 75 | } 76 | 77 | private showSymbol(id: string): boolean { 78 | if (this.showLibs) { 79 | return true; 80 | } 81 | return id.indexOf('node_modules') < 0; 82 | } 83 | 84 | private init(): void { 85 | this.context.getAllModules().forEach((module: NgModuleSymbol) => { 86 | this.states.push(new ModuleTreeState(this.context, module)); 87 | }); 88 | if (!this.showModules) { 89 | this.context.getAllModules().forEach((module: NgModuleSymbol) => { 90 | this.states.push(new AppModuleState(this.context, module)); 91 | }); 92 | this.context.getAllDirectives().forEach((directive: DirectiveSymbol) => { 93 | this.states.push(new DirectiveState(this.context, directive, false)); 94 | }); 95 | this.context.getAllComponents().forEach((component: ComponentSymbol) => { 96 | this.states.push(new DirectiveState(this.context, component, false)); 97 | }); 98 | this.context.getAllInjectable().forEach((injectable: InjectableSymbol) => { 99 | this.states.push(new ProviderState(this.context, injectable)); 100 | }); 101 | this.context.getAllPipes().forEach((pipe: PipeSymbol) => { 102 | this.states.push(new PipeState(this.context, pipe)); 103 | }); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/electron/states/directive.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentSymbol, 3 | DirectiveSymbol, 4 | InjectableSymbol, 5 | TemplateNode, 6 | WorkspaceSymbols 7 | } from 'ngast'; 8 | import { State } from './state'; 9 | import { 10 | Direction, 11 | Edge, 12 | getId, 13 | isAngularSymbol, 14 | isThirdParty, 15 | Metadata, 16 | Node, 17 | SymbolTypes, 18 | VisualizationConfig, 19 | } from '../../shared/data-format'; 20 | import { getDirectiveMetadata, getInjectableMetadata } from '../formatters/model-formatter'; 21 | import { TemplateState } from './template.state'; 22 | 23 | interface NodeMap { 24 | [id: string]: DirectiveSymbol | ComponentSymbol | TemplateNode; 25 | } 26 | 27 | const TemplateId = 'template'; 28 | const DependenciesId = 'dependencies'; 29 | const ProvidersId = 'providers'; 30 | 31 | export class DirectiveState extends State { 32 | private symbols: NodeMap = {}; 33 | 34 | constructor( 35 | context: WorkspaceSymbols, 36 | protected directive: DirectiveSymbol | ComponentSymbol, 37 | private showControl = true 38 | ) { 39 | super(getId(directive), context); 40 | } 41 | 42 | getMetadata(id: string): Metadata | null { 43 | if (id === this.stateSymbolId) { 44 | return getDirectiveMetadata(this.directive); 45 | } 46 | const s = this.symbols[id]; 47 | if (s) { 48 | if (s instanceof DirectiveSymbol || s instanceof ComponentSymbol) { 49 | return getDirectiveMetadata(s); 50 | } else if (s instanceof InjectableSymbol) { 51 | return getInjectableMetadata(s); 52 | } else { 53 | return null; 54 | } 55 | } 56 | return null; 57 | } 58 | 59 | nextState(id: string): State | null { 60 | console.log('State', id, this.directive instanceof ComponentSymbol); 61 | if (id === TemplateId && this.directive instanceof ComponentSymbol) { 62 | return new TemplateState(this.context, this.directive); 63 | } 64 | if (id === this.symbolId) { 65 | return null; 66 | } 67 | const symbol = this.symbols[id]; 68 | // ngtsc does not allow us to resolve many of the properties 69 | // we need for third-party symbols so we don't allow the navigation. 70 | if ((symbol instanceof ComponentSymbol || symbol instanceof DirectiveSymbol) && 71 | isThirdParty(symbol)) { 72 | return null; 73 | } 74 | if (symbol instanceof DirectiveSymbol) { 75 | return new DirectiveState(this.context, symbol); 76 | } else { 77 | return null; 78 | } 79 | } 80 | 81 | getData(): VisualizationConfig { 82 | const nodeId = getId(this.directive); 83 | const nodes: Node[] = [ 84 | { 85 | id: nodeId, 86 | label: this.directive.name, 87 | data: this.directive, 88 | type: { 89 | type: (this.directive instanceof DirectiveSymbol) ? SymbolTypes.Directive : SymbolTypes.Component, 90 | angular: isAngularSymbol(this.directive), 91 | }, 92 | }, 93 | ]; 94 | const edges: Edge[] = []; 95 | if (this.showControl) { 96 | if (this.directive instanceof ComponentSymbol) { 97 | nodes.push({ 98 | id: TemplateId, 99 | label: 'Template', 100 | type: { 101 | type: SymbolTypes.Meta, 102 | angular: false, 103 | }, 104 | }); 105 | edges.push({ 106 | from: nodeId, 107 | to: TemplateId, 108 | }); 109 | } 110 | const addedSymbols: { [key: string]: boolean } = {}; 111 | this.addProviderNodes( 112 | nodes, 113 | edges, 114 | addedSymbols, 115 | 'Dependencies', 116 | DependenciesId, 117 | this.directive.getDependencies() as InjectableSymbol[] 118 | ); 119 | console.log('Dependencies of the directive', this.directive.getDependencies()); 120 | this.addProviderNodes( 121 | nodes, 122 | edges, 123 | addedSymbols, 124 | 'Providers', 125 | ProvidersId, 126 | this.directive.getProviders() as InjectableSymbol[] 127 | ); 128 | } 129 | return { 130 | title: this.directive.name, 131 | graph: { 132 | nodes: nodes.map((n) => ({ 133 | id: n.id, 134 | type: n.type, 135 | label: n.label, 136 | })), 137 | edges, 138 | }, 139 | }; 140 | } 141 | 142 | private addProviderNodes( 143 | nodes: Node[], 144 | edges: any[], 145 | addedSymbols: { [key: string]: boolean }, 146 | rootLabel: string, 147 | rootId: string, 148 | providers: InjectableSymbol[] 149 | ): void { 150 | console.log('Total provider for directive', this.directive.name, providers.length); 151 | if (providers.length > 0) { 152 | nodes.push({ 153 | id: rootId, 154 | label: rootLabel, 155 | type: { 156 | type: SymbolTypes.Meta, 157 | angular: false, 158 | }, 159 | }); 160 | edges.push({ 161 | from: getId(this.directive), 162 | to: rootId, 163 | }); 164 | } 165 | const existing: { [key: string]: number } = {}; 166 | const directiveId = getId(this.directive); 167 | providers.forEach((p) => { 168 | const id = getId(p); 169 | if (id === null) { 170 | return; 171 | } 172 | existing[id] = (existing[id] || 0) + 1; 173 | const node: Node = { 174 | id, 175 | data: p, 176 | label: p.name, 177 | type: { 178 | angular: isAngularSymbol(p), 179 | type: SymbolTypes.Provider, 180 | }, 181 | }; 182 | // Handle circular references 183 | if (!addedSymbols[id]) { 184 | nodes.push(node); 185 | addedSymbols[id] = true; 186 | } 187 | }); 188 | if (existing[directiveId]) { 189 | edges.push({ 190 | from: rootId, 191 | to: directiveId, 192 | direction: Direction.To, 193 | }); 194 | } 195 | Object.keys(existing).forEach((key: string) => { 196 | edges.push({ 197 | from: rootId, 198 | to: key, 199 | direction: Direction.To, 200 | }); 201 | }); 202 | nodes.forEach((n) => { 203 | this.symbols[n.id] = n.data; 204 | }); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/electron/states/module-tree.state.ts: -------------------------------------------------------------------------------- 1 | import { State } from './state'; 2 | import { ModuleState } from './module.state'; 3 | import { 4 | VisualizationConfig, 5 | Layout, 6 | Node, 7 | Metadata, 8 | Graph, 9 | getId, 10 | Direction, 11 | isAngularSymbol, 12 | SymbolTypes, 13 | isThirdParty 14 | } from '../../shared/data-format'; 15 | import { getModuleMetadata } from '../formatters/model-formatter'; 16 | import { Trie } from '../utils/trie'; 17 | import { NgModuleSymbol, WorkspaceSymbols } from 'ngast'; 18 | 19 | interface NodeMap { 20 | [id: string]: NgModuleSymbol; 21 | } 22 | 23 | const ModuleIndex = new Trie((str: string) => str.split(/\/|#/)); 24 | 25 | const formatModuleGraph = (data: Graph) => { 26 | return { 27 | edges: data.edges, 28 | nodes: data.nodes.map(n => ({ 29 | id: n.id, 30 | type: n.type, 31 | label: n.label 32 | })) 33 | }; 34 | }; 35 | 36 | export class ModuleTreeState extends State { 37 | private data: VisualizationConfig; 38 | private symbols: NodeMap = {}; 39 | 40 | constructor(private rootContext: WorkspaceSymbols, private module: NgModuleSymbol) { 41 | super(getId(module), rootContext); 42 | 43 | if (!ModuleIndex.size) { 44 | this.rootContext.getAllModules().forEach(m => ModuleIndex.insert(getId(m), m)); 45 | } 46 | 47 | const graph = this._getModuleGraph(module); 48 | graph.nodes.forEach(n => { 49 | if (n.data) { 50 | this.symbols[n.id] = n.data; 51 | } 52 | }); 53 | this.data = { 54 | title: `${module.name}'s imports & exports`, 55 | graph: formatModuleGraph(graph), 56 | layout: Layout.Regular 57 | }; 58 | } 59 | 60 | getMetadata(id: string): Metadata | null { 61 | const m = this.symbols[id]; 62 | if (m) { 63 | return getModuleMetadata(m); 64 | } 65 | return null; 66 | } 67 | 68 | getData(): VisualizationConfig { 69 | return this.data; 70 | } 71 | 72 | // Switch to binary search if gets too slow. 73 | nextState(id: string): State | null { 74 | const module = this.symbols[id]; 75 | // ngtsc does not allow us to resolve many of the properties 76 | // we need for third-party symbols so we don't allow the navigation. 77 | if (isThirdParty(module)) { 78 | return null; 79 | } 80 | if (module === this.module) { 81 | return new ModuleState(this.context, module); 82 | } else { 83 | return new ModuleTreeState(this.context, module); 84 | } 85 | } 86 | 87 | destroy(): void { 88 | ModuleIndex.clear(); 89 | } 90 | 91 | private _getModuleGraph(module: NgModuleSymbol): Graph { 92 | const imports = (module.getImports() || []).filter((m?: NgModuleSymbol) => !!m); 93 | const nodes: Node[] = [ 94 | { 95 | id: getId(module), 96 | label: module.name, 97 | data: module, 98 | type: { 99 | angular: isAngularSymbol(module), 100 | type: SymbolTypes.Module 101 | } 102 | } 103 | ] 104 | .concat( 105 | imports.map(m => { 106 | return { 107 | id: getId(m), 108 | label: m.name, 109 | data: m, 110 | type: { 111 | angular: isAngularSymbol(module), 112 | type: SymbolTypes.Module 113 | } 114 | }; 115 | }) 116 | ); 117 | const edges = nodes.slice(1, nodes.length).map((n) => { 118 | return { 119 | from: nodes[0].id, 120 | to: n.id, 121 | direction: Direction.To, 122 | dashes: n.type && n.type.type === SymbolTypes.LazyModule 123 | }; 124 | }); 125 | return { 126 | nodes, 127 | edges 128 | }; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/electron/states/module.state.ts: -------------------------------------------------------------------------------- 1 | import { State } from './state'; 2 | import { DirectiveState } from './directive.state'; 3 | import { 4 | Node, 5 | Edge, 6 | Metadata, 7 | getId, 8 | SymbolTypes, 9 | isAngularSymbol, 10 | VisualizationConfig, 11 | isThirdParty 12 | } from '../../shared/data-format'; 13 | import { ComponentSymbol, DirectiveSymbol, InjectableSymbol, NgModuleSymbol, PipeSymbol, WorkspaceSymbols } from 'ngast'; 14 | import { 15 | getDirectiveMetadata, 16 | getInjectableMetadata, 17 | getPipeMetadata, getModuleMetadata 18 | } from '../formatters/model-formatter'; 19 | import { ProviderState } from './provider.state'; 20 | import { PipeState } from './pipe.state'; 21 | 22 | interface DataType { 23 | symbol: InjectableSymbol | PipeSymbol | DirectiveSymbol | ComponentSymbol | NgModuleSymbol | null; 24 | metadata: any; 25 | } 26 | 27 | interface NodeMap { 28 | [id: string]: Node; 29 | } 30 | 31 | const BootstrapId = '$$bootstrap'; 32 | const DeclarationsId = '$$declarations'; 33 | const ExportsId = '$$exports'; 34 | const ProvidersId = '$$providers'; 35 | 36 | export class ModuleState extends State { 37 | private symbols: NodeMap = {}; 38 | 39 | constructor(context: WorkspaceSymbols, protected module: NgModuleSymbol) { 40 | super(getId(module), context); 41 | } 42 | 43 | getMetadata(id: string): Metadata | null { 44 | const data = this.symbols[id].data; 45 | if (!data) { 46 | return null; 47 | } 48 | if (data.symbol instanceof DirectiveSymbol || data.symbol instanceof ComponentSymbol) { 49 | return getDirectiveMetadata(data.symbol); 50 | } else if (data.symbol instanceof InjectableSymbol) { 51 | return getInjectableMetadata(data.symbol); 52 | } else if (data.symbol instanceof PipeSymbol) { 53 | return getPipeMetadata(data.symbol); 54 | } else if (data.symbol instanceof NgModuleSymbol) { 55 | return getModuleMetadata(data.symbol); 56 | } 57 | return null; 58 | } 59 | 60 | nextState(nodeId: string): State | null { 61 | if (nodeId === this.symbolId) { 62 | return null; 63 | } 64 | const data = this.symbols[nodeId].data; 65 | if (!data || !data.symbol) { 66 | return null; 67 | } 68 | // ngtsc does not allow us to resolve many of the properties 69 | // we need for third-party symbols so we don't allow the navigation. 70 | if (isThirdParty(data.symbol)) { 71 | return null; 72 | } 73 | if (data.symbol instanceof DirectiveSymbol || data.symbol instanceof ComponentSymbol) { 74 | return new DirectiveState(this.context, data.symbol); 75 | } else if (data.symbol instanceof InjectableSymbol) { 76 | return new ProviderState(this.context, data.symbol); 77 | } else if (data.symbol instanceof PipeSymbol) { 78 | return new PipeState(this.context, data.symbol); 79 | } 80 | return null; 81 | } 82 | 83 | getData(): VisualizationConfig { 84 | const currentModuleId = getId(this.module); 85 | const nodes: NodeMap = { 86 | [currentModuleId]: { 87 | id: currentModuleId, 88 | label: this.module.name, 89 | data: { 90 | symbol: this.module, 91 | metadata: null 92 | }, 93 | type: { 94 | angular: isAngularSymbol(this.module), 95 | type: SymbolTypes.Module 96 | } 97 | } 98 | }; 99 | const edges: Edge[] = []; 100 | const declarations = this.module.getDeclarations(); 101 | if (declarations?.length) { 102 | declarations.forEach(s => { 103 | const node = s; 104 | if (node instanceof PipeSymbol) { 105 | this._appendSet(DeclarationsId, s, nodes, SymbolTypes.Pipe, edges); 106 | } else { 107 | this._appendSet(DeclarationsId, s, nodes, SymbolTypes.ComponentOrDirective, edges); 108 | } 109 | }); 110 | nodes[DeclarationsId] = { 111 | id: DeclarationsId, 112 | label: 'Declarations', 113 | data: { 114 | symbol: null, 115 | metadata: null 116 | }, 117 | type: { 118 | angular: false, 119 | type: SymbolTypes.Meta 120 | } 121 | }; 122 | edges.push({ from: currentModuleId, to: DeclarationsId }); 123 | } 124 | 125 | const bootstrap = this.module.getBootstap(); 126 | if (bootstrap?.length) { 127 | bootstrap.forEach(s => { 128 | this._appendSet(BootstrapId, s, nodes, SymbolTypes.ComponentOrDirective, edges); 129 | }); 130 | nodes[BootstrapId] = { 131 | id: BootstrapId, 132 | label: 'Bootstrap', 133 | data: { 134 | symbol: null, 135 | metadata: null 136 | }, 137 | type: { 138 | angular: false, 139 | type: SymbolTypes.Meta 140 | } 141 | }; 142 | edges.push({ from: currentModuleId, to: BootstrapId }); 143 | } 144 | 145 | const exports = this.module.getExports(); 146 | if (exports?.length) { 147 | exports.forEach(node => { 148 | if (node instanceof PipeSymbol) { 149 | this._appendSet(ExportsId, node, nodes, SymbolTypes.Pipe, edges); 150 | } else if (node instanceof DirectiveSymbol || node instanceof ComponentSymbol) { 151 | this._appendSet(ExportsId, node, nodes, SymbolTypes.ComponentOrDirective, edges); 152 | } 153 | }); 154 | nodes[ExportsId] = { 155 | id: ExportsId, 156 | label: 'Exports', 157 | data: { 158 | symbol: null, 159 | metadata: null 160 | }, 161 | type: { 162 | angular: false, 163 | type: SymbolTypes.Meta 164 | } 165 | }; 166 | edges.push({ from: currentModuleId, to: ExportsId }); 167 | } 168 | const providers = this.module.getProviders().filter(provider => { 169 | if (!(provider instanceof InjectableSymbol)) { 170 | return false; 171 | } 172 | return true; 173 | }) as InjectableSymbol[]; 174 | if (providers.length) { 175 | edges.push({ from: currentModuleId, to: ProvidersId }); 176 | nodes[ProvidersId] = { 177 | id: ProvidersId, 178 | label: 'Providers', 179 | data: { 180 | symbol: null, 181 | metadata: null 182 | }, 183 | type: { 184 | angular: false, 185 | type: SymbolTypes.Meta 186 | } 187 | }; 188 | providers.forEach(provider => { 189 | this._appendSet(ProvidersId, provider, nodes, SymbolTypes.Provider, edges); 190 | }); 191 | } 192 | this.symbols = nodes; 193 | return { 194 | title: this.module.name, 195 | graph: { 196 | nodes: Object.keys(nodes).map((id: string) => { 197 | const node = nodes[id]; 198 | return { 199 | id, 200 | type: node.type, 201 | label: node.label 202 | }; 203 | }), 204 | edges: edges 205 | } 206 | }; 207 | } 208 | 209 | private _appendSet( 210 | parentSet: string, 211 | node: InjectableSymbol | PipeSymbol | DirectiveSymbol | ComponentSymbol | NgModuleSymbol, 212 | nodes: NodeMap, 213 | symbolType: SymbolTypes, 214 | edges: Edge[] 215 | ) { 216 | const id = getId(node); 217 | const name = node.name; 218 | nodes[id] = { 219 | id, 220 | label: name, 221 | data: { 222 | symbol: node, 223 | metadata: null 224 | }, 225 | type: { 226 | angular: isAngularSymbol(node), 227 | type: symbolType 228 | } 229 | }; 230 | edges.push({ 231 | from: parentSet, 232 | to: id 233 | }); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/electron/states/pipe.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Node, 3 | Metadata, 4 | getId, 5 | VisualizationConfig, 6 | Layout, 7 | Direction, 8 | isAngularSymbol, 9 | SymbolTypes, 10 | Edge, 11 | isThirdParty 12 | } from '../../shared/data-format'; 13 | import { InjectableSymbol, PipeSymbol, WorkspaceSymbols } from 'ngast'; 14 | import { State } from './state'; 15 | import { getInjectableMetadata, getPipeMetadata } from '../formatters/model-formatter'; 16 | import { ProviderState } from './provider.state'; 17 | 18 | interface NodeMap { 19 | [id: string]: InjectableSymbol | PipeSymbol; 20 | } 21 | 22 | export class PipeState extends State { 23 | private symbols: NodeMap = {}; 24 | 25 | constructor(context: WorkspaceSymbols, protected pipe: PipeSymbol) { 26 | super(getId(pipe), context); 27 | } 28 | 29 | getMetadata(id: string): Metadata | null { 30 | const s = this.symbols[id]; 31 | if (s) { 32 | if (s instanceof InjectableSymbol) { 33 | return getInjectableMetadata(s); 34 | } else { 35 | return getPipeMetadata(s); 36 | } 37 | } 38 | return null; 39 | } 40 | 41 | nextState(nodeId: string): State | null { 42 | if (nodeId === this.symbolId) { 43 | return null; 44 | } 45 | const symbol = this.symbols[nodeId]; 46 | if (symbol instanceof InjectableSymbol) { 47 | // ngtsc does not allow us to resolve many of the properties 48 | // we need for third-party symbols so we don't allow the navigation. 49 | if (!symbol || isThirdParty(symbol)) { 50 | return null; 51 | } 52 | return new ProviderState(this.context, symbol); 53 | } 54 | return null; 55 | } 56 | 57 | getData(): VisualizationConfig { 58 | const symbol = this.pipe; 59 | const nodes: Node[] = [ 60 | { 61 | id: getId(symbol), 62 | data: this.pipe as any, 63 | label: symbol.name, 64 | type: { 65 | angular: isAngularSymbol(symbol), 66 | type: SymbolTypes.Pipe 67 | } 68 | } 69 | ]; 70 | (this.pipe.getDependencies() || []).forEach(p => { 71 | if (!(p instanceof InjectableSymbol)) { 72 | return; 73 | } 74 | const m = p; 75 | nodes.push({ 76 | id: getId(m), 77 | data: p, 78 | label: p.name, 79 | type: { 80 | angular: isAngularSymbol(m), 81 | type: SymbolTypes.Provider 82 | } 83 | }); 84 | }); 85 | nodes.forEach(n => n.data && (this.symbols[n.id] = n.data)); 86 | const resultEdges: Edge[] = []; 87 | const edges = nodes.slice(1, nodes.length).forEach(n => { 88 | const data = n.data; 89 | if (data) { 90 | resultEdges.push({ 91 | from: getId(symbol), 92 | to: getId(data), 93 | direction: Direction.To 94 | }); 95 | } else { 96 | console.warn('No data for ' + symbol.name); 97 | } 98 | }); 99 | return { 100 | title: this.pipe.name, 101 | layout: Layout.Regular, 102 | graph: { 103 | edges: resultEdges, 104 | nodes: nodes.map(n => ({ 105 | id: n.id, 106 | label: n.label, 107 | type: n.type 108 | })) 109 | } 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/electron/states/provider.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Node, 3 | Metadata, 4 | getId, 5 | VisualizationConfig, 6 | Layout, 7 | Direction, 8 | isAngularSymbol, 9 | SymbolTypes, 10 | Edge, 11 | isThirdParty 12 | } from '../../shared/data-format'; 13 | import { State } from './state'; 14 | import { getInjectableMetadata } from '../formatters/model-formatter'; 15 | import { InjectableSymbol, WorkspaceSymbols } from 'ngast'; 16 | 17 | interface NodeMap { 18 | [id: string]: InjectableSymbol; 19 | } 20 | 21 | export class ProviderState extends State { 22 | private symbols: NodeMap = {}; 23 | 24 | constructor(context: WorkspaceSymbols, protected provider: InjectableSymbol) { 25 | super(getId(provider), context); 26 | } 27 | 28 | getMetadata(id: string): Metadata | null { 29 | const s = this.symbols[id]; 30 | if (s) { 31 | return getInjectableMetadata(this.symbols[id]); 32 | } 33 | return null; 34 | } 35 | 36 | nextState(nodeId: string): State | null { 37 | if (nodeId === this.symbolId) { 38 | return null; 39 | } 40 | const symbol = this.symbols[nodeId]; 41 | if (!symbol) { 42 | return null; 43 | } 44 | // ngtsc does not allow us to resolve many of the properties 45 | // we need for third-party symbols so we don't allow the navigation. 46 | if (isThirdParty(symbol)) { 47 | return null; 48 | } 49 | return new ProviderState(this.context, symbol); 50 | } 51 | 52 | getData(): VisualizationConfig { 53 | const existing: { [key: string]: number } = {}; 54 | const currentId = getId(this.provider); 55 | const nodes: Node[] = [ 56 | { 57 | id: currentId, 58 | data: this.provider, 59 | label: this.provider.name, 60 | type: { 61 | angular: isAngularSymbol(this.provider), 62 | type: SymbolTypes.Provider 63 | } 64 | } 65 | ]; 66 | existing[currentId] = 1; 67 | (this.provider.getDependencies() || []).forEach(p => { 68 | if (!(p instanceof InjectableSymbol)) { 69 | return; 70 | } 71 | const id = getId(p); 72 | if (!existing[id]) { 73 | nodes.push({ 74 | id, 75 | data: p, 76 | label: p.name, 77 | type: { 78 | angular: isAngularSymbol(p), 79 | type: SymbolTypes.Provider 80 | } 81 | }); 82 | } 83 | existing[id] = (existing[id] || 0) + 1; 84 | }); 85 | existing[currentId] -= 1; 86 | nodes.forEach(n => n.data && (this.symbols[n.id] = n.data)); 87 | const resultEdges: Edge[] = []; 88 | 89 | // Show only a single arrow 90 | Object.keys(existing).forEach(id => { 91 | if (existing[id] >= 1) { 92 | resultEdges.push({ 93 | from: currentId, 94 | to: id, 95 | direction: Direction.To 96 | }); 97 | } 98 | }); 99 | return { 100 | title: this.provider.name, 101 | layout: Layout.Regular, 102 | graph: { 103 | edges: resultEdges, 104 | nodes: nodes.map(n => ({ 105 | id: n.id, 106 | label: n.label, 107 | type: n.type 108 | })) 109 | } 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/electron/states/state.ts: -------------------------------------------------------------------------------- 1 | import { VisualizationConfig, Metadata } from '../../shared/data-format'; 2 | import { WorkspaceSymbols } from 'ngast'; 3 | 4 | export abstract class State { 5 | constructor(protected symbolId: string, protected context: WorkspaceSymbols) {} 6 | 7 | abstract getMetadata(id: string): Metadata | null; 8 | 9 | abstract getData(): VisualizationConfig; 10 | 11 | abstract nextState(id: string): State | null; 12 | 13 | destroy(): void {} 14 | 15 | get stateSymbolId(): string { 16 | return this.symbolId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/electron/states/template.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentSymbol, 3 | DirectiveSymbol, 4 | WorkspaceSymbols, 5 | TemplateNode, 6 | } from 'ngast'; 7 | import { State } from './state'; 8 | import { 9 | VisualizationConfig, 10 | Metadata, 11 | getId, 12 | Node, 13 | SymbolTypes, 14 | isThirdParty, Edge 15 | } from '../../shared/data-format'; 16 | import { 17 | getDirectiveMetadata, 18 | getElementMetadata, 19 | } from '../formatters/model-formatter'; 20 | import { DirectiveState } from './directive.state'; 21 | 22 | interface NodeMap { 23 | [id: string]: ComponentSymbol | DirectiveSymbol | TemplateNode; 24 | } 25 | 26 | const TemplateId = 'template'; 27 | 28 | export class TemplateState extends State { 29 | private symbols: NodeMap = {}; 30 | 31 | constructor(context: WorkspaceSymbols, protected directive: ComponentSymbol) { 32 | super(getId(directive), context); 33 | } 34 | 35 | getMetadata(id: string): Metadata | null { 36 | const s = this.symbols[id]; 37 | if (s) { 38 | if (s instanceof ComponentSymbol || s instanceof DirectiveSymbol) { 39 | // We can't analyze well symbols coming from node modules. 40 | return isThirdParty(s) ? null : getDirectiveMetadata(s); 41 | } else { 42 | return getElementMetadata(s); 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | nextState(id: string): State | null { 49 | if (id === this.symbolId) { 50 | return null; 51 | } 52 | const symbol = this.symbols[id]; 53 | if (!symbol) { 54 | return null; 55 | } 56 | // ngtsc does not allow us to resolve many of the properties 57 | // we need for third-party symbols so we don't allow the navigation. 58 | if ((symbol instanceof ComponentSymbol || symbol instanceof DirectiveSymbol) && 59 | isThirdParty(symbol)) { 60 | return null; 61 | } 62 | if (symbol instanceof ComponentSymbol) { 63 | return new DirectiveState(this.context, symbol); 64 | } else { 65 | return null; 66 | } 67 | } 68 | 69 | getData(): VisualizationConfig { 70 | const label = `${this.directive.name}'s Template`; 71 | const nodes: Node[] = [ 72 | { 73 | id: TemplateId, 74 | label, 75 | type: { 76 | type: SymbolTypes.Meta, 77 | angular: false, 78 | }, 79 | }, 80 | ]; 81 | const edges: Edge[] = []; 82 | this.addTemplateNodes(nodes, edges); 83 | return { 84 | title: label, 85 | graph: { 86 | nodes: nodes.map(node => ({ 87 | id: node.id, 88 | label: node.label, 89 | type: node.type 90 | })), 91 | edges, 92 | }, 93 | }; 94 | } 95 | 96 | private addTemplateNodes( 97 | resNodes: Node[], 98 | edges: Edge[] 99 | ) { 100 | const rootNodes = this.directive.getTemplateAst(); 101 | if (!rootNodes) { 102 | return; 103 | } 104 | let currentNode = 0; 105 | const addNodes = (nodes: TemplateNode[], parentNodeId: string) => { 106 | nodes.forEach((n) => { 107 | if (!n) return; 108 | 109 | currentNode += 1; 110 | const nodeId = 'el-' + currentNode.toString(); 111 | edges.push({ 112 | from: parentNodeId, 113 | to: nodeId, 114 | }); 115 | const node = { 116 | id: nodeId, 117 | label: n.name, 118 | data: n, 119 | type: { 120 | angular: false, 121 | type: n.directives.length 122 | ? SymbolTypes.HtmlElementWithDirective 123 | : SymbolTypes.HtmlElement, 124 | }, 125 | }; 126 | this.symbols[nodeId] = n; 127 | if (n.component) { 128 | this.symbols[nodeId] = n.component; 129 | node.type.type = SymbolTypes.Component; 130 | } 131 | resNodes.push(node); 132 | addNodes( 133 | n.children, 134 | nodeId 135 | ); 136 | }); 137 | }; 138 | addNodes( 139 | rootNodes, 140 | TemplateId 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/electron/utils/trie.ts: -------------------------------------------------------------------------------- 1 | export class Node { 2 | children: { [key: string]: Node } = {}; 3 | data?: T; 4 | } 5 | 6 | export interface SplitFunction { 7 | (str: string): string[]; 8 | } 9 | 10 | const defaultSplit = (str: string) => str.split(''); 11 | 12 | export class Trie { 13 | private _root = new Node(); 14 | private _size = 0; 15 | 16 | constructor(private splitFunction: SplitFunction = defaultSplit) {} 17 | 18 | get size() { 19 | return this._size; 20 | } 21 | 22 | insert(key: string, data: T) { 23 | const keyParts = this.splitFunction(key); 24 | let node = this.findNode(key, true); 25 | node.data = data; 26 | this._size += 1; 27 | } 28 | 29 | get(key: string): T | null { 30 | const node = this.findNode(key); 31 | return node.data || null; 32 | } 33 | 34 | clear() { 35 | this._root = new Node(); 36 | this._size = 0; 37 | } 38 | 39 | private findNode(key: string, createIfDoesNotExist = false): Node { 40 | const parts = this.splitFunction(key); 41 | let currentNode = this._root; 42 | for (let i = 0; i < parts.length; i += 1) { 43 | let child = currentNode.children[parts[i]]; 44 | if (!child) { 45 | if (createIfDoesNotExist) { 46 | child = new Node(); 47 | currentNode.children[parts[i]] = child; 48 | } else { 49 | return currentNode; 50 | } 51 | } 52 | currentNode = child; 53 | } 54 | return currentNode; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'DEV' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: true, 3 | environment: 'PROD' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'LOCAL' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.web.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'WEB' 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngrev 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-electron'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: [ 'html', 'lcovonly' ], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | browsers: ['AngularElectron'], 28 | customLaunchers: { 29 | AngularElectron: { 30 | base: 'Electron', 31 | flags: [ 32 | '--remote-debugging-port=9222' 33 | ], 34 | browserWindowOptions: { 35 | webPreferences: { 36 | nodeIntegration: true, 37 | nodeIntegrationInSubFrames: true, 38 | allowRunningInsecureContent: true, 39 | enableRemoteModule: true 40 | } 41 | } 42 | } 43 | } 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from "@angular/core"; 2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; 3 | 4 | import { AppModule } from "./app/app.module"; 5 | import { AppConfig } from "./environments/environment"; 6 | 7 | if (AppConfig.production) { 8 | enableProdMode(); 9 | } 10 | 11 | declare global { 12 | interface Window { 13 | require: any; 14 | Buffer: any; 15 | } 16 | } 17 | 18 | platformBrowserDynamic() 19 | .bootstrapModule(AppModule, { 20 | preserveWhitespaces: false, 21 | }) 22 | .catch((err) => console.error(err)); 23 | -------------------------------------------------------------------------------- /src/polyfills-test.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es/reflect'; 2 | import 'zone.js/dist/zone'; 3 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/shared/data-format.ts: -------------------------------------------------------------------------------- 1 | import { R3InjectableMetadata } from '@angular/compiler'; 2 | import { BoxTheme, Theme } from './themes/color-map'; 3 | import { AnnotationNames } from 'ngast'; 4 | 5 | export interface Graph { 6 | nodes: Node[]; 7 | edges: Edge[]; 8 | } 9 | 10 | export enum SymbolTypes { 11 | Provider = 'provider', 12 | HtmlElement = 'html-element', 13 | HtmlElementWithDirective = 'html-element-with-directive', 14 | ComponentWithDirective = 'component-with-directive', 15 | Component = 'component', 16 | Directive = 'directive', 17 | ComponentOrDirective = 'component-or-directive', 18 | Pipe = 'pipe', 19 | Module = 'module', 20 | LazyModule = 'lazy-module', 21 | Meta = 'meta', 22 | Unknown = 'unknown' 23 | } 24 | 25 | export interface IdentifiedStaticSymbol { 26 | id: string; 27 | name: string; 28 | annotation: AnnotationNames; 29 | path: string; 30 | } 31 | 32 | export interface SymbolType { 33 | angular: boolean; 34 | type: SymbolTypes; 35 | } 36 | 37 | export interface Node { 38 | id: string; 39 | label: string; 40 | data?: T; 41 | type?: SymbolType; 42 | styles?: BoxTheme; 43 | } 44 | 45 | export enum Direction { 46 | From, 47 | To, 48 | Both 49 | } 50 | 51 | export interface Edge { 52 | from: string; 53 | to: string; 54 | direction?: Direction; 55 | data?: any; 56 | dashes?: boolean; 57 | } 58 | 59 | export enum Layout { 60 | HierarchicalLRDirected, 61 | HierarchicalUDDirected, 62 | Regular 63 | } 64 | 65 | export interface Config { 66 | showLibs: boolean; 67 | showModules: boolean; 68 | theme: string; 69 | themes: { [key: string]: Theme }; 70 | } 71 | 72 | export interface VisualizationConfig { 73 | layout?: Layout; 74 | title: string; 75 | graph: Graph; 76 | } 77 | 78 | export type StringPair = { key: string; value: string | null }; 79 | 80 | export interface Metadata { 81 | properties: StringPair[]; 82 | filePath?: string | null; 83 | } 84 | 85 | export const getId = (symbol: { name: string; path: string }): string => { 86 | return `${symbol.path}#${symbol.name}`; 87 | }; 88 | 89 | export const getProviderName = (provider: R3InjectableMetadata): string | null => { 90 | if (provider.type.value) { 91 | return provider.name; 92 | } 93 | return null; 94 | }; 95 | 96 | export const isAngularSymbol = (symbol: { path: string }): boolean => { 97 | return /node_modules\/@angular/.test(symbol.path); 98 | }; 99 | 100 | export const isThirdParty = (symbol: { path: string }): boolean => { 101 | return /node_modules/.test(symbol.path); 102 | }; 103 | -------------------------------------------------------------------------------- /src/shared/ipc-constants.ts: -------------------------------------------------------------------------------- 1 | export enum Message { 2 | ToggleLibsMenuAction = 'toggle-libs-menu-action', 3 | ToggleLibs = 'toggle-libs', 4 | ToggleModulesMenuAction = 'toggle-modules-menu-action', 5 | ToggleModules = 'toggle-modules', 6 | LoadProject = 'load-project', 7 | PrevState = 'prev-state', 8 | GetMetadata = 'get-metadata', 9 | GetData = 'get-data', 10 | NextState = 'next-state', 11 | GetSymbols = 'get-symbols', 12 | DirectStateTransition = 'direct-state-transition', 13 | SaveImage = 'save-image', 14 | ChangeTheme = 'change-theme', 15 | ImageData = 'image-data', 16 | DisableExport = 'disable-export', 17 | EnableExport = 'enable-export', 18 | Config = 'config', 19 | FitView = 'fit-view' 20 | } 21 | 22 | export enum Status { 23 | Failure = 0, 24 | Success = 1 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/themes/color-map.ts: -------------------------------------------------------------------------------- 1 | export const DefaultTheme = 'Light'; 2 | 3 | export interface BoxHighlightColor { 4 | background: string; 5 | border: string; 6 | } 7 | 8 | export interface BoxColor { 9 | background: string; 10 | border: string; 11 | highlight: BoxHighlightColor; 12 | } 13 | 14 | export interface BoxFont { 15 | color: string; 16 | } 17 | 18 | export interface BoxTheme { 19 | margin: number; 20 | color: BoxColor; 21 | labelHighlightBold: boolean; 22 | font: BoxFont; 23 | } 24 | 25 | export interface LegendTheme { 26 | background: string; 27 | font: string; 28 | title: string; 29 | border: string; 30 | } 31 | 32 | export interface BackButtonTheme { 33 | background: string; 34 | font: string; 35 | border: string; 36 | } 37 | 38 | export interface ArrowTheme { 39 | color: string; 40 | highlight: string; 41 | } 42 | 43 | export interface FuzzySearchTheme { 44 | font: string; 45 | background: string; 46 | border: string; 47 | shadowColor: string; 48 | selected: string; 49 | } 50 | 51 | export interface Theme { 52 | name: string; 53 | historyLabel: string; 54 | legend: LegendTheme; 55 | backButton: BackButtonTheme; 56 | background: string; 57 | arrow: ArrowTheme; 58 | fuzzySearch: FuzzySearchTheme; 59 | component: BoxTheme; 60 | 'component-or-directive': BoxTheme; 61 | 'component-with-directive': BoxTheme; 62 | 'html-element': BoxTheme; 63 | 'html-element-with-directive': BoxTheme; 64 | module: BoxTheme; 65 | 'lazy-module': BoxTheme; 66 | provider: BoxTheme; 67 | pipe: BoxTheme; 68 | } 69 | 70 | export const DefaultColor: BoxTheme = { 71 | margin: 10, 72 | color: { 73 | background: '#90A4AE', 74 | border: '#607D8B', 75 | highlight: { 76 | background: '#90A4AE', 77 | border: '#607D8B' 78 | } 79 | }, 80 | labelHighlightBold: false, 81 | font: { 82 | color: '#FFFFFF' 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | 6 | height: 100%; 7 | font-family: Arial, Helvetica, sans-serif; 8 | } 9 | 10 | /* CAN (MUST) BE REMOVED ! Sample Global style */ 11 | .container { 12 | height: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | 18 | .title { 19 | color: white; 20 | margin: 0; 21 | padding: 50px 20px; 22 | } 23 | 24 | a { 25 | color: #fff !important; 26 | text-transform: uppercase; 27 | text-decoration: none; 28 | background: #ed3330; 29 | padding: 20px; 30 | border-radius: 5px; 31 | display: inline-block; 32 | border: none; 33 | transition: all 0.4s ease 0s; 34 | 35 | &:hover { 36 | background: #fff; 37 | color: #ed3330 !important; 38 | letter-spacing: 1px; 39 | -webkit-box-shadow: 0px 5px 40px -10px rgba(0,0,0,0.57); 40 | -moz-box-shadow: 0px 5px 40px -10px rgba(0,0,0,0.57); 41 | box-shadow: 5px 40px -10px rgba(0,0,0,0.57); 42 | transition: all 0.4s ease 0s; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "include": [ 10 | "main.ts", 11 | "polyfill.ts" 12 | ], 13 | "exclude": [ 14 | "**/*.spec.ts" 15 | ], 16 | "angularCompilerOptions": { 17 | "fullTemplateTypeCheck": true, 18 | "strictInjectionParameters": true, 19 | "preserveWhitespaces": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills-test.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ], 19 | "exclude": [ 20 | "dist", 21 | "release", 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare const nodeModule: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | interface Window { 7 | process: any; 8 | require: any; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "es2020", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "strict": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "es2016", 19 | "es2015", 20 | "dom" 21 | ] 22 | }, 23 | "files": [ 24 | "src/main.ts", 25 | "src/polyfills.ts" 26 | ], 27 | "include": [ 28 | "src/**/*.d.ts", 29 | "src/shared/**/*.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ], 34 | "angularCompilerOptions": { 35 | "strictInjectionParameters": true, 36 | "strictInputAccessModifiers": true, 37 | "strictTemplates": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "strict": true, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "target": "es5", 10 | "types": [ 11 | "node" 12 | ], 13 | "lib": [ 14 | "es2017", 15 | "es2016", 16 | "es2015", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "main.ts", 22 | "src/electron/**/*.ts", 23 | "src/shared/**/*.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "**/*.spec.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | const args = process.argv.slice(1); 2 | export const isDev = args.some(val => val === '--serve'); 3 | --------------------------------------------------------------------------------