├── .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 |
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 |
53 |
54 |
55 |
56 |
57 |
58 |
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 | [
](https://github.com/mgechev) |[
](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 |
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 | Name |
4 | Value |
5 |
6 |
7 |
8 | {{pair.key}} |
9 | {{pair.value}} |
10 |
11 |
12 |
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 |
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 |
--------------------------------------------------------------------------------