├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── images │ ├── ionic-angular-example.png │ ├── self-example.png │ └── vis-network-example.png ├── package-lock.json ├── package.json ├── src ├── app │ ├── components │ │ ├── App.tsx │ │ ├── ControlPanel.tsx │ │ ├── DepGraph.tsx │ │ └── SelectModules.tsx │ ├── hooks │ │ └── filters.ts │ ├── index.html │ ├── index.tsx │ ├── utils │ │ ├── deps.ts │ │ ├── format.ts │ │ ├── icons.ts │ │ ├── parsers.ts │ │ └── types.ts │ └── workers │ │ ├── graph.worker.ts │ │ └── types.d.ts └── cli │ ├── main.ts │ ├── reporter.ts │ └── server.ts ├── tsconfig.json └── webpack.config.js /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM and GitHub Packages 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish_to_npm: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v2 14 | - name: Set up Node.js and NPM 15 | uses: actions/setup-node@v2 16 | with: 17 | registry-url: 'https://registry.npmjs.org' 18 | scope: '@rx-angular' 19 | node-version-file: .nvmrc 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Publish to NPM 23 | run: npm publish --access public 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | 27 | publish_to_gpr: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Check out repository 31 | uses: actions/checkout@v2 32 | - name: Set up Node.js and NPM 33 | uses: actions/setup-node@v2 34 | with: 35 | registry-url: 'https://npm.pkg.github.com' 36 | scope: '@rx-angular' 37 | node-version-file: .nvmrc 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Publish to GPR 41 | run: npm publish 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # IDE 107 | .vscode/ 108 | .idea/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.15.1 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.15.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.2](https://github.com/rx-angular/import-graph-visualizer/compare/v0.2.1...v0.2.2) (2024-02-10) 6 | 7 | ### [0.2.1](https://github.com/rx-angular/import-graph-visualizer/compare/v0.2.0...v0.2.1) (2024-02-10) 8 | 9 | 10 | ### Features 11 | 12 | * **app:** update material icons from VSCode extension ([e8b3d62](https://github.com/rx-angular/import-graph-visualizer/commit/e8b3d623df720fd33f32304c250067e6f49015ff)) 13 | 14 | ## [0.2.0](https://github.com/rx-angular/import-graph-visualizer/compare/v0.1.2...v0.2.0) (2021-05-14) 15 | 16 | 17 | ### Features 18 | 19 | * **app:** create graph using BFS if source or target missing ([91af9d9](https://github.com/rx-angular/import-graph-visualizer/commit/91af9d95542a11bf55189c3c54f0cabbc7c5375f)) 20 | 21 | ### [0.1.2](https://github.com/rx-angular/import-graph-visualizer/compare/v0.1.1...v0.1.2) (2021-04-14) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * enable publishing to npmjs.com ([156c10c](https://github.com/rx-angular/import-graph-visualizer/commit/156c10ccb7ff48e4e0d269c5c15ed5cb948ca743)) 27 | 28 | ### [0.1.1](https://github.com/rx-angular/import-graph-visualizer/compare/v0.1.0...v0.1.1) (2021-04-13) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * update installation docs in README ([890f0a9](https://github.com/rx-angular/import-graph-visualizer/commit/890f0a9e946fa362c43b9fbba6861b27130a9c43)) 34 | 35 | ## 0.1.0 (2021-04-13) 36 | 37 | 38 | ### Features 39 | 40 | * **app:** add file icons in autocomplete and graph ([9c6568e](https://github.com/rx-angular/import-graph-visualizer/commit/9c6568e06e320957906e17d64b3e5b759c5a1302)) 41 | * **app:** add snackbars ([7593faa](https://github.com/rx-angular/import-graph-visualizer/commit/7593faad5d0b13e25e98566426b32eb8acf6c7de)) 42 | * **app:** autocomplete for modules ([545394a](https://github.com/rx-angular/import-graph-visualizer/commit/545394add19ab38ea7b5fa27ede655d568d5e02b)) 43 | * **app:** button for terminating worker ([dfb481e](https://github.com/rx-angular/import-graph-visualizer/commit/dfb481ee669d2933383ebb43ce18446c44ba1150)) 44 | * **app:** create dep-graph for roots and leaves ([3ed86a2](https://github.com/rx-angular/import-graph-visualizer/commit/3ed86a24dcb3068861f318953be670c85db7b7ab)) 45 | * **app:** dep graph WIP ([6e57ec7](https://github.com/rx-angular/import-graph-visualizer/commit/6e57ec7ba5efb5405c86f12d19e21e1e2b9f1afb)) 46 | * **app:** fetch list of modules ([2bfd3e8](https://github.com/rx-angular/import-graph-visualizer/commit/2bfd3e88a95a27c0214314986c9884219e5612d4)) 47 | * **app:** get material icons from VSCode extension ([c670982](https://github.com/rx-angular/import-graph-visualizer/commit/c670982a8d780b2267678d73fda7ecf72ab62f66)) 48 | * **app:** improve graph readability ([cc1a206](https://github.com/rx-angular/import-graph-visualizer/commit/cc1a206de07447c5134807e25f83db6ff3a18402)) 49 | * **app:** improve graph styles ([5273edc](https://github.com/rx-angular/import-graph-visualizer/commit/5273edc5d15c3d33ac81c2f43c8930439fb91b2f)) 50 | * **app:** improve layout ([76140a5](https://github.com/rx-angular/import-graph-visualizer/commit/76140a536484bdf822662383d0c9c9529fe7d0d4)) 51 | * **app:** sync filters with url ([fb58ee6](https://github.com/rx-angular/import-graph-visualizer/commit/fb58ee6fd1173db68347842bf7cafb1eb7e7ce35)) 52 | * **cli:** make ts-config argument optional ([a850f8c](https://github.com/rx-angular/import-graph-visualizer/commit/a850f8ce7c99ea55f15bb4ab84c9e5171e584f93)) 53 | * **cli:** print progress messages ([817feba](https://github.com/rx-angular/import-graph-visualizer/commit/817feba576ba33496768b36268750adf13b671f1)) 54 | * generate json via dependency-cruiser ([28edf0a](https://github.com/rx-angular/import-graph-visualizer/commit/28edf0a3f1beabfbf392c24d3f93cc0d291e843f)) 55 | * set up Node.js CLI tool ([e20f3d9](https://github.com/rx-angular/import-graph-visualizer/commit/e20f3d9e6a0565077d7d5579faa4bde357940207)) 56 | * set up Webpack + React + TypeScript ([662ef93](https://github.com/rx-angular/import-graph-visualizer/commit/662ef93c445cd1f231d1e236923f41c2d777f979)) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **app:** exclude unrelated modules from graph ([f2f8861](https://github.com/rx-angular/import-graph-visualizer/commit/f2f88617b3d4d5535d95cbd72d57942d5b27dc5c)) 62 | * **app:** reverse graph search direction ([b46360a](https://github.com/rx-angular/import-graph-visualizer/commit/b46360a7c6c255cf3f9872a9522afdd5f9dffdb1)) 63 | * **app:** style non-local modules in SelectModules ([c60b4ac](https://github.com/rx-angular/import-graph-visualizer/commit/c60b4ac773ea7999b24203859aeeba93d6676510)) 64 | * **cli:** resolve typescript path aliases ([8b07450](https://github.com/rx-angular/import-graph-visualizer/commit/8b07450b231028517573e6b993ecb7f9ed4505d2)) 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 FlowUp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Import Graph Visualizer 2 | 3 | [![npm version](https://img.shields.io/npm/v/@rx-angular/import-graph-visualizer.svg)](https://www.npmjs.com/package/@rx-angular/import-graph-visualizer) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | **Import Graph Visualizer is a development tool for filtering and visualizing import paths within a JavaScript/TypeScript application.** 7 | 8 | ## Motivation 9 | 10 | Although there are already excellent tools out there for visualizing imports between Node.js modules (e.g. [Dependency Cruiser](https://github.com/sverweij/dependency-cruiser), which is actually used by this tool under the hood), for large apps these graphs may be too large to comprehend. 11 | This is why Import Graph Visualizer allows filtering import paths by source and target modules, allowing you to zoom in to a limited subsection of your app, which will likely be easier to analyze than the entire app as a whole. 12 | 13 | ## Examples 14 | 15 | Screenshot of Import Graph Visualizer for this repository: 16 | 17 | ![self example](https://raw.githubusercontent.com/rx-angular/import-graph-visualizer/main/docs/images/self-example.png) 18 | 19 | For an Ionic/Angular starter project: 20 | 21 | ![Ionic/Angular example](https://raw.githubusercontent.com/rx-angular/import-graph-visualizer/main/docs/images/ionic-angular-example.png) 22 | 23 | For `vis-network` repo: 24 | 25 | ![vis-network example](https://raw.githubusercontent.com/rx-angular/import-graph-visualizer/main/docs/images/vis-network-example.png) 26 | 27 | ## Setup 28 | 29 | To install as a development dependency in your Node project: 30 | 31 | ```sh 32 | npm install --save-dev @rx-angular/import-graph-visualizer 33 | ``` 34 | 35 | Alternatively, to install as a global dependency (prefix with `sudo` on Unix systems): 36 | 37 | ```sh 38 | npm install --global @rx-angular/import-graph-visualizer 39 | ``` 40 | 41 | ## Usage 42 | 43 | To run this tool, you must supply at least one entry point for your application (e.g. `src/index.js`): 44 | 45 | ```sh 46 | npx @rx-angular/import-graph-visualizer --entry-points path/to/entry/module 47 | ``` 48 | 49 | For Typescript projects that use path aliases, make sure to also specify your `tsconfig.json` path: 50 | 51 | ```sh 52 | npx @rx-angular/import-graph-visualizer --entry-points path/to/entry/module --ts-config path/to/tsconfig 53 | ``` 54 | 55 | For greater convenience, you may wish to add a script to your `package.json`: 56 | 57 | ```jsonc 58 | { 59 | // ... 60 | "scripts": { 61 | // ... 62 | "import-graph-visualizer": "import-graph-visualizer --entry-points path/to/entry/module ..." 63 | } 64 | } 65 | ``` 66 | 67 | Then you may run it as: 68 | 69 | ```sh 70 | npm run import-graph-visualizer 71 | ``` 72 | 73 | ## Development 74 | 75 | For local development, clone this repo and first install dependencies with: 76 | 77 | ```sh 78 | npm install 79 | ``` 80 | 81 | Then pick a project for testing and generate its dependencies using the CLI: 82 | 83 | ```sh 84 | npm run reporter -- --entry-points path/to/entry/module --ts-config path/to/tsconfig/file 85 | ``` 86 | 87 | Then run a development server with: 88 | 89 | ```sh 90 | npm start 91 | ``` 92 | 93 | If you need to update icons and mappings from [VSCode Material Icon Theme](https://github.com/PKief/vscode-material-icon-theme), run: 94 | 95 | ```sh 96 | npm install --save-dev vscode-material-icons@latest 97 | ``` 98 | 99 | --- 100 | 101 | made with ❤ by [push-based.io](https://www.push-based.io) -------------------------------------------------------------------------------- /docs/images/ionic-angular-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rx-angular/import-graph-visualizer/78f8789e63dd26acaa43a238ca19778a7b5c58a4/docs/images/ionic-angular-example.png -------------------------------------------------------------------------------- /docs/images/self-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rx-angular/import-graph-visualizer/78f8789e63dd26acaa43a238ca19778a7b5c58a4/docs/images/self-example.png -------------------------------------------------------------------------------- /docs/images/vis-network-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rx-angular/import-graph-visualizer/78f8789e63dd26acaa43a238ca19778a7b5c58a4/docs/images/vis-network-example.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rx-angular/import-graph-visualizer", 3 | "version": "0.2.2", 4 | "description": "A development tool for filtering and visualizing import paths within a JavaScript/TypeScript application", 5 | "bin": "dist/cli/main.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "prepublishOnly": "npm run build", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "webpack serve --open --hot --mode development", 13 | "build": "npm run build:app && npm run build:cli", 14 | "build:app": "webpack --mode production", 15 | "build:cli": "tsc src/cli/main.ts --strict --esModuleInterop --outDir dist/cli", 16 | "reporter": "npm run build:cli && node dist/cli/reporter.js", 17 | "release": "standard-version", 18 | "release:dry-run": "standard-version --dry-run" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/rx-angular/import-graph-visualizer.git" 23 | }, 24 | "author": "Matěj Chalk ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/rx-angular/import-graph-visualizer/issues" 28 | }, 29 | "homepage": "https://github.com/rx-angular/import-graph-visualizer#readme", 30 | "devDependencies": { 31 | "@types/express": "^4.17.11", 32 | "@types/react": "^17.0.3", 33 | "@types/react-dom": "^17.0.2", 34 | "@types/react-router-dom": "^5.1.7", 35 | "@types/react-window": "^1.8.2", 36 | "@types/yargs": "^16.0.0", 37 | "copy-webpack-plugin": "^8.1.0", 38 | "html-webpack-plugin": "^5.3.1", 39 | "standard-version": "^9.2.0", 40 | "ts-loader": "^8.0.18", 41 | "webpack": "^5.26.0", 42 | "webpack-cli": "^4.5.0", 43 | "webpack-dev-server": "^3.11.2", 44 | "worker-loader": "^3.0.8" 45 | }, 46 | "dependencies": { 47 | "@material-ui/core": "^4.11.3", 48 | "@material-ui/icons": "^4.11.2", 49 | "@material-ui/lab": "^4.0.0-alpha.57", 50 | "dependency-cruiser": "^9.23.3", 51 | "express": "^4.17.1", 52 | "match-sorter": "^6.3.0", 53 | "open": "^8.0.5", 54 | "ora": "^5.4.0", 55 | "react": "^17.0.1", 56 | "react-dom": "^17.0.1", 57 | "react-router-dom": "^5.2.0", 58 | "react-window": "^1.8.6", 59 | "typescript": "^5.3.3", 60 | "vis-network": "^9.0.4", 61 | "vscode-material-icons": "^0.1.0", 62 | "yargs": "^16.2.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { LinearProgress } from '@material-ui/core'; 2 | import { ICruiseResult } from 'dependency-cruiser'; 3 | import React, { FC, useEffect, useMemo, useState } from 'react'; 4 | import { useFilters } from '../hooks/filters'; 5 | import { parseModuleDeps } from '../utils/parsers'; 6 | import ControlPanel from './ControlPanel'; 7 | import DepGraph from './DepGraph'; 8 | 9 | const JSON_URL = 10 | process.env.NODE_ENV === 'production' 11 | ? '/assets/reporter-output.json' 12 | : '../../../dist/cli/reporter-output.json'; 13 | 14 | const App: FC = () => { 15 | const [data, setData] = useState(); 16 | 17 | const moduleDeps = useMemo(() => data && parseModuleDeps(data), [data]); 18 | 19 | useEffect(() => { 20 | fetch(JSON_URL) 21 | .then(response => response.json()) 22 | .then(json => { 23 | setData(json); 24 | }); 25 | }, []); 26 | 27 | const [filters, setFilters] = useFilters(); 28 | 29 | if (moduleDeps == null) { 30 | return ; 31 | } 32 | 33 | return ( 34 | <> 35 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /src/app/components/ControlPanel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Card, 4 | CardActions, 5 | CardContent, 6 | CardHeader, 7 | Container, 8 | Grid, 9 | } from '@material-ui/core'; 10 | import React, { FC, useMemo, useState } from 'react'; 11 | import { getModules } from '../utils/deps'; 12 | import { Filters, Module, ModuleDeps } from '../utils/types'; 13 | import SelectModules from './SelectModules'; 14 | 15 | type Props = { 16 | moduleDeps: ModuleDeps; 17 | filters: Filters; 18 | onSubmit?: (filters: Filters) => void; 19 | }; 20 | 21 | const ControlPanel: FC = ({ moduleDeps, filters, onSubmit }) => { 22 | const modules = useMemo(() => getModules(moduleDeps), [moduleDeps]); 23 | 24 | const [targetModules, setTargetModules] = useState( 25 | filters.targetModules, 26 | ); 27 | const [sourceModules, setSourceModules] = useState( 28 | filters.sourceModules, 29 | ); 30 | 31 | const targetModulesValue = useMemo( 32 | () => targetModules.map(path => moduleDeps.modules[path]), 33 | [targetModules, moduleDeps], 34 | ); 35 | const sourceModulesValue = useMemo( 36 | () => sourceModules.map(path => moduleDeps.modules[path]), 37 | [sourceModules, moduleDeps], 38 | ); 39 | 40 | const handleTargetModulesChange = (modules: Module[]) => { 41 | setTargetModules(modules.map(({ path }) => path)); 42 | }; 43 | const handleSourceModulesChange = (modules: Module[]) => { 44 | setSourceModules(modules.map(({ path }) => path)); 45 | }; 46 | const handleSubmit = () => { 47 | onSubmit?.({ targetModules: targetModules, sourceModules: sourceModules }); 48 | }; 49 | 50 | return ( 51 | 52 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | export default ControlPanel; 92 | -------------------------------------------------------------------------------- /src/app/components/DepGraph.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Snackbar, SnackbarContent } from '@material-ui/core'; 2 | import React, { FC, useEffect, useRef, useState } from 'react'; 3 | import { Edge, Network, Node } from 'vis-network/standalone'; 4 | import { getIconUrlByName, getIconUrlForFilePath } from 'vscode-material-icons'; 5 | import Worker from 'worker-loader!../workers/graph.worker'; 6 | import { filenameFromPath } from '../utils/format'; 7 | import { ICONS_URL } from '../utils/icons'; 8 | import { DepGraph, Filters, ModuleDeps } from '../utils/types'; 9 | 10 | type Props = { 11 | moduleDeps: ModuleDeps; 12 | filters: Filters; 13 | }; 14 | 15 | type Stage = 'idle' | 'computing' | 'drawing'; 16 | 17 | const DepGraph: FC = ({ moduleDeps, filters }) => { 18 | const containerRef = useRef(null); 19 | 20 | const [graph, setGraph] = useState(); 21 | 22 | const [worker, setWorker] = useState(); 23 | 24 | const [stage, setStage] = useState('idle'); 25 | 26 | useEffect(() => { 27 | setStage('computing'); 28 | if (worker != null) { 29 | worker.terminate(); 30 | } 31 | const newWorker = new Worker(); 32 | newWorker.postMessage({ moduleDeps, ...filters }); 33 | newWorker.onmessage = ({ data }: MessageEvent) => { 34 | setGraph(data); 35 | setStage('drawing'); 36 | newWorker.terminate(); 37 | }; 38 | setWorker(newWorker); 39 | }, [moduleDeps, filters]); 40 | 41 | useEffect(() => { 42 | if (containerRef.current && graph != null) { 43 | const nodes = graph.modules.map( 44 | ({ path, isLocal }): Node => ({ 45 | id: path, 46 | label: isLocal ? filenameFromPath(path) : path, 47 | title: path, 48 | image: isLocal 49 | ? getIconUrlForFilePath(path, ICONS_URL) 50 | : getIconUrlByName('npm', ICONS_URL), 51 | }), 52 | ); 53 | 54 | const edges = graph.imports.map( 55 | ({ fromPath, toPath, isDynamic }): Edge => ({ 56 | from: fromPath, 57 | to: toPath, 58 | dashes: isDynamic, 59 | }), 60 | ); 61 | 62 | const network = new Network( 63 | containerRef.current, 64 | { edges, nodes }, 65 | { 66 | nodes: { 67 | shape: 'image', 68 | shapeProperties: { 69 | useBorderWithImage: true, 70 | }, 71 | image: getIconUrlByName('file', ICONS_URL), 72 | color: { 73 | border: '#888', 74 | background: '#fff', 75 | highlight: { 76 | border: '#888', 77 | background: '#eee', 78 | }, 79 | }, 80 | }, 81 | edges: { 82 | arrows: 'to', 83 | color: '#888', 84 | }, 85 | }, 86 | ); 87 | 88 | network.on('afterDrawing', () => { 89 | setStage('idle'); 90 | }); 91 | } 92 | }, [containerRef.current, graph]); 93 | 94 | const handleTerminate = () => { 95 | if (worker != null) { 96 | worker.terminate(); 97 | setWorker(undefined); 98 | setStage('idle'); 99 | } 100 | }; 101 | 102 | return ( 103 | <> 104 |
114 | 115 | 119 | Terminate 120 | 121 | } 122 | /> 123 | 124 | 125 | 126 | 127 | 128 | ); 129 | }; 130 | 131 | export default DepGraph; 132 | -------------------------------------------------------------------------------- /src/app/components/SelectModules.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Grid, 3 | Icon, 4 | makeStyles, 5 | TextField, 6 | Typography, 7 | useMediaQuery, 8 | useTheme, 9 | } from '@material-ui/core'; 10 | import { 11 | Autocomplete, 12 | AutocompleteRenderInputParams, 13 | FilterOptionsState, 14 | } from '@material-ui/lab'; 15 | import { matchSorter } from 'match-sorter'; 16 | import React, { 17 | ChangeEvent, 18 | Children, 19 | cloneElement, 20 | ComponentType, 21 | createContext, 22 | FC, 23 | forwardRef, 24 | HTMLAttributes, 25 | useContext, 26 | } from 'react'; 27 | import { FixedSizeList, ListChildComponentProps } from 'react-window'; 28 | import { getIconUrlByName, getIconUrlForFilePath } from 'vscode-material-icons'; 29 | import { dirnameFromPath, filenameFromPath } from '../utils/format'; 30 | import { ICONS_URL } from '../utils/icons'; 31 | import { Module } from '../utils/types'; 32 | 33 | type Props = { 34 | modules: Module[]; 35 | label: string; 36 | value?: Module[]; 37 | onChange?: (modules: Module[]) => void; 38 | }; 39 | 40 | const LISTBOX_PADDING = 8; 41 | 42 | function renderRow(props: ListChildComponentProps) { 43 | const { data, index, style } = props; 44 | return cloneElement(data[index], { 45 | style: { 46 | ...style, 47 | top: Number(style.top) + LISTBOX_PADDING, 48 | }, 49 | }); 50 | } 51 | 52 | const OuterElementContext = createContext({}); 53 | 54 | const OuterElementType = forwardRef((props, ref) => { 55 | const outerProps = useContext(OuterElementContext); 56 | return
; 57 | }); 58 | 59 | // Adapter for react-window 60 | const ListboxComponent = forwardRef(function ListboxComponent( 61 | props, 62 | ref, 63 | ) { 64 | const { children, ...other } = props; 65 | const itemData = Children.toArray(children); 66 | const theme = useTheme(); 67 | const smUp = useMediaQuery(theme.breakpoints.up('sm'), { noSsr: true }); 68 | const itemCount = itemData.length; 69 | const itemSize = smUp ? 36 : 48; 70 | const height = Math.min(itemCount, 8) * itemSize + 2 * LISTBOX_PADDING; 71 | 72 | return ( 73 |
74 | 75 | 85 | {renderRow} 86 | 87 | 88 |
89 | ); 90 | }); 91 | 92 | function renderOption(option: Module) { 93 | const iconSrc = option.isLocal 94 | ? getIconUrlForFilePath(option.path, ICONS_URL) 95 | : getIconUrlByName('npm', ICONS_URL); 96 | const filename = filenameFromPath(option.path); 97 | const dirname = dirnameFromPath(option.path); 98 | const dirnameMaxWidth = filename.length > 35 ? 300 : 500; 99 | return ( 100 | 101 | 102 | 103 | 107 | 108 | 109 | {option.isLocal ? ( 110 | <> 111 | 112 | 113 | {filename} 114 | 115 | 116 | 117 | 122 | {dirname} 123 | 124 | 125 | 126 | ) : ( 127 | 128 | 129 | {option.path} 130 | 131 | 132 | )} 133 | 134 | ); 135 | } 136 | 137 | const useStyles = makeStyles({ 138 | listbox: { 139 | boxSizing: 'border-box', 140 | '& ul': { 141 | padding: 0, 142 | margin: 0, 143 | }, 144 | }, 145 | }); 146 | 147 | const SelectModules: FC = ({ modules, label, value, onChange }) => { 148 | const classes = useStyles(); 149 | const filterOptions = ( 150 | options: Module[], 151 | { inputValue }: FilterOptionsState, 152 | ) => 153 | matchSorter(options, inputValue, { 154 | keys: [({ path }) => path.replace(/\/_-\./g, ' ')], 155 | }); 156 | const getOptionLabel = ({ path }: Module) => path; 157 | const renderInput = (params: AutocompleteRenderInputParams) => ( 158 | 159 | ); 160 | const handleChange = (_: ChangeEvent<{}>, value: Module[]) => { 161 | onChange?.(value); 162 | }; 163 | const areModulesEqual = (option: Module, value: Module) => 164 | option.path === value.path; 165 | 166 | return ( 167 | > 174 | } 175 | disableListWrap 176 | getOptionLabel={getOptionLabel} 177 | renderInput={renderInput} 178 | renderOption={renderOption} 179 | filterOptions={filterOptions} 180 | onChange={handleChange} 181 | getOptionSelected={areModulesEqual} 182 | /> 183 | ); 184 | }; 185 | 186 | export default SelectModules; 187 | -------------------------------------------------------------------------------- /src/app/hooks/filters.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Filters } from '../utils/types'; 3 | 4 | export function useFilters(): [ 5 | filters: Filters, 6 | setFilters: (filters: Filters) => void, 7 | ] { 8 | const [searchFilters, setSearchFilters] = useState({ 9 | targetModules: [], 10 | sourceModules: [], 11 | }); 12 | 13 | useEffect(() => { 14 | const searchParams = new URLSearchParams(window.location.search); 15 | if ( 16 | searchParams.get('tgt') !== searchFilters.targetModules.join(',') || 17 | searchParams.get('src') !== searchFilters.sourceModules.join(',') 18 | ) { 19 | setSearchFilters({ 20 | targetModules: searchParams.get('tgt')?.split(',') ?? [], 21 | sourceModules: searchParams.get('src')?.split(',') ?? [], 22 | }); 23 | } 24 | }, []); 25 | 26 | const updateSearch = (filters: Filters) => { 27 | const searchParams = new URLSearchParams(); 28 | if (filters.targetModules.length > 0) { 29 | searchParams.append('tgt', filters.targetModules.join(',')); 30 | } 31 | if (filters.sourceModules.length > 0) { 32 | searchParams.append('src', filters.sourceModules.join(',')); 33 | } 34 | window.history.pushState(null, '', `?${searchParams.toString()}`); 35 | setSearchFilters(filters); 36 | }; 37 | 38 | return [searchFilters, updateSearch]; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Import Graph Visualizer 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/app/utils/deps.ts: -------------------------------------------------------------------------------- 1 | import { Module, ModuleDeps } from './types'; 2 | 3 | export function getModules(moduleDeps: ModuleDeps): Module[] { 4 | return moduleDeps.paths.map(path => moduleDeps.modules[path]); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function filenameFromPath(path: string): string { 2 | const files = path.split('/'); 3 | return files[files.length - 1]; 4 | } 5 | 6 | export function dirnameFromPath(path: string): string { 7 | return path.replace(/\/[^/]+$/, '/'); 8 | } 9 | -------------------------------------------------------------------------------- /src/app/utils/icons.ts: -------------------------------------------------------------------------------- 1 | export const ICONS_URL = 2 | process.env.NODE_ENV === 'production' 3 | ? 'assets/icons' 4 | : '../../../dist/app/assets/icons'; 5 | -------------------------------------------------------------------------------- /src/app/utils/parsers.ts: -------------------------------------------------------------------------------- 1 | import { ICruiseResult } from 'dependency-cruiser'; 2 | import { Module, ModuleDeps, ModuleImportMap } from './types'; 3 | 4 | export function parseModuleDeps(result: ICruiseResult): ModuleDeps { 5 | const localModules = new Set(); 6 | const aliases = new Map(); 7 | const npmPackageNames = new Map(); 8 | const sourceDeps = new Map< 9 | string, 10 | { source: string; isDynamic: boolean }[] 11 | >(); 12 | const sourceImportedBy = new Map< 13 | string, 14 | { source: string; isDynamic: boolean }[] 15 | >(); 16 | 17 | result.summary.optionsUsed.args?.split(/\s+/).forEach(entryPoint => { 18 | localModules.add(entryPoint); 19 | }); 20 | 21 | result.modules.forEach(module => { 22 | module.dependencies.forEach(dependency => { 23 | sourceDeps.set(module.source, [ 24 | ...(sourceDeps.get(module.source) ?? []), 25 | { source: dependency.resolved, isDynamic: dependency.dynamic }, 26 | ]); 27 | sourceImportedBy.set(dependency.resolved, [ 28 | ...(sourceImportedBy.get(dependency.resolved) ?? []), 29 | { source: module.source, isDynamic: dependency.dynamic }, 30 | ]); 31 | 32 | if (dependency.dependencyTypes.includes('local')) { 33 | localModules.add(dependency.resolved); 34 | } 35 | if (dependency.dependencyTypes.includes('aliased')) { 36 | aliases.set(dependency.resolved, dependency.module); 37 | localModules.add(dependency.resolved); 38 | } else if ( 39 | dependency.dependencyTypes.some(type => type.startsWith('npm')) 40 | ) { 41 | npmPackageNames.set(dependency.resolved, dependency.module); 42 | } 43 | }); 44 | }); 45 | 46 | const allModules = result.modules 47 | .map( 48 | (module): Module => { 49 | const npmPackageName = npmPackageNames.get(module.source); 50 | const alias = aliases.get(module.source); 51 | const isLocal = localModules.has(module.source); 52 | return { 53 | path: npmPackageName ?? module.source, 54 | source: module.source, 55 | isLocal, 56 | ...(alias && { alias }), 57 | }; 58 | }, 59 | ) 60 | .filter( 61 | (item, index, array) => 62 | array.findIndex(({ path }) => path === item.path) === index, 63 | ) 64 | .sort((a, b) => a.path.localeCompare(b.path)); 65 | 66 | const { moduleBySource, moduleByPath } = allModules.reduce<{ 67 | moduleBySource: Record; 68 | moduleByPath: Record; 69 | }>( 70 | (acc, module) => ({ 71 | moduleBySource: { ...acc.moduleBySource, [module.source]: module }, 72 | moduleByPath: { ...acc.moduleByPath, [module.path]: module }, 73 | }), 74 | { moduleBySource: {}, moduleByPath: {} }, 75 | ); 76 | 77 | const pathDeps: ModuleImportMap = {}; 78 | sourceDeps.forEach((value, key) => { 79 | pathDeps[moduleBySource[key].path] = value.map(({ source, isDynamic }) => ({ 80 | path: moduleBySource[source].path, 81 | isDynamic, 82 | })); 83 | }); 84 | const pathImportedBy: ModuleImportMap = {}; 85 | sourceImportedBy.forEach((value, key) => { 86 | pathImportedBy[moduleBySource[key].path] = value.map( 87 | ({ source, isDynamic }) => ({ 88 | path: moduleBySource[source].path, 89 | isDynamic, 90 | }), 91 | ); 92 | }); 93 | 94 | return { 95 | modules: moduleByPath, 96 | paths: allModules.map(({ path }) => path), 97 | deps: pathDeps, 98 | importedBy: pathImportedBy, 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /src/app/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type Module = { 2 | path: string; 3 | source: string; 4 | alias?: string; 5 | isLocal: boolean; 6 | }; 7 | 8 | export type ModuleDeps = { 9 | modules: Record; 10 | paths: string[]; 11 | deps: ModuleImportMap; 12 | importedBy: ModuleImportMap; 13 | }; 14 | 15 | export type ModuleImportMap = Record< 16 | string, 17 | { path: string; isDynamic: boolean }[] 18 | >; 19 | 20 | export type DepGraph = { 21 | modules: Module[]; 22 | imports: { 23 | fromPath: string; 24 | toPath: string; 25 | isDynamic: boolean; 26 | }[]; 27 | }; 28 | 29 | export type Filters = { 30 | targetModules: string[]; 31 | sourceModules: string[]; 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/workers/graph.worker.ts: -------------------------------------------------------------------------------- 1 | import { DepGraph, ModuleDeps } from '../utils/types'; 2 | 3 | const ctx: Worker = self as any; 4 | 5 | type Args = { 6 | moduleDeps: ModuleDeps; 7 | sourceModules: string[]; 8 | targetModules: string[]; 9 | }; 10 | 11 | ctx.addEventListener('message', (ev: MessageEvent) => { 12 | ctx.postMessage(createDepGraph(ev.data)); 13 | }); 14 | 15 | function createDepGraph(args: Args): DepGraph { 16 | const { moduleDeps, sourceModules, targetModules } = args; 17 | 18 | if (sourceModules.length === 0) { 19 | const { vertices, edges } = bfs( 20 | targetModules, 21 | module => moduleDeps.importedBy[module]?.map(({ path }) => path) ?? [], 22 | ); 23 | return { 24 | modules: vertices.map(module => moduleDeps.modules[module]), 25 | imports: edges.map(({ from, to }) => ({ 26 | fromPath: from, 27 | toPath: to, 28 | isDynamic: 29 | moduleDeps.importedBy[from].find(({ path }) => path === to) 30 | ?.isDynamic ?? false, 31 | })), 32 | }; 33 | } 34 | 35 | if (targetModules.length === 0) { 36 | const { vertices, edges } = bfs( 37 | sourceModules, 38 | module => moduleDeps.deps[module]?.map(({ path }) => path) ?? [], 39 | ); 40 | return { 41 | modules: vertices.map(module => moduleDeps.modules[module]), 42 | imports: edges.map(({ from, to }) => ({ 43 | fromPath: from, 44 | toPath: to, 45 | isDynamic: 46 | moduleDeps.deps[from].find(({ path }) => path === to)?.isDynamic ?? 47 | false, 48 | })), 49 | }; 50 | } 51 | 52 | const paths = findAllPaths( 53 | targetModules, 54 | sourceModules, 55 | module => moduleDeps.importedBy[module]?.map(({ path }) => path) ?? [], 56 | ); 57 | 58 | const relevantModules = new Set(); 59 | paths.forEach(path => { 60 | path.forEach(module => { 61 | relevantModules.add(module); 62 | }); 63 | }); 64 | 65 | return { 66 | modules: Array.from(relevantModules).map( 67 | module => moduleDeps.modules[module], 68 | ), 69 | imports: Array.from(relevantModules).reduce( 70 | (acc, module) => [ 71 | ...acc, 72 | ...(moduleDeps.importedBy[module] 73 | ?.filter(({ path }) => relevantModules.has(path)) 74 | .map(({ path, isDynamic }) => ({ 75 | fromPath: path, 76 | toPath: module, 77 | isDynamic, 78 | })) ?? []), 79 | ], 80 | [], 81 | ), 82 | }; 83 | } 84 | 85 | function bfs( 86 | from: T[], 87 | adjacent: (vertex: T) => T[], 88 | ): { 89 | vertices: T[]; 90 | edges: { from: T; to: T }[]; 91 | } { 92 | const queue = from; 93 | const discovered = new Set(from); 94 | const vertices: T[] = []; 95 | const edges: { from: T; to: T }[] = []; 96 | while (queue.length > 0) { 97 | const vertex = queue.shift()!; 98 | vertices.push(vertex); 99 | for (const other of adjacent(vertex)) { 100 | edges.push({ from: vertex, to: other }); 101 | if (!discovered.has(other)) { 102 | queue.push(other); 103 | discovered.add(other); 104 | } 105 | } 106 | } 107 | return { vertices, edges }; 108 | } 109 | 110 | function findAllPaths( 111 | from: T[], 112 | to: T[], 113 | adjacent: (vertex: T) => T[], 114 | ): T[][] { 115 | const ends = new Set(to); 116 | const isPathEnd = (vertex: T) => ends.has(vertex); 117 | 118 | const paths: T[][] = []; 119 | for (const vertex of from) { 120 | const visited = new Set(); 121 | const path: T[] = []; 122 | findAllPathsUtil(vertex, isPathEnd, adjacent, visited, path, paths); 123 | } 124 | 125 | return paths; 126 | } 127 | 128 | function findAllPathsUtil( 129 | vertex: T, 130 | isPathEnd: (vertex: T) => boolean, 131 | adjacent: (vertex: T) => T[], 132 | visited: Set, 133 | path: T[], 134 | paths: T[][], 135 | ): void { 136 | visited.add(vertex); 137 | path.push(vertex); 138 | 139 | if (isPathEnd(vertex)) { 140 | paths.push([...path]); 141 | } else { 142 | for (const other of adjacent(vertex)) { 143 | if (!visited.has(other)) { 144 | findAllPathsUtil(other, isPathEnd, adjacent, visited, path, paths); 145 | } 146 | } 147 | } 148 | 149 | visited.delete(vertex); 150 | path.pop(); 151 | } 152 | -------------------------------------------------------------------------------- /src/app/workers/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'worker-loader!*' { 2 | class WebpackWorker extends Worker { 3 | constructor(); 4 | } 5 | 6 | export default WebpackWorker; 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { createReporterOutput } from './reporter'; 4 | import { runStaticServer } from './server'; 5 | 6 | createReporterOutput(); 7 | 8 | runStaticServer(); 9 | -------------------------------------------------------------------------------- /src/cli/reporter.ts: -------------------------------------------------------------------------------- 1 | import { cruise, ICruiseOptions } from 'dependency-cruiser'; 2 | import fs from 'fs'; 3 | import ora from 'ora'; 4 | import path from 'path'; 5 | import typescript from 'typescript'; 6 | import yargs from 'yargs'; 7 | 8 | export function createReporterOutput(): void { 9 | const args = yargs 10 | .option('entry-points', { 11 | alias: 'e', 12 | demandOption: true, 13 | array: true, 14 | string: true, 15 | }) 16 | .option('ts-config', { 17 | alias: 't', 18 | demandOption: false, 19 | string: true, 20 | }).argv; 21 | 22 | const entryPoints = args['entry-points']; 23 | const tsConfigFileName = args['ts-config']; 24 | 25 | const tsConfig = 26 | tsConfigFileName == null 27 | ? null 28 | : typescript.parseJsonConfigFileContent( 29 | typescript.readConfigFile(tsConfigFileName, typescript.sys.readFile) 30 | .config, 31 | typescript.sys, 32 | path.dirname(tsConfigFileName), 33 | {}, 34 | tsConfigFileName, 35 | ); 36 | 37 | const options: ICruiseOptions = { 38 | doNotFollow: { 39 | path: 'node_modules', 40 | dependencyTypes: [ 41 | 'npm', 42 | 'npm-dev', 43 | 'npm-optional', 44 | 'npm-peer', 45 | 'npm-bundled', 46 | 'npm-no-pkg', 47 | ], 48 | }, 49 | tsPreCompilationDeps: true, 50 | }; 51 | 52 | const cruiseSpinner = ora('Analyzing project imports').start(); 53 | const { output } = cruise( 54 | entryPoints, 55 | { 56 | ruleSet: { 57 | options: { ...options, tsConfig: { fileName: tsConfigFileName } }, 58 | }, 59 | } as any, 60 | null, 61 | tsConfig, 62 | ); 63 | cruiseSpinner.succeed('Analyzed project imports'); 64 | 65 | const fsSpinner = ora('Creating dependency graph').start(); 66 | fs.writeFileSync( 67 | path.resolve(__dirname, 'reporter-output.json'), 68 | JSON.stringify(output), 69 | ); 70 | fsSpinner.succeed('Created dependency graph'); 71 | } 72 | 73 | if (require.main === module) { 74 | createReporterOutput(); 75 | } 76 | -------------------------------------------------------------------------------- /src/cli/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import open from 'open'; 3 | import ora from 'ora'; 4 | import path from 'path'; 5 | 6 | const PORT = 4000; 7 | const url = `http://localhost:${PORT}`; 8 | 9 | export function runStaticServer(): void { 10 | const spinner = ora(`Opening browser at ${url}`).start(); 11 | 12 | const app = express(); 13 | 14 | app.use(express.static(path.join(__dirname, '..', 'app'))); 15 | app.use('/assets', express.static(path.join(__dirname, '..', 'cli'))); 16 | 17 | app.listen(PORT, () => { 18 | open(url).then(() => { 19 | spinner.succeed(`Opened browser at ${url}`); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "jsx": "react", 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/app/index.tsx', 7 | output: { 8 | filename: 'main.js', 9 | path: path.resolve(__dirname, 'dist/app'), 10 | clean: true, 11 | }, 12 | devtool: 'inline-source-map', 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/, 19 | }, 20 | { 21 | test: /\.worker\.js/, 22 | use: { loader: 'worker-loader' }, 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['.tsx', '.ts', '.js'], 28 | }, 29 | plugins: [ 30 | new HtmlWebpackPlugin({ 31 | template: __dirname + '/src/app/index.html', 32 | filename: 'index.html', 33 | inject: 'body', 34 | }), 35 | new CopyPlugin({ 36 | patterns: [ 37 | { 38 | from: 'node_modules/vscode-material-icons/generated/icons', 39 | to: 'assets/icons', 40 | }, 41 | ], 42 | }), 43 | ], 44 | }; 45 | --------------------------------------------------------------------------------