├── .gitignore ├── LICENSE ├── README.md ├── examples └── sound-redux │ └── stats.json ├── index.html ├── jsconfig.json ├── package.json ├── src ├── client.tsx ├── components │ ├── app │ │ ├── app-style.css │ │ └── app.tsx │ ├── button │ │ ├── button-style.css │ │ └── index.tsx │ ├── file-upload │ │ ├── file-upload-style.css │ │ └── index.tsx │ ├── form │ │ └── checkbox.tsx │ ├── graph │ │ ├── graph-style.css │ │ └── index.tsx │ ├── input │ │ ├── index.tsx │ │ └── input-style.css │ ├── overview-control │ │ ├── button-style.css │ │ ├── button.tsx │ │ ├── select-style.css │ │ └── select.tsx │ ├── reference-table │ │ ├── index.tsx │ │ ├── reference-table-style.css │ │ ├── table.tsx │ │ └── text-cell.tsx │ └── table │ │ ├── index.tsx │ │ ├── sort-types.ts │ │ ├── sortable-header-cell.tsx │ │ ├── table-style.css │ │ ├── table.tsx │ │ └── text-cell.tsx ├── graph │ ├── elements │ │ ├── hull.ts │ │ ├── link.ts │ │ ├── node.ts │ │ └── text.ts │ ├── graph-style.css │ └── index.ts ├── init-style.css ├── init.tsx ├── styles │ ├── common.css │ └── reset.css ├── types │ └── index.ts └── utils │ ├── filter.ts │ ├── graph │ └── control.ts │ ├── network.ts │ ├── process-data.ts │ ├── search.ts │ └── traverse.ts ├── tools └── publish.js ├── tsconfig.json ├── tslint.json ├── typings.json ├── typings ├── custom.d.ts ├── globals │ └── node │ │ ├── index.d.ts │ │ └── typings.json ├── index.d.ts └── modules │ ├── d3 │ ├── index.d.ts │ └── typings.json │ ├── lodash │ ├── index.d.ts │ └── typings.json │ ├── react-dom │ ├── index.d.ts │ └── typings.json │ └── react │ ├── index.d.ts │ └── typings.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mikhail Shustov 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 | ### Motivation 2 | The purpose of the lib to provide an easier way for understanding dependency graph in our apps. 3 | Usually, we want to see next dependencies: 4 | 1. Why that specific module or file was included in the bundle. 5 | 2. Which dependencies in the bundle are included due to the specific module. 6 | 3. Which modules use that specific module. 7 | 8 | ### Usage 9 | Tool uses `stat.json` file, provided by `webpack`, to build dependency graph. 10 | So to get a visual representation of dependency graph: 11 | 1. Run webpack build with saving stats file on disk. ie, `webpack --json > stats.json`. 12 | 2. Go to [online version](https://restrry.github.io/webpack-deps-tree/static/). 13 | 3. Upload generated `stats.json` file. 14 | 15 | Next version will have the option to be used as webpack plugin as well. 16 | 17 | ### Usage cases 18 | Let's overview all 3 main cases: 19 | #### Module Overview 20 | In that mode, we can see all third party modules that are used in our code base and 21 | connection between them. In this way we can spot hidden dependencies or find duplicated modules. 22 | 23 | ![overview common](http://i.imgur.com/x0h1o6Z.png) 24 | ##### Example: 25 | Here we can see that stacktrace.js is used only in one place in our code and included some 26 | heavy third party modules. So we can conclude that it's a good candidate for [async loading](https://webpack.js.org/guides/code-splitting-async/) 27 | 28 | ![overview the stacktrace.js connections](http://i.imgur.com/v4m14c1.png) 29 | 30 | #### Module usage 31 | If we want to see all modules that require specific module, we can select appropriate module 32 | in select element that places at top of that page 33 | 34 | ![select](http://i.imgur.com/Jozdufk.png) 35 | 36 | ##### Example: 37 | We want to check how much it costs to switch to [preact library](https://github.com/developit/preact-compat). Selecting `react-dom` we can see next picture 38 | 39 | ![rect-dom occurrence](http://i.imgur.com/9hRJtP4.png) 40 | Well, seems that we need to look more carefully at those libraries that use React-DOM API under the hood 41 | and check how they are compatible with `preact` 42 | 43 | #### File usage 44 | Another case is when we need to understand the whole picture why the file was included in the bundle. 45 | The specific files are listed in the table on the left side of the page 46 | 47 | ![file table](http://i.imgur.com/w6REQi6.png) 48 | To see the whole inclusion tree (up to webpack entry point) you should select specific file with a click on its name. 49 | Than tree will be rendered: 50 | 1. inclusion tree 51 | 52 | ![inclusion tree](http://i.imgur.com/gyl7ewr.png) 53 | 2. reference table with listing all places where that specific file was required and listing all modules that are required by our specific module 54 | 55 | [reference table](http://i.imgur.com/Wwn2ICf.png) 56 | ##### Example 57 | We want to split our domain logic by pages, before we need to understand on which pages those files 58 | are utilized. Having selected desired module in the table at the right, we are able to see the whole 59 | inclusion tree. 60 | 61 | ![inclusion tree](http://i.imgur.com/2ywZpWw.png) 62 | To reduce visual noise use filtering by name(input element at top left corner of the page) 63 | 64 | ![filtering by name](http://i.imgur.com/70mifYn.png) 65 | 66 | 67 | well, now we figured out that desired domain logic file is utilized only on one page and we can start thinking 68 | about code-splitting and inclusion that domain logic file into appropriate chunk 69 | 70 | ![menu page](http://i.imgur.com/zz9i1lJ.png) 71 | 72 | 73 | ### Additional notes 74 | The lib is still under development and was created due to personal needs. If you have any ideas what should be improved or fixed don't hesitate to create an issue. 75 | 76 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Webpack deps tree 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-deps-tree", 3 | "version": "0.0.1", 4 | "author": "Mikhail Shustov ", 5 | "license": "MIT", 6 | "dependencies": { 7 | "babel-runtime": "^6.3.19", 8 | "d3": "^3.5.17", 9 | "fixed-data-table-2": "^0.7.17", 10 | "history": "3.2.0", 11 | "loader-utils": "^1.1.0", 12 | "lodash": "^4.17.4", 13 | "react": "15.4.1", 14 | "react-dimensions": "^1.3.0", 15 | "react-dom": "15.4.1", 16 | "react-github-corner": "^0.3.0", 17 | "react-table": "^5.6.0", 18 | "source-map": "^0.5.6" 19 | }, 20 | "devDependencies": { 21 | "awesome-typescript-loader": "^3.1.3", 22 | "babel-core": "^6.4.5", 23 | "babel-eslint": "^7.0.0", 24 | "babel-loader": "6.2.7", 25 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 26 | "babel-preset-env": "^1.4.0", 27 | "babel-preset-es2015": "^6.3.13", 28 | "babel-preset-react": "^6.3.13", 29 | "babel-preset-react-hmre": "^1.1.0", 30 | "css-loader": "^0.25.0", 31 | "gh-pages": "^1.0.0", 32 | "html-webpack-inline-source-plugin": "0.0.9", 33 | "html-webpack-plugin": "^2.22.0", 34 | "source-map-loader": "^0.2.1", 35 | "style-loader": "^0.13.1", 36 | "tslint": "^5.4.2", 37 | "tslint-react": "^3.0.0", 38 | "typescript": "^2.3.2", 39 | "typings": "^2.1.1", 40 | "webpack": "2.4.1", 41 | "webpack-dev-server": "^1.14.1", 42 | "webpack-hot-middleware": "^2.6.4" 43 | }, 44 | "scripts": { 45 | "build": "export NODE_ENV=production; webpack", 46 | "dev": "webpack-dev-server --inline --hot --open --progress --colors --content-base .", 47 | "prepublish": "npm run build", 48 | "publish": "node ./tools/publish.js", 49 | "install:typings": "typings install", 50 | "test": "mocha --opts test/mocha.opts", 51 | "lint": "tslint src/" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/client.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import createHistory from 'history/lib/createBrowserHistory'; 4 | import * as url from 'url'; 5 | import processData from './utils/process-data'; 6 | 7 | import App from './components/app/app'; 8 | 9 | import { filterData } from './utils/filter'; 10 | 11 | const history = createHistory(); 12 | 13 | interface Store { 14 | module: string; 15 | group: string; 16 | } 17 | 18 | const defStore: Store = { 19 | module: '', 20 | group: '' 21 | }; 22 | 23 | function onSelect(name: string, value: string): void { 24 | const current = new URL(window.location.href); 25 | const store = Object.assign({}, defStore, { [name]: value }); 26 | Object.entries(store).forEach(([key, val]) => current.searchParams.set(key, val)); 27 | 28 | history.push({ 29 | pathname: current.pathname, 30 | search: current.search 31 | }); 32 | } 33 | 34 | function onReset(): void { 35 | history.push({ pathname: window.location.pathname }); 36 | } 37 | 38 | interface FilterModulesProps { 39 | activeModuleId: ModuleId; 40 | activeGroupId: string; 41 | isModuleOverview: boolean; 42 | modules: Module[]; 43 | getModuleById: GetModuleById; 44 | } 45 | 46 | function run (rawData) { 47 | const { groups, edges, modules, getModuleById } = processData(rawData); 48 | 49 | function render(store: Store) { 50 | const activeModuleId = store.module; 51 | const module = activeModuleId ? getModuleById(activeModuleId) : null; 52 | const activeGroupId = store.group; 53 | const isModuleOverview = !activeModuleId && !activeGroupId; 54 | 55 | const filteredData = filterData(modules, edges, { 56 | modules, 57 | activeModuleId, 58 | activeGroupId, 59 | isModuleOverview, 60 | getModuleById 61 | }); 62 | 63 | ReactDOM.render( 64 | , 78 | document.getElementById('root') 79 | ); 80 | } 81 | 82 | function locationChangeHandler() { 83 | var { query } = url.parse(window.location.search, true); 84 | render(query); 85 | } 86 | 87 | history.listen(locationChangeHandler); 88 | 89 | render(defStore); 90 | } 91 | 92 | export default run; 93 | -------------------------------------------------------------------------------- /src/components/app/app-style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | 4 | display: flex; 5 | flex-direction: row; 6 | } 7 | 8 | .container__control-list { 9 | height: 100%; 10 | width: 500px; 11 | 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .container__control-item { 17 | flex-grow: 1; 18 | } 19 | 20 | .container__group-control { 21 | height: 24px; 22 | line-height: 24px; 23 | padding: 1px 0; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/app/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Table from '../table/'; 4 | import ReferenceTable from '../reference-table/'; 5 | 6 | import OverviewSelect from '../overview-control/select'; 7 | import OverviewButton from '../overview-control/button'; 8 | import Graph from '../graph/'; 9 | 10 | import './app-style.css'; 11 | 12 | interface AppProps { 13 | nodes: ModuleNode[]; 14 | edges: ModuleEdge[]; 15 | 16 | modules: Module[]; 17 | groups: {[key: string]: ModuleGroup}; 18 | activeModule: Module | null; 19 | activeModuleId: ModuleId | null; 20 | activeGroupId: string; 21 | isModuleOverview: boolean; 22 | 23 | onSelect(name: string, value: string): void; 24 | onReset(): void; 25 | } 26 | 27 | const App = (props: AppProps) => ( 28 |
29 |
30 | props.onSelect('module', v)} 38 | onGroupSelect={(v) => props.onSelect('group', v)} 39 | > 40 | props.onSelect('group', v)} 44 | /> 45 | 49 | 50 |
51 |
52 |
53 | props.onSelect('module', v)} 56 | /> 57 | props.onSelect('module', v)} 60 | /> 61 | 62 | 63 | 64 | ); 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /src/components/button/button-style.css: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | height: 100%; 4 | 5 | border: none; 6 | background: none; 7 | outline: none; 8 | appearance: none; 9 | user-select: none; 10 | } 11 | 12 | .button:hover:enabled { 13 | cursor: pointer; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './button-style.css'; 3 | 4 | interface ButtonProps { 5 | onClick: () => void; 6 | disabled?: boolean; 7 | // workaround from https://github.com/Microsoft/TypeScript/issues/13618 8 | children?: React.ReactNode; 9 | } 10 | 11 | const Button: React.SFC = props => { 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | export default Button; 24 | -------------------------------------------------------------------------------- /src/components/file-upload/file-upload-style.css: -------------------------------------------------------------------------------- 1 | .file-upload { 2 | position: relative; 3 | padding: 10px 20px; 4 | 5 | border: 1px solid #eee; 6 | border-radius: 2px; 7 | 8 | cursor: pointer; 9 | } 10 | 11 | .file-upload__element { 12 | position: absolute; 13 | top: 0; 14 | bottom: 0; 15 | left: 0; 16 | right: 0; 17 | margin: 0; 18 | padding: 0; 19 | font-size: 20px; 20 | cursor: pointer; 21 | opacity: 0; 22 | filter: alpha(opacity=0); 23 | pointer-events: none; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/file-upload/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './file-upload-style.css'; 3 | 4 | interface FileUploadProps { 5 | onChange: (e: React.FormEvent) => void; 6 | // workaround from https://github.com/Microsoft/TypeScript/issues/13618 7 | children?: React.ReactNode; 8 | } 9 | 10 | const FileUpload: React.SFC = props => { 11 | return ( 12 | 21 | ); 22 | }; 23 | 24 | export default FileUpload; 25 | -------------------------------------------------------------------------------- /src/components/form/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface CheckboxProps { 4 | checked: boolean; 5 | disabled?: boolean; 6 | name: string; 7 | onChange: (e: React.FormEvent) => void; 8 | children?: React.ReactNode; 9 | } 10 | 11 | const Checkbox: React.SFC = props => ( 12 | 23 | ); 24 | 25 | export default Checkbox; 26 | -------------------------------------------------------------------------------- /src/components/graph/graph-style.css: -------------------------------------------------------------------------------- 1 | .graph { 2 | position: relative; 3 | } 4 | 5 | .graph__settings{ 6 | position: absolute; 7 | 8 | left: 20px; 9 | top: 20px; 10 | } 11 | 12 | .graph__filter { 13 | display: inline-block; 14 | width: 200px; 15 | } 16 | 17 | .graph__controls { 18 | display: inline-block; 19 | margin-left: 20px; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/graph/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Input from '../input'; 3 | // ok ,here we want to render 2 differnet graphs, get back to problem later 4 | import { bootstrap } from '../../graph/'; 5 | import './graph-style.css'; 6 | 7 | interface GraphProps { 8 | nodes: ModuleNode[]; 9 | edges: ModuleEdge[]; 10 | 11 | activeModuleId: ModuleId; 12 | isModuleOverview: boolean; 13 | 14 | onModuleSelect: (name: string) => void; 15 | onGroupSelect: (name: string) => void; 16 | } 17 | 18 | interface GraphState { 19 | filter: string; 20 | } 21 | 22 | export default class Graph extends React.Component { 23 | private graphElem: HTMLElement; 24 | private updateData: (props: GraphProps) => void; 25 | private updateFilter: (str: string, isModuleOverview: boolean) => void; 26 | 27 | constructor(props: GraphProps) { 28 | super(props); 29 | this.state = { 30 | filter: '' 31 | }; 32 | 33 | this.onFilterChange = this.onFilterChange.bind(this); 34 | } 35 | 36 | componentDidMount() { 37 | const { updateData, updateFilter } = bootstrap(this.graphElem); 38 | this.updateData = updateData; 39 | this.updateFilter = updateFilter; 40 | this.updateData(this.props); 41 | } 42 | 43 | componentWillReceiveProps(nextProps: GraphProps) { 44 | this.updateData(nextProps); 45 | this.setState(s => ({ 46 | filter: '' 47 | })); 48 | } 49 | 50 | onFilterChange(filterValue: string) { 51 | this.setState(s => ({ 52 | filter: filterValue 53 | })); 54 | 55 | this.updateFilter(filterValue, this.props.isModuleOverview); 56 | } 57 | 58 | render() { 59 | 60 | return ( 61 |
this.graphElem = r} > 62 |
63 |
64 | 69 |
70 |
71 | {this.props.children} 72 |
73 |
74 |
75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/input/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './input-style.css'; 3 | 4 | interface InputProps { 5 | placeholder?: string; 6 | value?: string; 7 | onChange: (value: string) => void; 8 | children?: React.ReactNode; 9 | } 10 | 11 | const getValue = (e: React.FormEvent): string => { 12 | const target = e.target as HTMLInputElement; 13 | return target.value.trim(); 14 | }; 15 | 16 | const Input: React.SFC = props => { 17 | return ( 18 |
19 | props.onChange(getValue(e))} 24 | value={props.value} 25 | /> 26 | {props.children} 27 |
28 | ); 29 | }; 30 | 31 | export default Input; 32 | -------------------------------------------------------------------------------- /src/components/input/input-style.css: -------------------------------------------------------------------------------- 1 | .input__wrapper { 2 | display: inline-block; 3 | width: 100%; 4 | padding: 4px 2px; 5 | border: 1px solid #eee; 6 | border-radius: 2px; 7 | background-color: #fff; 8 | } 9 | 10 | .input__wrapper:focus-within { 11 | border-color: #afafaf; 12 | } 13 | 14 | .input__element { 15 | width: 100%; 16 | 17 | font-size: 12px; 18 | line-height: 12px; 19 | 20 | border: none; 21 | outline: none; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/overview-control/button-style.css: -------------------------------------------------------------------------------- 1 | .module-overview-button { 2 | display: inline-block; 3 | height: 28px; 4 | border: 1px solid #eee; 5 | background-color: #fff; 6 | 7 | border-top-right-radius: 2px; 8 | border-bottom-right-radius: 2px; 9 | } 10 | 11 | .module-overview-button:hover:not(.module-overview-button--disabled) { 12 | border-color: #afafaf; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/overview-control/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | import Button from '../button/'; 5 | import './button-style.css'; 6 | 7 | interface OverviewButtonProps { 8 | disabled: boolean; 9 | onClick: () => void; 10 | } 11 | 12 | const OverviewButton: React.SFC = props => { 13 | const buttonClassname = classnames('module-overview-button', { 14 | 'module-overview-button--disabled': props.disabled 15 | }); 16 | 17 | return ( 18 |
19 | 25 |
26 | ); 27 | }; 28 | 29 | export default OverviewButton; 30 | -------------------------------------------------------------------------------- /src/components/overview-control/select-style.css: -------------------------------------------------------------------------------- 1 | .overview-select { 2 | display: inline-block; 3 | border: 1px solid #eee; 4 | border-right: none; 5 | border-top-left-radius: 2px; 6 | border-bottom-left-radius: 2px; 7 | background-color: #fff; 8 | } 9 | 10 | .overview-select__element { 11 | display: inline-block; 12 | max-width: 200px; 13 | height: 26px; 14 | border: none; 15 | background: none; 16 | outline: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/overview-control/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './select-style.css'; 3 | 4 | interface OverviewSelectProps { 5 | active: string; 6 | groups: {[key: string]: ModuleGroup}; 7 | onChange: (name: string) => void; 8 | } 9 | 10 | const OverviewSelect: React.SFC = props => ( 11 |
12 | 26 |
27 | ); 28 | 29 | // const defaultProps: Partial = { 30 | // active: '' 31 | // }; 32 | // OverviewSelect.defaultProps = defaultProps; 33 | 34 | export default OverviewSelect; 35 | -------------------------------------------------------------------------------- /src/components/reference-table/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import ReferenceTable from './table'; 4 | import './reference-table-style.css'; 5 | 6 | interface ReferenceTableWrapperProps { 7 | module: Module | null; 8 | onSelect: (moduleId: ModuleId) => void; 9 | } 10 | 11 | const ReferenceTableWrapper: React.SFC = props => { 12 | if (!props.module) { 13 | return ( 14 |
15 | 16 | No module was selected 17 | 18 |
19 | ); 20 | } 21 | 22 | const rowsCount = Math.max(props.module.reasons.length, props.module.dependencies.length); 23 | 24 | return ( 25 |
26 |
27 | Active module: {props.module.name} 28 |
29 | 33 |
34 | ); 35 | }; 36 | 37 | export default ReferenceTableWrapper; 38 | -------------------------------------------------------------------------------- /src/components/reference-table/reference-table-style.css: -------------------------------------------------------------------------------- 1 | .reference-table { 2 | display: flex; 3 | height: 100%; 4 | flex-direction: column; 5 | } 6 | 7 | .reference-table__title { 8 | display: block; 9 | height: 24px; 10 | line-height: 18px; 11 | padding: 2px 0; 12 | } 13 | 14 | .reference-table__element { 15 | flex-grow: 1; 16 | font-size: 11px; 17 | } 18 | 19 | .reference-table--empty .reference-table__title { 20 | text-align: center; 21 | } 22 | 23 | .reference-table__cell--clickable { 24 | cursor: pointer; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/reference-table/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Dimensions from 'react-dimensions'; 3 | import { Table, Column, Cell } from 'fixed-data-table-2'; 4 | 5 | import TextCell from './text-cell'; 6 | import './reference-table-style.css'; 7 | 8 | interface ReferenceTableProps { 9 | rowsCount: number; 10 | containerWidth: number; // injected by react-dimension 11 | containerHeight: number; // injected by react-dimension 12 | module: Module | null; 13 | onSelect: (moduleId: ModuleId) => void; 14 | } 15 | 16 | // we use class since react-dimension uses refs 17 | class ReferenceTable extends React.Component { 18 | render () { 19 | return ( 20 |
28 | Required from} 31 | cell={ 32 | 37 | } 38 | fixed={true} 39 | flexGrow={1} 40 | width={200} 41 | /> 42 | Requires} 45 | cell={ 46 | 51 | } 52 | fixed={true} 53 | flexGrow={1} 54 | width={200} 55 | /> 56 |
57 | ); 58 | } 59 | } 60 | 61 | export default Dimensions({ 62 | getHeight: function(elem: HTMLElement) { 63 | return elem.offsetHeight; 64 | }, 65 | getWidth: function(elem: HTMLElement) { 66 | return elem.offsetWidth; 67 | } 68 | })(ReferenceTable); 69 | -------------------------------------------------------------------------------- /src/components/reference-table/text-cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classnames from 'classnames'; 3 | import { Cell } from 'fixed-data-table-2'; 4 | import './reference-table-style.css'; 5 | 6 | export interface TextCellProps { 7 | rowIndex?: number; // will be injected by 'fixed-data-table' 8 | data: Module; 9 | col: keyof Module; 10 | onClick?: (id: ModuleId) => void; 11 | } 12 | 13 | const TextCell: React.SFC = props => { 14 | const { rowIndex, data, col, onClick, ...cleanProps } = props; 15 | const value = data[col][rowIndex]; 16 | 17 | const cellClassnames = classnames('reference-table__cell', { 18 | 'reference-table__cell--clickable': Boolean(onClick) 19 | }); 20 | 21 | return ( 22 | value ? 23 | onClick(value.moduleUid) : undefined} 27 | > 28 | {value.moduleName} 29 | : 30 | null 31 | ); 32 | }; 33 | 34 | export default TextCell; 35 | -------------------------------------------------------------------------------- /src/components/table/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import orderBy from 'lodash/orderBy'; 3 | 4 | import { SortDir, SortTypes, SortKey, SortingOrder } from './sort-types'; 5 | 6 | import Input from '../input/'; 7 | import ModuleTable from './table'; 8 | 9 | import './table-style.css'; 10 | 11 | interface ModulesTableProps { 12 | modules: Module[]; 13 | onSelect: (moduleId: ModuleId) => void; 14 | } 15 | 16 | interface ModulesTableState { 17 | filteredDataList: Module[]; 18 | colSortDirs: SortingOrder; 19 | } 20 | 21 | const defaultSort: SortKey = 'name'; 22 | const defaultSortDirection: SortDir = SortTypes.ASC; 23 | 24 | class ModuleTableWrapper extends React.Component { 25 | _dataList: Module[]; 26 | 27 | static sortDataList(modules: Module[], columnKey: SortKey, sortDir: SortDir): Module[] { 28 | return orderBy(modules, columnKey, sortDir); 29 | } 30 | 31 | constructor(props: ModulesTableProps) { 32 | super(props); 33 | 34 | this._dataList = props.modules; 35 | this.state = { 36 | filteredDataList: this._dataList, 37 | colSortDirs: { 38 | key: defaultSort, 39 | direction: defaultSortDirection 40 | } 41 | }; 42 | 43 | this._onFilterChange = this._onFilterChange.bind(this); 44 | this._onSortChange = this._onSortChange.bind(this); 45 | } 46 | 47 | _onFilterChange(value: string) { 48 | const filterBy = value.toLowerCase(); 49 | if (!filterBy) { 50 | this.setState((prevState, props) => { 51 | return { ...prevState, filteredDataList: this._dataList }; 52 | }); 53 | } 54 | 55 | const filterReg = new RegExp(filterBy, 'i'); 56 | const filteredDataList = this._dataList.filter(m => filterReg.test(m.name)); 57 | 58 | this.setState((prevState, props) => { 59 | return { ...prevState, filteredDataList }; 60 | }); 61 | } 62 | 63 | _onSortChange(sortKey: SortKey, sortDir: SortDir): void { 64 | this.setState((prevState, props) => { 65 | return { 66 | ...prevState, 67 | sortedDataList: ModuleTableWrapper.sortDataList( 68 | this.state.filteredDataList, 69 | sortKey, 70 | sortDir 71 | ), 72 | colSortDirs: { 73 | key: sortKey, 74 | direction: sortDir 75 | } 76 | }; 77 | }); 78 | } 79 | 80 | render() { 81 | const { filteredDataList, colSortDirs } = this.state; 82 | const modules = ModuleTableWrapper.sortDataList( 83 | this.state.filteredDataList, 84 | colSortDirs.key, 85 | colSortDirs.direction 86 | ); 87 | 88 | return ( 89 |
90 |
91 | 95 |
96 | 102 |
103 | ); 104 | } 105 | } 106 | 107 | export default ModuleTableWrapper; 108 | -------------------------------------------------------------------------------- /src/components/table/sort-types.ts: -------------------------------------------------------------------------------- 1 | // want to be moved to separate module 2 | export const SORT_ASC = 'asc'; 3 | export type SortDirAsc = typeof SORT_ASC; 4 | 5 | export const SORT_DESC = 'desc'; 6 | export type SortDirDesc = typeof SORT_DESC; 7 | 8 | export type SortDir = SortDirAsc | SortDirDesc; 9 | 10 | export const SortTypes: {[key: string]: SortDir} = { 11 | ASC: SORT_ASC, 12 | DESC: SORT_DESC 13 | }; 14 | 15 | export type SortingOrder = { 16 | key: SortKey; 17 | direction: SortDir; 18 | }; 19 | 20 | export type SortKey = 21 | 'name' | 22 | 'size' | 23 | 'reasonsCount'; 24 | -------------------------------------------------------------------------------- /src/components/table/sortable-header-cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Cell } from 'fixed-data-table-2'; 3 | import classnames from 'classnames'; 4 | 5 | import { SortDir, SortTypes } from './sort-types'; 6 | 7 | function reverseSortDirection(sortDir: SortDir): SortDir { 8 | return sortDir === SortTypes.DESC ? SortTypes.ASC : SortTypes.DESC; 9 | } 10 | 11 | export interface SortHeaderCellProps { 12 | columnKey?: string; // will be injected by 'fixed-data-table' 13 | sortDir?: SortDir; 14 | onSortChange?: (columnKey: string, sortDir: SortDir) => void; 15 | } 16 | 17 | class SortHeaderCell extends React.Component { 18 | constructor(props: SortHeaderCellProps) { 19 | super(props); 20 | 21 | this._onSortChange = this._onSortChange.bind(this); 22 | } 23 | 24 | _onSortChange(e: React.MouseEvent) { 25 | if (this.props.onSortChange) { 26 | this.props.onSortChange( 27 | this.props.columnKey, 28 | this.props.sortDir ? 29 | reverseSortDirection(this.props.sortDir) : 30 | SortTypes.DESC 31 | ); 32 | } 33 | } 34 | 35 | render() { 36 | var { sortDir, children, onSortChange, ...props } = this.props; 37 | const sortingLabel = sortDir ? (sortDir === SortTypes.DESC ? '↑' : '↓') : ''; 38 | 39 | const cellClassnames = classnames('table__cell', { 40 | 'table__cell--clickable': Boolean(onSortChange) 41 | }); 42 | 43 | return ( 44 | 49 | {children} {sortingLabel} 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default SortHeaderCell; 56 | -------------------------------------------------------------------------------- /src/components/table/table-style.css: -------------------------------------------------------------------------------- 1 | .table { 2 | display: flex; 3 | height: 100%; 4 | flex-direction: column; 5 | } 6 | 7 | .reference-table__element { 8 | font-size: 11px; 9 | } 10 | 11 | .table__cell--clickable { 12 | cursor: pointer; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/table/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Dimensions from 'react-dimensions'; 3 | import { Table, Column, Cell } from 'fixed-data-table-2'; 4 | 5 | import { SortDir, SortTypes, SortKey, SortingOrder } from './sort-types'; 6 | import SortHeaderCell from './sortable-header-cell'; 7 | import TextCell from './text-cell'; 8 | import 'fixed-data-table-2/dist/fixed-data-table.css'; 9 | 10 | interface ModulesTableProps { 11 | containerWidth: number; // injected by react-dimension 12 | containerHeight: number; // injected by react-dimension 13 | colSortDirs: SortingOrder; 14 | modules: Module[]; 15 | onSelect: (moduleId: ModuleId) => void; 16 | onSortChange: (columnKey: string, sortDir: SortDir) => void; 17 | } 18 | 19 | function getSortDirFor(key: SortKey, colSortDirs: SortingOrder) { 20 | return colSortDirs.key === key ? colSortDirs.direction : null; 21 | } 22 | 23 | // we use class since react-dimension uses refs 24 | class ModuleTable extends React.Component { 25 | render() { 26 | return ( 27 | 35 | 39 | Module id 40 | 41 | } 42 | cell={} 43 | fixed={true} 44 | flexGrow={1} 45 | width={50} 46 | /> 47 | 54 | Module name 55 | 56 | } 57 | cell={ 58 | 63 | } 64 | fixed={true} 65 | flexGrow={2} 66 | width={310} 67 | /> 68 | 75 | Module size 76 | 77 | } 78 | cell={} 79 | fixed={true} 80 | flexGrow={1} 81 | width={70} 82 | /> 83 | 84 | 91 | Module occurences 92 | 93 | } 94 | cell={} 95 | fixed={true} 96 | flexGrow={1} 97 | width={70} 98 | /> 99 |
100 | ); 101 | } 102 | } 103 | 104 | export default Dimensions({ 105 | getHeight: function(elem: HTMLElement) { 106 | return elem.offsetHeight; 107 | }, 108 | getWidth: function(elem: HTMLElement) { 109 | return elem.offsetWidth; 110 | } 111 | })(ModuleTable); 112 | -------------------------------------------------------------------------------- /src/components/table/text-cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Cell } from 'fixed-data-table-2'; 3 | import classnames from 'classnames'; 4 | import './table-style.css'; 5 | 6 | export interface TextCellProps { 7 | rowIndex?: number; // will be injected by 'fixed-data-table' 8 | data: Module[]; 9 | col: keyof Module; 10 | onClick?: (id: ModuleId) => void; 11 | } 12 | 13 | const TextCell: React.SFC = props => { 14 | const { rowIndex, data, col, onClick, ...otherProps } = props; 15 | const module = data[rowIndex]; 16 | 17 | const cellClassnames = classnames('table__cell', { 18 | 'table__cell--clickable': Boolean(onClick) 19 | }); 20 | 21 | return ( 22 | onClick(module.uid) : undefined} 26 | > 27 | {module[col]} 28 | 29 | ); 30 | }; 31 | 32 | export default TextCell; 33 | -------------------------------------------------------------------------------- /src/graph/elements/hull.ts: -------------------------------------------------------------------------------- 1 | const d3 = require('d3'); 2 | 3 | const offset = 15; 4 | const curve = d3.svg.line().interpolate('cardinal-closed').tension(0.85); 5 | const drawCluster = d => curve(d.path); 6 | const getGroup = (i: Module): string => i.group.name; 7 | 8 | // move me from here 9 | function buildHulls(nodes) { 10 | var hulls = {}; 11 | // create point sets 12 | for (var k = 0; k < nodes.length; ++k) { 13 | var n = nodes[k]; 14 | 15 | var i = getGroup(n); 16 | var l = hulls[i] || (hulls[i] = []); 17 | 18 | // when node without any connections - ignore it 19 | if (n.weight) { 20 | l.push([n.x - offset, n.y - offset]); 21 | l.push([n.x - offset, n.y + offset]); 22 | l.push([n.x + offset, n.y - offset]); 23 | l.push([n.x + offset, n.y + offset]); 24 | } 25 | } 26 | 27 | return hulls; 28 | } 29 | 30 | function groupNodes(nodes) { 31 | const hulls = buildHulls(nodes); 32 | 33 | return Object.entries(hulls).map(([key, value]) => ({ group: key, path: d3.geom.hull(value) })); 34 | } 35 | 36 | export default function reInitilizeHull(options) { 37 | options.parent.selectAll('path.hull').remove(); 38 | 39 | const hull = options.parent 40 | .selectAll('path.hull') 41 | .data(groupNodes(options.nodes)) 42 | .enter() 43 | .append('path') 44 | .attr('class', 'hull') 45 | .attr('d', drawCluster) 46 | .style('fill', options.fill); 47 | 48 | function updateHullPosition() { 49 | if (!hull.empty()) { 50 | hull.data(groupNodes(options.nodes)).attr('d', drawCluster); 51 | } 52 | } 53 | 54 | return updateHullPosition; 55 | } 56 | -------------------------------------------------------------------------------- /src/graph/elements/link.ts: -------------------------------------------------------------------------------- 1 | export default function reInitilizeLinks({ parent, data, key }) { 2 | const link = parent.selectAll('line.link').data(data, key); 3 | const arrow = parent.selectAll('marker').data(data); 4 | 5 | arrow 6 | .enter() 7 | .append('marker') 8 | .attr('class', 'arrow') 9 | .attr('id', d => `marker-${d.uid}`) 10 | .attr('viewBox', '0 -2 4 4') 11 | .attr('refX', 8) 12 | .attr('refY', -0) 13 | .attr('markerWidth', 6) 14 | .attr('markerHeight', 6) 15 | .attr('orient', 'auto') 16 | .append('path') 17 | .attr('d', 'M0,-2L4,0L0,2'); 18 | 19 | link.exit().remove(); 20 | 21 | link 22 | .enter() 23 | .append('line') 24 | .attr('class', d => { 25 | var classStr = [ 26 | 'link', 27 | d.async && 'dashed' 28 | ].filter(Boolean).join(' '); 29 | 30 | return classStr; 31 | }) 32 | .attr('x1', d => { 33 | return d.source.x; 34 | }) 35 | .attr('y1', d => d.source.y) 36 | .attr('x2', d => d.target.x) 37 | .attr('y2', d => d.target.y) 38 | .style('stroke-width', d => 1) 39 | .attr('marker-end', d => `url(#marker-${d.uid})`); 40 | 41 | function updateLinkPosition() { 42 | link 43 | .attr('x1', d => d.source.x) 44 | .attr('y1', d => d.source.y) 45 | .attr('x2', d => d.target.x) 46 | .attr('y2', d => d.target.y); 47 | } 48 | 49 | return updateLinkPosition; 50 | } 51 | -------------------------------------------------------------------------------- /src/graph/elements/node.ts: -------------------------------------------------------------------------------- 1 | import { configureNodeDrag } from '../../utils/graph/control'; 2 | 3 | export default function reInitilizeNode({ parent, data, key, tick, fill, config, ondblclick }) { 4 | const node = parent.selectAll('circle.node').data(data, key); 5 | 6 | node.exit().remove(); 7 | 8 | node 9 | .enter() 10 | .append('circle') 11 | .attr('class', 'node') 12 | .attr('r', function(d: d3ForceItem) { 13 | // could be an object in case of module and number (as size) in case of module 14 | return Math.max( 15 | (d.linkCount ? Math.log2(d.size) : 0), // FIXME: d.linkCount as way to distinguish 16 | config.node.radius 17 | ); 18 | }) 19 | .attr('cx', d => d.x) 20 | .attr('cy', d => d.y) 21 | .on('dblclick', ondblclick) 22 | .call(configureNodeDrag(tick)); 23 | 24 | node.style('fill', fill); 25 | 26 | // boundaries https://gist.github.com/mbostock/1129492 , https://bl.ocks.org/mbostock/1129492 27 | function updateNodePosition(e: d3ForceEvent) { 28 | node 29 | .attr('cx', d => { 30 | if (d.x < -config.svg.xBound) { 31 | return d.x = Math.max(config.node.radius, d.x + 500 * e.alpha); 32 | } else if (d.x > config.svg.xBound) { 33 | return d.x = Math.min(config.svg.xBound, d.x - 500 * e.alpha); 34 | } 35 | 36 | return d.x; 37 | }) 38 | .attr('cy', d => { 39 | if (d.y < -config.svg.yBound) { 40 | return d.y = Math.max(config.node.radius, d.y + 500 * e.alpha); 41 | } else if (d.y > config.svg.yBound) { 42 | return d.y = Math.min(config.svg.yBound, d.y - 500 * e.alpha); 43 | } 44 | 45 | return d.y; 46 | }); 47 | } 48 | 49 | return updateNodePosition; 50 | } 51 | -------------------------------------------------------------------------------- /src/graph/elements/text.ts: -------------------------------------------------------------------------------- 1 | const transform = d => `translate(${d.x}, ${d.y})`; 2 | 3 | export default function reInitilizeText({ parent, data, key, config }){ 4 | const text = parent.selectAll('text.text').data(data, key); 5 | 6 | text.exit().remove(); 7 | 8 | text 9 | .enter() 10 | .append('text') 11 | .attr('class', 'text') 12 | .attr('x', config.node.radius + 2) 13 | .attr('y', config.node.radius * 0.75) 14 | .text(d => d.name); 15 | 16 | function updateTextPosition() { 17 | text.attr('transform', transform); 18 | } 19 | 20 | return updateTextPosition; 21 | } 22 | -------------------------------------------------------------------------------- /src/graph/graph-style.css: -------------------------------------------------------------------------------- 1 | .link { 2 | stroke: #333; 3 | stroke-opacity: 0.5; 4 | pointer-events: none; 5 | } 6 | 7 | .link.dashed { 8 | stroke-dasharray: 8, 8; 9 | } 10 | 11 | .node { 12 | fill: lightsteelblue; 13 | stroke: #555; 14 | stroke-width: 3px; 15 | } 16 | 17 | .node.leaf { 18 | stroke: #fff; 19 | stroke-width: 1.5px; 20 | } 21 | 22 | text { 23 | font: 8px sans-serif; 24 | pointer-events: none; 25 | text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; 26 | } 27 | 28 | path.hull { 29 | fill: lightsteelblue; 30 | fill-opacity: 0.2; 31 | } 32 | 33 | .node.fade { 34 | opacity: 0.3; 35 | } 36 | 37 | .link.fade { 38 | stroke-width: 1px; 39 | stroke-opacity: 0.1; 40 | } 41 | 42 | .text.fade { 43 | opacity: 0.3; 44 | } 45 | 46 | .arrow.fade { 47 | opacity: 0.3; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/graph/index.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import get from 'lodash/get'; 3 | import uniqBy from 'lodash/uniqBy'; 4 | 5 | // that's kludge since typings for d3@v3 are outdated 6 | const d3 = require('d3'); 7 | 8 | import './graph-style.css'; 9 | 10 | import { 11 | configureZoomFit, 12 | cofigureZoom, 13 | resize 14 | } from '../utils/graph/control'; 15 | 16 | const getIndex = i => i.name; 17 | const getGroup = (i: Module): string => i.group.name; 18 | 19 | import reInitilizeLinks from './elements/link'; 20 | import reInitilizeText from './elements/text'; 21 | import reInitilizeNode from './elements/node'; 22 | import reInitilizeHull from './elements/hull'; 23 | 24 | const config = { 25 | svg: { 26 | width: 500, 27 | height: 500, 28 | offsetRight: 500, 29 | xBound: 3000, 30 | yBound: 3000 31 | }, 32 | node: { 33 | radius: 4, 34 | activeColor: 'red' 35 | }, 36 | 37 | }; 38 | 39 | const nodeid = (d: d3ForceItem): string => d.uid; 40 | 41 | function linkid(l: d3Force) { 42 | const u = nodeid(l.source); 43 | const v = nodeid(l.target); 44 | 45 | return u < v ? u + '|' + v : v + '|' + u; 46 | } 47 | 48 | interface GraphProps { 49 | nodes: Module[]; 50 | edges: ModuleEdge[]; 51 | 52 | activeModuleId: ModuleId; 53 | isModuleOverview: boolean; 54 | 55 | onModuleSelect: (name: string) => void; 56 | onGroupSelect: (name: string) => void; 57 | } 58 | 59 | // pefrormance http://stackoverflow.com/questions/26188266/how-to-speed-up-the-force-layout-animation-in-d3-js 60 | export function bootstrap(root: HTMLElement): { 61 | updateData: (props: GraphProps) => void, 62 | updateFilter: (value: string, isModuleOverview: boolean) => void 63 | } { 64 | let force; 65 | const fill = d3.scale.category20(); 66 | 67 | const svg = d3.select(root).append('svg'); 68 | const g = svg.append('g'); 69 | 70 | // NOTE: order matters for capturing events 71 | const hullg = g.append('g'); 72 | const linkg = g.append('g'); 73 | const nodeg = g.append('g'); 74 | const textg = g.append('g'); 75 | 76 | const zoomFit = configureZoomFit(); 77 | cofigureZoom({ frame: g, target: svg }); 78 | 79 | function cleanFiltered() { 80 | g.selectAll('.fade').classed('fade', false); 81 | } 82 | 83 | function cleanHull() { 84 | hullg.selectAll('*').remove(); 85 | } 86 | 87 | function updateFilter(value: string, isModuleOverview: boolean) { 88 | cleanFiltered(); 89 | 90 | if (!value) { 91 | return; 92 | } 93 | 94 | const filterReg = new RegExp(value, 'i'); 95 | 96 | // TODO: make declarative api 97 | g.selectAll('.node') 98 | .filter(d => !filterReg.test(getIndex(d))) 99 | .classed('fade', true); 100 | 101 | g.selectAll('.text') 102 | .filter(d => !filterReg.test(getIndex(d))) 103 | .classed('fade', true); 104 | 105 | g.selectAll('.link') 106 | .filter(d => !(filterReg.test(getIndex(d.source)) && filterReg.test(getIndex(d.target)))) 107 | .classed('fade', true); 108 | 109 | g.selectAll('.arrow') 110 | .filter(d => !(filterReg.test(getIndex(d.source)) && filterReg.test(getIndex(d.target)))) 111 | .classed('fade', true); 112 | } 113 | 114 | function updateData(props: GraphProps) { 115 | if (props.isModuleOverview) { 116 | renderOverview(props); 117 | } else { 118 | render(props); 119 | } 120 | 121 | cleanFiltered(); 122 | zoomFit(); 123 | } 124 | 125 | function renderOverview(props: GraphProps) { 126 | cleanHull(); 127 | const { nodes, edges, isModuleOverview, activeModuleId } = props; 128 | 129 | if (force) { 130 | force.stop(); 131 | } 132 | 133 | force = d3.layout 134 | .force() 135 | .nodes(nodes) 136 | .links(edges) 137 | .size([config.svg.width, config.svg.height]) 138 | // check 139 | // http://stackoverflow.com/questions/11894057/configure-fixed-layout-static-graph-in-d3-js 140 | // http://stackoverflow.com/questions/34355120/d3-js-linkstrength-influence-on-linkdistance-in-a-force-graph 141 | .linkDistance(function(l: d3Force, i: d3Force) { 142 | var n1 = l.source; 143 | var n2 = l.target; 144 | 145 | var sizeInfluence = Math.max( 146 | Math.log2(n1.size), 147 | Math.log2(n2.size), 148 | 1 149 | ); 150 | 151 | var connectionsInfluence = n1.uid === n2.uid 152 | ? 0 153 | : Math.max( 154 | Math.log2(n1.linkCount), 155 | Math.log2(n2.linkCount), 156 | 0 157 | ); 158 | 159 | var long = 30 + Math.max( 160 | Math.min( 161 | 20 * sizeInfluence, 162 | -20 + 20 * connectionsInfluence 163 | ), 164 | 100 165 | ); 166 | 167 | return long; 168 | }) 169 | // understanding gravity http://bl.ocks.org/sathomas/191a8a302a363ac6a4b0 170 | // gravity+charge tweaked to ensure good 'grouped' view 171 | .gravity(0.05) 172 | .charge(n => -800 * Math.max(Math.log2(n.size), 1)) 173 | .friction(0.2) // also influnce groupping 174 | .on('tick', tick) 175 | .start(); 176 | 177 | const updateNodePosition = reInitilizeNode({ 178 | parent: nodeg, 179 | data: nodes, 180 | key: nodeid, 181 | // we need to control node drag. 182 | // since we also need to update hull position, so we pass the whole tick 183 | tick, 184 | fill(d: d3ForceItem) { 185 | return fill(d.uid); 186 | }, 187 | config, 188 | ondblclick(d: d3ForceItem) { 189 | props.onGroupSelect(d.uid); 190 | } 191 | }); 192 | 193 | const updateLinkPosition = reInitilizeLinks({ 194 | parent: linkg, 195 | data: edges, 196 | key: linkid 197 | }); 198 | 199 | const updateTextPosition = reInitilizeText({ 200 | parent: textg, 201 | data: nodes, 202 | key: nodeid, 203 | config 204 | }); 205 | 206 | function tick(e: d3ForceEvent) { 207 | updateNodePosition(e); 208 | updateLinkPosition(); 209 | updateTextPosition(); 210 | } 211 | 212 | resize({ target: svg, force, offsetRight: config.svg.offsetRight }); 213 | } 214 | 215 | function render(props: GraphProps) { 216 | const { nodes, edges, isModuleOverview, activeModuleId } = props; 217 | if (force) { 218 | force.stop(); 219 | } 220 | 221 | const isModuleActive = (d): boolean => d.uid === activeModuleId; 222 | 223 | const net = { nodes, links: edges }; // network({ nodes, edges }, previousState, isNodeExpand); 224 | console.log('init module', net); 225 | 226 | force = d3.layout 227 | .force() 228 | .nodes(nodes) 229 | .links(edges) 230 | .size([config.svg.width, config.svg.height]) 231 | // check 232 | // http://stackoverflow.com/questions/11894057/configure-fixed-layout-static-graph-in-d3-js 233 | // http://stackoverflow.com/questions/34355120/d3-js-linkstrength-influence-on-linkdistance-in-a-force-graph 234 | .linkDistance(function(l: d3Force, i: d3Force) { 235 | const path1 = l.source.path; 236 | const path2 = l.target.path; 237 | const long = 5 * (90 238 | - 30 * Number(path1[0] === path2[0]) 239 | - 20 * Number(path1[0] === path2[0]) 240 | - 10 * Number(path1[0] === path2[0]) 241 | // - 10 * (path1[3] === path2[3] ? 1 : 0) 242 | // - 5 * (path1[4] === path2[4] ? 1 : 0) 243 | ); 244 | 245 | return long; 246 | }) 247 | // understanding gravity http://bl.ocks.org/sathomas/191a8a302a363ac6a4b0 248 | // gravity+charge tweaked to ensure good 'grouped' view 249 | .gravity(0) 250 | .charge(-900) 251 | .friction(0.5) // friction adjusted to get dampened display 252 | .on('tick', tick) 253 | .start(); 254 | 255 | const updateHullPosition = reInitilizeHull({ 256 | parent: hullg, 257 | get nodes() { 258 | // here we need lazy access to update data on tick 259 | return nodes; 260 | }, 261 | fill(d: d3HullItem) { 262 | const group = d.group; 263 | return fill(group); 264 | } 265 | }); 266 | 267 | const updateNodePosition = reInitilizeNode({ 268 | parent: nodeg, 269 | data: nodes, 270 | key: nodeid, 271 | // we need to contol node drag. 272 | // since we also need to update hull position, so we pass the whole tick 273 | tick, 274 | fill(d: d3ForceItem) { 275 | return isModuleActive(d) ? config.node.activeColor : fill(getGroup(d)); 276 | }, 277 | config, 278 | ondblclick(d: d3ForceItem) { 279 | props.onModuleSelect(d.uid); 280 | } 281 | }); 282 | 283 | const updateLinkPosition = reInitilizeLinks({ 284 | parent: linkg, 285 | data: edges, 286 | key: linkid 287 | }); 288 | 289 | const updateTextPosition = reInitilizeText({ 290 | parent: textg, 291 | data: nodes, 292 | key: nodeid, 293 | config 294 | }); 295 | 296 | function tick(e: d3ForceEvent) { 297 | updateNodePosition(e); 298 | updateLinkPosition(); 299 | updateTextPosition(); 300 | updateHullPosition(); 301 | } 302 | 303 | resize({ target: svg, force, offsetRight: config.svg.offsetRight }); 304 | } 305 | 306 | zoomFit(); 307 | 308 | return { updateData, updateFilter }; 309 | } 310 | -------------------------------------------------------------------------------- /src/init-style.css: -------------------------------------------------------------------------------- 1 | .init-page { 2 | height: 100%; 3 | } 4 | 5 | .init-upload { 6 | display: flex; 7 | height: 100%; 8 | 9 | justify-content: center; 10 | align-items: center; 11 | flex-direction: column; 12 | } 13 | -------------------------------------------------------------------------------- /src/init.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import GithubCorner from 'react-github-corner'; 4 | 5 | import FileUpload from './components/file-upload/'; 6 | import Button from './components/button/'; 7 | 8 | import run from './client'; 9 | 10 | import './styles/reset.css'; 11 | import './styles/common.css'; 12 | import './init-style.css'; 13 | 14 | const exampleStats = require('../examples/sound-redux/stats.json'); 15 | 16 | function onLoad(e: React.FormEvent) { 17 | var fileReader = new FileReader(); 18 | var file = (e.target as HTMLInputElement).files[0]; 19 | 20 | fileReader.readAsText(file); 21 | fileReader.onload = function() { 22 | var data = fileReader.result; 23 | run(JSON.parse(data)); 24 | }; 25 | } 26 | 27 | function runWithExample() { 28 | run(exampleStats); 29 | } 30 | 31 | ReactDOM.render( 32 |
33 |
34 | 35 | Select stats.json 36 | 37 |
38 | 41 |
42 |
43 | 44 |
45 | , 46 | document.getElementById('root') 47 | ); 48 | -------------------------------------------------------------------------------- /src/styles/common.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, *:before, *:after { 6 | box-sizing: inherit; 7 | } 8 | 9 | html, 10 | body, 11 | #root { 12 | height: 100%; 13 | } 14 | 15 | body { 16 | color:#424242; 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface StoreState { 2 | nodes: Module[]; 3 | extendedModuleInfoId: ModuleId; 4 | extendedModuleGroupId: string; 5 | moduleIdStack: ModuleId[]; 6 | shortRenderedPathType: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/filter.ts: -------------------------------------------------------------------------------- 1 | import buildNetwork from './network'; 2 | import { getHighlightedPathByIdShort, getHighlightedPathById } from './search'; 3 | 4 | interface FilterModulesProps { 5 | activeModuleId: ModuleId; 6 | activeGroupId: string; 7 | isModuleOverview: boolean; 8 | 9 | modules: Module[]; 10 | getModuleById: GetModuleById; 11 | } 12 | 13 | export function filterData( 14 | nodes: Module[], 15 | edges: ModuleEdge[], 16 | props: FilterModulesProps 17 | ): {nodes: ModuleNode[], edges: ModuleEdge[]} { 18 | if (props.isModuleOverview) { 19 | return buildNetwork(nodes, edges); 20 | } 21 | 22 | const filteredModuleIds = filterModules(props); 23 | 24 | return { 25 | nodes: nodes.filter(node => filteredModuleIds.includes(node.uid)), 26 | edges: edges.filter(edge => 27 | filteredModuleIds.includes(edge.source.uid) && filteredModuleIds.includes(edge.target.uid) 28 | ) 29 | }; 30 | } 31 | 32 | function filterModules(props: FilterModulesProps): ModuleId[] { 33 | if (props.activeModuleId) { 34 | return filterByModuleId(props.activeModuleId, props.getModuleById); 35 | } else if (props.activeGroupId) { 36 | return filterByGroup(props.activeGroupId, props.modules, props.getModuleById); 37 | } 38 | } 39 | 40 | function filterByModuleId(id: ModuleId, getModuleById: GetModuleById): ModuleId[] { 41 | const module = getModuleById(id); 42 | 43 | const shortForm = module.group.is3rdPartyLibrary; 44 | 45 | const fn = shortForm ? getHighlightedPathByIdShort : getHighlightedPathById; 46 | let highlightedNodes = fn(id, getModuleById); 47 | 48 | return highlightedNodes; 49 | } 50 | 51 | function filterByGroup(name: string, modules: Module[], getModuleById: GetModuleById): ModuleId[] { 52 | var temp = modules 53 | .filter(m => name === m.group.name) 54 | .map(m => { 55 | const shouldUseChidlrenFilter = m.group.is3rdPartyLibrary; 56 | if (m.reasons.length) { 57 | const externals = m.reasons 58 | .filter(r => shouldUseChidlrenFilter ? r.isExternalModule : true) 59 | .map(r => r.moduleUid); 60 | 61 | if (externals.length) { 62 | return [m.uid, ...externals]; 63 | } 64 | } 65 | }) 66 | .filter(Boolean) 67 | .reduce( 68 | (acc, highlightedNodes) => { 69 | highlightedNodes.forEach(acc.add, acc); 70 | return acc; 71 | }, 72 | new Set() 73 | ); 74 | 75 | return Array.from(temp); 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/graph/control.ts: -------------------------------------------------------------------------------- 1 | const d3 = require('d3'); 2 | 3 | // custom dnd to prevent problems with zooming 4 | // drag over zoom https://bl.ocks.org/mbostock/6123708 5 | function dragstart(d) { 6 | d3.event.sourceEvent.stopPropagation(); 7 | } 8 | 9 | function dragmove(tick: () => void, d) { 10 | d.px += d3.event.dx; 11 | d.py += d3.event.dy; 12 | d.x += d3.event.dx; 13 | d.y += d3.event.dy; 14 | 15 | tick(); 16 | } 17 | 18 | export function configureNodeDrag(tick: () => void) { 19 | return d3.behavior.drag() 20 | .on('dragstart', dragstart) 21 | .on('drag', d => dragmove(tick, d)); 22 | } 23 | 24 | const minZoom = 0.2; 25 | const maxZoom = 1; 26 | const medZoom = (minZoom + maxZoom) / 2; 27 | const zoom = d3.behavior.zoom().scaleExtent([minZoom, maxZoom]); 28 | 29 | export function cofigureZoom({ frame, target }) { 30 | zoom.on('zoom', function() { 31 | frame.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); 32 | }); 33 | 34 | target.call(zoom) 35 | .on('dblclick.zoom', null); // doesn't work if add above 36 | } 37 | 38 | // http://bl.ocks.org/TWiStErRob/b1c62730e01fe33baa2dea0d0aa29359 39 | // fit in with resize https://jsfiddle.net/adityap16/11edxrnq/1/ 40 | export function configureZoomFit() { 41 | return function zoomFit(scaleVal: number = medZoom) { 42 | return zoom.scale(scaleVal); 43 | // const node = target.node(); 44 | // const bounds = node.getBBox(); 45 | // const parent = node.parentElement; 46 | // const fullWidth = parent.clientWidth || parent.parentNode.clientWidth; 47 | // const fullHeight = parent.clientHeight || parent.parentNode.clientHeight; 48 | // const currentWidth = bounds.width; 49 | // const currentHeight = bounds.height; 50 | // const midX = bounds.x + currentWidth / 2, 51 | // midY = bounds.y + currentHeight / 2; 52 | 53 | // // nothing to fit 54 | // if (!currentWidth || !currentHeight) { 55 | // return; 56 | // } 57 | 58 | // const scale = 0.85 / Math.max(currentWidth / fullWidth, currentHeight / fullHeight); 59 | // const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY]; 60 | 61 | // target 62 | // .transition() 63 | // .duration(transitionDuration) // milliseconds 64 | // .call(zoom.translate(translate).scale(scale).event); 65 | }; 66 | } 67 | 68 | // resize implementation 69 | // http://bl.ocks.org/eyaler/10586116 70 | export function resize({ target, force, offsetRight }) { 71 | const currentWidth = window.innerWidth - offsetRight; 72 | const currentHeight = window.innerHeight; 73 | 74 | target.attr('width', currentWidth).attr('height', currentHeight); 75 | 76 | // force.size([currentWidth / zoom.scale(), currentHeight / zoom.scale()]).resume(); 77 | } 78 | 79 | // http://bl.ocks.org/altocumulus/32ab2bd41ec092f8a233dbefe37742e3 80 | export function stopForce(target) { 81 | target.stop(); 82 | } 83 | 84 | const sustainKoeff = 0.01; // sustain dead val 85 | export function speedUpForce(target) { 86 | var safetyGuard = 0; 87 | while (target.alpha() > sustainKoeff) { 88 | target.tick(); 89 | if (safetyGuard++ > 200) { 90 | break; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/network.ts: -------------------------------------------------------------------------------- 1 | // http://bl.ocks.org/GerHobbelt/3071239 2 | const getGroup = (i: Module): string => i.group.name; 3 | 4 | // constructs the network to visualize 5 | function network(dataNodes: Module[], dataEdges: ModuleEdge[]): {nodes: ModuleOverview[], edges: ModuleEdge[]} { 6 | const nodes = []; 7 | const groupMap = {}; 8 | const linksMap: {[key: string]: ModuleEdge} = {}; 9 | 10 | for (let k = 0; k < dataNodes.length; ++k) { 11 | const n = dataNodes[k]; 12 | const i = getGroup(n); 13 | 14 | if (!groupMap[i]) { 15 | groupMap[i] = { 16 | uid: i, 17 | nodeIndex: nodes.length, 18 | name: i, 19 | size: n.group.size, 20 | // chunks: new Set(), 21 | linkCount: 0 22 | }; 23 | nodes.push(groupMap[i]); 24 | } 25 | // groupMap[i].chunks.add(...n.chunks); 26 | } 27 | 28 | for (let i = 0; i < dataEdges.length; ++i) { 29 | const edge = dataEdges[i]; 30 | const sourceIdentifier = getGroup(edge.source); 31 | const targetIdentifier = getGroup(edge.target); 32 | 33 | // ignore internal connections 34 | if (sourceIdentifier === targetIdentifier) { 35 | continue; 36 | } 37 | 38 | const sourceLocalId = groupMap[sourceIdentifier].nodeIndex; 39 | const targetLocalId = groupMap[targetIdentifier].nodeIndex; 40 | 41 | // don't count external links 42 | if (sourceLocalId !== targetLocalId) { 43 | nodes[sourceLocalId].linkCount++; 44 | nodes[targetLocalId].linkCount++; 45 | } 46 | 47 | const id = sourceLocalId < targetLocalId ? 48 | sourceLocalId + '|' + targetLocalId : 49 | targetLocalId + '|' + sourceLocalId; 50 | 51 | if (!linksMap[id]) { 52 | linksMap[id] = { 53 | source: sourceLocalId, 54 | target: targetLocalId, 55 | id: id 56 | }; 57 | } 58 | } 59 | 60 | return { nodes, edges: Object.values(linksMap) }; 61 | } 62 | 63 | export default network; 64 | -------------------------------------------------------------------------------- /src/utils/process-data.ts: -------------------------------------------------------------------------------- 1 | import uniqBy from 'lodash/uniqBy'; 2 | import flow from 'lodash/flow'; 3 | const path = require('path'); 4 | 5 | const exec3rdPartyLibraryRegexp = /~|node_modules|\(webpack\)/; 6 | function exec3rdPartyLibrary(moduleName: string): RegExpExecArray | null { 7 | return exec3rdPartyLibraryRegexp.exec(moduleName); 8 | } 9 | 10 | function formatPathIden(targetId: ModuleId, sourceId: ModuleId): string { 11 | return `${targetId}-${sourceId}`; 12 | } 13 | 14 | function byId(a: WebpackModule, b: WebpackModule) { 15 | if (a.id > b.id) { 16 | return 1; 17 | } 18 | 19 | if (a.id < b.id) { 20 | return -1; 21 | } 22 | 23 | return 0; 24 | } 25 | 26 | function byReasonsCount(moduleA: Module, moduleB: Module) { 27 | return moduleB.reasonsCount - moduleA.reasonsCount; 28 | } 29 | 30 | const toFixed = (precision: number = 0) => (num: number) => num.toFixed(precision); 31 | const toKb = (size: number) => size / 1024; 32 | 33 | const castToKb = flow(toKb, toFixed(1), parseFloat); 34 | 35 | function processData(stats: WebpackStat) { 36 | stats.assets.sort(function(a: WebpackAsset, b: WebpackAsset) { 37 | return b.size - a.size; 38 | }); 39 | stats.modules.sort(byId); 40 | var mapModules = {}; 41 | const mapGroup = {}; 42 | 43 | const getModuleById: GetModuleById = id => mapModules[id]; 44 | 45 | const isExternalModule = (moduleId: ModuleId, reasonId: ModuleId): boolean => { 46 | const module = getModuleById(moduleId); 47 | const reason = getModuleById(reasonId); 48 | return module.group.name !== reason.group.name; 49 | }; 50 | 51 | // create module cache and normalise the shape 52 | const modules: Module[] = stats.modules.map(function(module: WebpackModule, idx: number): Module { 53 | const uid = String(module.id); 54 | 55 | const modulePathFull = module.name.replace(/^(\.\/)/, ''); 56 | const exec3rdPartyMatched = exec3rdPartyLibrary(module.name); 57 | const is3rdPartyLibrary = Boolean(exec3rdPartyMatched); 58 | 59 | const modulePath = is3rdPartyLibrary ? modulePathFull.slice(exec3rdPartyMatched.index) : modulePathFull; 60 | 61 | // TODO: check, not sure if it works in the browser 62 | const modulePathGroup = modulePath.split(path.sep); 63 | let [groupName] = modulePathGroup; 64 | 65 | if (!groupName) { 66 | groupName = module.name; 67 | } 68 | 69 | mapGroup[groupName] = mapGroup[groupName] || { 70 | name: groupName, 71 | size: 0, 72 | count: 0, 73 | is3rdPartyLibrary 74 | }; 75 | 76 | mapGroup[groupName].count += 1; 77 | mapGroup[groupName].size += module.size; 78 | 79 | const m = { 80 | uid, 81 | name: module.name, 82 | group: mapGroup[groupName], 83 | size: module.size, 84 | type: module.type, 85 | path: modulePathGroup, 86 | 87 | chunks: module.chunks.map(String), 88 | dependencies: [], 89 | reasons: [], 90 | reasonsCount: null 91 | }; 92 | 93 | mapModules[m.uid] = m; 94 | 95 | return m; 96 | }); 97 | 98 | // create chunk cache and normalise shape 99 | // var mapChunks = {}; 100 | // stats.chunks = stats.chunks || []; 101 | 102 | // stats.chunks.forEach(function(chunk: WebpackChunk) { 103 | // mapChunks[chunk.id] = chunk; 104 | // ... 105 | // }); 106 | 107 | // transform modules (reasons --> dependencies ...) 108 | modules.forEach(function(module: Module, idx: number) { 109 | var reasons = stats.modules[idx].reasons || []; 110 | 111 | var uniqueReasons = uniqBy(reasons, 'moduleId') 112 | .map(reason => ({ 113 | moduleUid: String(reason.moduleId), 114 | moduleName: reason.moduleName, 115 | type: reason.type, 116 | isExternalModule: isExternalModule(module.uid, String(reason.moduleId)) 117 | })); 118 | 119 | module.reasons = uniqueReasons; 120 | module.reasonsCount = uniqueReasons.length; 121 | 122 | uniqueReasons.forEach((r: Reason) => { 123 | var m = mapModules[r.moduleUid]; 124 | if (!m) { 125 | return; 126 | } 127 | 128 | m.dependencies.push({ 129 | moduleUid: module.uid, 130 | moduleName: module.name, 131 | type: module.type, 132 | isExternalModule: isExternalModule(module.uid, String(r.moduleUid)) 133 | }); 134 | }); 135 | }); 136 | 137 | var edges: ModuleEdge[] = []; 138 | modules.forEach(module => { 139 | const reasons = module.reasons; 140 | reasons.forEach(function(reason: Reason) { 141 | var parentModule = mapModules[reason.moduleUid]; 142 | if (!parentModule) { 143 | return; 144 | } 145 | 146 | var async = !module.chunks.every(function(chunk: string): boolean { 147 | return parentModule.chunks.includes(chunk); 148 | }); 149 | 150 | // borrowed from webpack-analyse 151 | // traverse and check belonging to parent(s) chunk 152 | 153 | // !module.chunks.some(function(chunk: string): boolean { 154 | // return (function isInChunks(chunks: string[], checked: string[]) { 155 | // if (chunks.length === 0) { 156 | // return false; 157 | // } 158 | 159 | // if (chunks.indexOf(chunk) !== -1) { 160 | // return true; 161 | // } 162 | 163 | // chunks = chunks.filter(function(c: string) { 164 | // return checked.indexOf(c) === -1; 165 | // }); 166 | 167 | // if (chunks.length === 0) { 168 | // return false; 169 | // } 170 | 171 | // return chunks.some(function(c: string) { 172 | // return isInChunks(mapChunks[c].parents, checked.concat(c)); 173 | // }); 174 | // }(parentModule.chunks, [])); 175 | // }); 176 | 177 | const edge: ModuleEdge = { 178 | id: formatPathIden(module.uid, parentModule.uid), 179 | source: mapModules[parentModule.uid], 180 | target: mapModules[module.uid], 181 | // async: false 182 | }; 183 | edges.push(edge); 184 | }); 185 | }); 186 | const groups = Object.keys(mapGroup).sort((keyA, keyB) => mapGroup[keyB].size - mapGroup[keyA].size); 187 | 188 | Object.keys(mapGroup).forEach(key => { 189 | mapGroup[key].size = castToKb(mapGroup[key].size); 190 | }); 191 | 192 | modules.sort(byReasonsCount); 193 | 194 | return { 195 | groups: mapGroup, 196 | modules: modules, 197 | edges, 198 | getModuleById 199 | }; 200 | } 201 | 202 | export default processData; 203 | -------------------------------------------------------------------------------- /src/utils/search.ts: -------------------------------------------------------------------------------- 1 | import { traverseBread } from './traverse'; 2 | 3 | export function getHighlightedPathByIdShort(startId: ModuleId, getModuleById: GetModuleById) { 4 | var highlightedNodes: ModuleId[] = []; 5 | traverseBread( 6 | startId, 7 | function visit(id: ModuleId): void{ 8 | highlightedNodes.push(id); 9 | }, 10 | function getChildren(id: ModuleId): ModuleId[]{ 11 | var module = getModuleById(id); 12 | if (module.reasons.length) { 13 | const externals = module.reasons.filter(r => r.isExternalModule).map(r => r.moduleUid); 14 | highlightedNodes = highlightedNodes.concat(externals); 15 | 16 | const internals = module.reasons.filter(r => !r.isExternalModule).map(r => r.moduleUid); 17 | return internals; 18 | } 19 | } 20 | ); 21 | 22 | return highlightedNodes; 23 | } 24 | 25 | export function getHighlightedPathById(startId: ModuleId, getModuleById: GetModuleById) { 26 | const highlightedNodes: ModuleId[] = []; 27 | traverseBread( 28 | startId, 29 | function visit(id: ModuleId): void{ 30 | highlightedNodes.push(id); 31 | }, 32 | function getChildren(id: ModuleId): ModuleId[]{ 33 | var module = getModuleById(id); 34 | if (module.reasons.length) { 35 | const reasonIds = module.reasons.map(r => r.moduleUid); 36 | 37 | return reasonIds; 38 | } 39 | } 40 | ); 41 | 42 | return highlightedNodes; 43 | } 44 | 45 | export function getHighlightedDepsById(startId: ModuleId, getModuleById: GetModuleById) { 46 | var highlightedNodes: ModuleId[] = []; 47 | traverseBread( 48 | startId, 49 | function visit(id: ModuleId): void{ 50 | highlightedNodes.push(id); 51 | }, 52 | function getChildren(id: ModuleId): ModuleId[]{ 53 | var module = getModuleById(id); 54 | if (module.reasons.length) { 55 | const depsIds = module.dependencies 56 | // .filter(dep => isExternalModule(id, dep.moduleId)) 57 | .filter(dep => dep.isExternalModule) 58 | .map(dep => dep.moduleUid); 59 | 60 | return depsIds; 61 | } 62 | } 63 | ); 64 | 65 | return highlightedNodes; 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/traverse.ts: -------------------------------------------------------------------------------- 1 | export function traverseBread( 2 | startId: ModuleId, 3 | visit: (id: ModuleId) => void, 4 | getChildren: (id: ModuleId) => ModuleId[] 5 | ): void { 6 | var queue = [startId]; 7 | var visited: {[key: string]: boolean} = {}; 8 | 9 | while (queue.length) { 10 | var id = queue.shift(); 11 | if (visited[id]) { 12 | continue; 13 | } 14 | 15 | visited[id] = true; 16 | 17 | visit(id); 18 | const children = getChildren(id); 19 | if (children) { 20 | queue = queue.concat(children); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tools/publish.js: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages'); 2 | const path = require('path'); 3 | 4 | const options = { 5 | src: 'index.html', 6 | dest: 'static' 7 | }; 8 | 9 | ghpages.publish('dist', options, function(err) { 10 | if(err){ 11 | console.error('publishing failed with error:', err); 12 | process.exit(1); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strictNullChecks": false, 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "target": "es2017", 9 | "allowJs": true 10 | }, 11 | "exclude": [ 12 | "build", 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react"], 3 | "rules": { 4 | "align": [ 5 | true, 6 | "parameters", 7 | "arguments", 8 | "statements" 9 | ], 10 | "ban": false, 11 | "class-name": true, 12 | "comment-format": [ 13 | true, 14 | "check-space" 15 | ], 16 | "curly": true, 17 | "eofline": false, 18 | "forin": true, 19 | "indent": [ true, "spaces" ], 20 | "interface-name": [true, "never-prefix"], 21 | "jsdoc-format": true, 22 | "jsx-boolean-value": "always", 23 | "jsx-no-multiline-js": false, 24 | "jsx-no-lambda": false, 25 | "jsx-wrap-multiline": false, 26 | "label-position": true, 27 | "max-line-length": [ true, 120 ], 28 | "member-ordering": [ 29 | true, 30 | "public-before-private", 31 | "static-before-instance", 32 | "variables-before-functions" 33 | ], 34 | "no-any": true, 35 | "no-arg": true, 36 | "no-bitwise": true, 37 | "no-consecutive-blank-lines": [true, 1], 38 | "no-construct": true, 39 | "no-debugger": true, 40 | "no-duplicate-variable": true, 41 | "no-empty": true, 42 | "no-eval": true, 43 | "no-shadowed-variable": true, 44 | "no-string-literal": true, 45 | "no-switch-case-fall-through": true, 46 | "no-trailing-whitespace": false, 47 | "no-unused-expression": true, 48 | "no-unused-variable": true, 49 | "no-use-before-declare": true, 50 | "one-line": [ 51 | true, 52 | "check-catch", 53 | "check-else", 54 | "check-open-brace", 55 | "check-whitespace" 56 | ], 57 | "quotemark": [true, "single", "jsx-double"], 58 | "radix": true, 59 | "semicolon": [true, "always"], 60 | "switch-default": true, 61 | 62 | "trailing-comma": false, 63 | 64 | "triple-equals": [ true, "allow-null-check" ], 65 | "typedef": [ 66 | true, 67 | "parameter", 68 | "property-declaration" 69 | ], 70 | "typedef-whitespace": [ 71 | true, 72 | { 73 | "call-signature": "nospace", 74 | "index-signature": "nospace", 75 | "parameter": "nospace", 76 | "property-declaration": "nospace", 77 | "variable-declaration": "nospace" 78 | } 79 | ], 80 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 81 | "whitespace": [ 82 | true, 83 | "check-branch", 84 | "check-decl", 85 | "check-module", 86 | "check-operator", 87 | "check-separator", 88 | "check-type", 89 | "check-typecast" 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "node": "registry:dt/node#7.0.0+20170322231424" 4 | }, 5 | "dependencies": { 6 | "d3": "registry:npm/d3#3.0.0+20160723033700", 7 | "lodash": "registry:npm/lodash#4.0.0+20161015015725", 8 | "react": "registry:npm/react#15.0.1+20170104200836", 9 | "react-dom": "registry:npm/react-dom#15.0.1+20160826174104" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | type ModuleId = string; 2 | type GetModuleById = (id: ModuleId) => Module; 3 | 4 | interface WebpackAsset { 5 | name: string; 6 | size: number; 7 | chunks: number[]; 8 | chunkNames: string[]; 9 | emitted: boolean; 10 | 11 | // added later 12 | normalizedName: string; 13 | } 14 | 15 | interface Reason { 16 | moduleUid: string; 17 | moduleName: string; 18 | type: string; 19 | isExternalModule: boolean; 20 | } 21 | 22 | interface ModuleGroup { 23 | name: string; 24 | is3rdPartyLibrary: boolean; 25 | path: string; 26 | size: number; 27 | } 28 | 29 | interface WebpackModule { 30 | id: ModuleId; 31 | identifier: string; 32 | name: string; 33 | index: number; 34 | index2: number; 35 | size: number; 36 | cacheable: boolean; 37 | built: boolean; 38 | optional: boolean; 39 | prefetched: boolean; 40 | chunks: number[]; 41 | assets: WebpackAsset[]; 42 | reasons: any[]; 43 | issuer: string; 44 | issuerId: number; 45 | issuerName: string; 46 | type: string; 47 | failed: boolean; 48 | errors: number; 49 | warnings: number; 50 | usedExports: any; // null | string[] 51 | providedExports: any; 52 | depth: number; 53 | source: string; 54 | 55 | // added later 56 | uid: string; 57 | dependencies: any[]; // ? 58 | reasonsCount: number; 59 | isOwn: boolean; 60 | isDirect: boolean; 61 | group: ModuleGroup; 62 | } 63 | 64 | interface WebpackChunk { 65 | id: number; 66 | rendered: boolean; 67 | initial: boolean; 68 | entry: boolean; 69 | extraAsync: boolean; 70 | size: number; 71 | names: any; //[]; 72 | files: any; // [extract-text-webpack-plugin-output-filename]; 73 | hash: string; 74 | parents: any; // [] 75 | origins: any //[] 76 | children: number[]; 77 | } 78 | 79 | declare interface WebpackStat { 80 | assets: WebpackAsset[]; 81 | character: number; 82 | modules: WebpackModule[]; 83 | chunks: WebpackChunk[] 84 | } 85 | 86 | declare interface Module { 87 | uid: string; 88 | group: ModuleGroup; 89 | size: number; 90 | type: string; 91 | name: string; 92 | path: string[]; 93 | 94 | chunks: string[]; 95 | dependencies: Reason[]; 96 | reasons: Reason[]; 97 | reasonsCount: number; 98 | linkCount?: number; // hack 99 | } 100 | 101 | declare interface ModuleOverview { 102 | uid: ModuleId; 103 | name: string; 104 | size: number; 105 | nodeIndex: number; 106 | linkCount: number; 107 | } 108 | 109 | 110 | type ModuleNode = Module | ModuleOverview; 111 | 112 | declare interface ModuleEdge { 113 | id: string; 114 | source: Module; 115 | target: Module; 116 | } 117 | 118 | interface d3ForceItem extends Module { 119 | id: number; 120 | px: number; 121 | py: number; 122 | weight: number; 123 | x: number; 124 | y: number; 125 | } 126 | 127 | interface d3Force { 128 | size: number; 129 | source: d3ForceItem; 130 | target: d3ForceItem; 131 | } 132 | 133 | interface d3HullItem { 134 | group: string; 135 | path: any[]; 136 | } 137 | 138 | interface d3ForceEvent { 139 | type: string; 140 | alpha: number; 141 | } 142 | -------------------------------------------------------------------------------- /typings/globals/node/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/a4a912a0cd1849fa7df0e5d909c8625fba04e49d/node/index.d.ts", 5 | "raw": "registry:dt/node#7.0.0+20170322231424", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/a4a912a0cd1849fa7df0e5d909c8625fba04e49d/node/index.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | -------------------------------------------------------------------------------- /typings/modules/d3/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/types/npm-d3/a3171387d85d30049479ca880c617e63dca23afe/typings.json", 5 | "raw": "registry:npm/d3#3.0.0+20160723033700", 6 | "main": "index.d.ts", 7 | "name": "d3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /typings/modules/lodash/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/types/npm-lodash/6557c3cd628ef6b39abfdc826b27ea1f06d8af89/typings.json", 5 | "raw": "registry:npm/lodash#4.0.0+20161015015725", 6 | "main": "index.d.ts", 7 | "version": "4.0.0", 8 | "name": "lodash" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /typings/modules/react-dom/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/cantide5ga/typed-react-dom/d498c5a1056d26babef04b56139628be1f59a357/server.d.ts 3 | declare module '~react-dom/server' { 4 | import { ReactElement } from '~react-dom~react/react'; 5 | 6 | namespace ReactDomServer { 7 | function renderToString(element: ReactElement): string; 8 | function renderToStaticMarkup(element: ReactElement): string; 9 | var version: string; 10 | } 11 | 12 | export = ReactDomServer; 13 | } 14 | declare module 'react-dom/server' { 15 | import main = require('~react-dom/server'); 16 | export = main; 17 | } 18 | 19 | // Generated by typings 20 | // Source: https://raw.githubusercontent.com/cantide5ga/typed-react/42692d400db3c333394ec75bce9f6d09b5a0a769/react.d.ts 21 | declare module '~react-dom~react/react' { 22 | // Type definitions for React v0.14 23 | // Project: http://facebook.github.io/react/ 24 | // Definitions by: Asana , AssureSign , Microsoft 25 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 26 | // Typings: Michael N. Payne 27 | 28 | namespace React { 29 | 30 | // 31 | // React Elements 32 | // ---------------------------------------------------------------------- 33 | 34 | type ReactType = string | ComponentClass | StatelessComponent; 35 | 36 | type Key = string | number; 37 | type Ref = string | ((instance: T) => any); 38 | type ComponentState = {} | void; 39 | 40 | interface Attributes { 41 | key?: Key; 42 | } 43 | interface ClassAttributes extends Attributes { 44 | ref?: Ref; 45 | } 46 | 47 | interface ReactElement

{ 48 | type: string | ComponentClass

| SFC

; 49 | props: P; 50 | key?: Key; 51 | } 52 | 53 | interface SFCElement

extends ReactElement

{ 54 | type: SFC

; 55 | } 56 | 57 | type CElement> = ComponentElement; 58 | interface ComponentElement> extends ReactElement

{ 59 | type: ComponentClass

; 60 | ref?: Ref; 61 | } 62 | 63 | type ClassicElement

= CElement>; 64 | 65 | interface DOMElement

extends ReactElement

{ 66 | type: string; 67 | ref: Ref; 68 | } 69 | 70 | interface ReactHTMLElement extends DOMElement { 71 | } 72 | 73 | interface ReactSVGElement extends DOMElement { 74 | } 75 | 76 | // 77 | // Factories 78 | // ---------------------------------------------------------------------- 79 | 80 | interface Factory

{ 81 | (props?: P & Attributes, ...children: ReactNode[]): ReactElement

; 82 | } 83 | 84 | interface SFCFactory

{ 85 | (props?: P & Attributes, ...children: ReactNode[]): SFCElement

; 86 | } 87 | 88 | interface ComponentFactory> { 89 | (props?: P & ClassAttributes, ...children: ReactNode[]): CElement; 90 | } 91 | 92 | type CFactory> = ComponentFactory; 93 | type ClassicFactory

= CFactory>; 94 | 95 | interface DOMFactory

{ 96 | (props?: P & ClassAttributes, ...children: ReactNode[]): DOMElement; 97 | } 98 | 99 | interface HTMLFactory extends DOMFactory { 100 | } 101 | 102 | interface SVGFactory extends DOMFactory { 103 | } 104 | 105 | // 106 | // React Nodes 107 | // http://facebook.github.io/react/docs/glossary.html 108 | // ---------------------------------------------------------------------- 109 | 110 | type ReactText = string | number; 111 | type ReactChild = ReactElement | ReactText; 112 | 113 | // Should be Array but type aliases cannot be recursive 114 | type ReactFragment = {} | Array; 115 | type ReactNode = ReactChild | ReactFragment | boolean; 116 | 117 | // 118 | // Top Level API 119 | // ---------------------------------------------------------------------- 120 | 121 | function createClass(spec: ComponentSpec): ClassicComponentClass

; 122 | 123 | function createFactory

( 124 | type: string): DOMFactory; 125 | function createFactory

(type: SFC

): SFCFactory

; 126 | function createFactory

( 127 | type: ClassType, ClassicComponentClass

>): CFactory>; 128 | function createFactory, C extends ComponentClass

>( 129 | type: ClassType): CFactory; 130 | function createFactory

(type: ComponentClass

| SFC

): Factory

; 131 | 132 | function createElement

( 133 | type: string, 134 | props?: P & ClassAttributes, 135 | ...children: ReactNode[]): DOMElement; 136 | function createElement

( 137 | type: SFC

, 138 | props?: P & Attributes, 139 | ...children: ReactNode[]): SFCElement

; 140 | function createElement

( 141 | type: ClassType, ClassicComponentClass

>, 142 | props?: P & ClassAttributes>, 143 | ...children: ReactNode[]): CElement>; 144 | function createElement, C extends ComponentClass

>( 145 | type: ClassType, 146 | props?: P & ClassAttributes, 147 | ...children: ReactNode[]): CElement; 148 | function createElement

( 149 | type: ComponentClass

| SFC

, 150 | props?: P & Attributes, 151 | ...children: ReactNode[]): ReactElement

; 152 | 153 | function cloneElement

( 154 | element: DOMElement, 155 | props?: P & ClassAttributes, 156 | ...children: ReactNode[]): DOMElement; 157 | function cloneElement

( 158 | element: SFCElement

, 159 | props?: Q, // should be Q & Attributes, but then Q is inferred as {} 160 | ...children: ReactNode[]): SFCElement

; 161 | function cloneElement

>( 162 | element: CElement, 163 | props?: Q, // should be Q & ClassAttributes 164 | ...children: ReactNode[]): CElement; 165 | function cloneElement

( 166 | element: ReactElement

, 167 | props?: Q, // should be Q & Attributes 168 | ...children: ReactNode[]): ReactElement

; 169 | 170 | function isValidElement

(object: {}): object is ReactElement

; 171 | 172 | var DOM: ReactDOM; 173 | var PropTypes: ReactPropTypes; 174 | var Children: ReactChildren; 175 | 176 | // 177 | // Component API 178 | // ---------------------------------------------------------------------- 179 | 180 | type ReactInstance = Component | Element; 181 | 182 | // Base component for plain JS classes 183 | class Component implements ComponentLifecycle { 184 | constructor(props?: P, context?: any); 185 | setState(f: (prevState: S, props: P) => S, callback?: () => any): void; 186 | setState(state: S, callback?: () => any): void; 187 | forceUpdate(callBack?: () => any): void; 188 | render(): ReactElement; 189 | 190 | // React.Props is now deprecated, which means that the `children` 191 | // property is not available on `P` by default, even though you can 192 | // always pass children as variadic arguments to `createElement`. 193 | // In the future, if we can define its call signature conditionally 194 | // on the existence of `children` in `P`, then we should remove this. 195 | props: P & { children?: ReactNode }; 196 | state: S; 197 | context: {}; 198 | refs: { 199 | [key: string]: ReactInstance 200 | }; 201 | } 202 | 203 | interface ClassicComponent extends Component { 204 | replaceState(nextState: S, callback?: () => any): void; 205 | isMounted(): boolean; 206 | getInitialState?(): S; 207 | } 208 | 209 | interface ChildContextProvider { 210 | getChildContext(): CC; 211 | } 212 | 213 | // 214 | // Class Interfaces 215 | // ---------------------------------------------------------------------- 216 | 217 | type SFC

= StatelessComponent

; 218 | interface StatelessComponent

{ 219 | (props?: P, context?: any): ReactElement; 220 | propTypes?: ValidationMap

; 221 | contextTypes?: ValidationMap; 222 | defaultProps?: P; 223 | displayName?: string; 224 | } 225 | 226 | interface ComponentClass

{ 227 | new(props?: P, context?: any): Component; 228 | propTypes?: ValidationMap

; 229 | contextTypes?: ValidationMap; 230 | childContextTypes?: ValidationMap; 231 | defaultProps?: P; 232 | displayName?: string; 233 | } 234 | 235 | interface ClassicComponentClass

extends ComponentClass

{ 236 | new(props?: P, context?: any): ClassicComponent; 237 | getDefaultProps?(): P; 238 | } 239 | 240 | /** 241 | * We use an intersection type to infer multiple type parameters from 242 | * a single argument, which is useful for many top-level API defs. 243 | * See https://github.com/Microsoft/TypeScript/issues/7234 for more info. 244 | */ 245 | type ClassType, C extends ComponentClass

> = 246 | C & 247 | (new() => T) & 248 | (new() => { props: P }); 249 | 250 | // 251 | // Component Specs and Lifecycle 252 | // ---------------------------------------------------------------------- 253 | 254 | interface ComponentLifecycle { 255 | componentWillMount?(): void; 256 | componentDidMount?(): void; 257 | componentWillReceiveProps?(nextProps: P, nextContext: any): void; 258 | shouldComponentUpdate?(nextProps: P, nextState: S, nextContext: any): boolean; 259 | componentWillUpdate?(nextProps: P, nextState: S, nextContext: any): void; 260 | componentDidUpdate?(prevProps: P, prevState: S, prevContext: any): void; 261 | componentWillUnmount?(): void; 262 | } 263 | 264 | interface Mixin extends ComponentLifecycle { 265 | mixins?: Mixin; 266 | statics?: { 267 | [key: string]: any; 268 | }; 269 | 270 | displayName?: string; 271 | propTypes?: ValidationMap; 272 | contextTypes?: ValidationMap; 273 | childContextTypes?: ValidationMap; 274 | 275 | getDefaultProps?(): P; 276 | getInitialState?(): S; 277 | } 278 | 279 | interface ComponentSpec extends Mixin { 280 | render(): ReactElement; 281 | 282 | [propertyName: string]: any; 283 | } 284 | 285 | // 286 | // Event System 287 | // ---------------------------------------------------------------------- 288 | 289 | interface SyntheticEvent { 290 | bubbles: boolean; 291 | cancelable: boolean; 292 | currentTarget: EventTarget; 293 | defaultPrevented: boolean; 294 | eventPhase: number; 295 | isTrusted: boolean; 296 | nativeEvent: Event; 297 | preventDefault(): void; 298 | stopPropagation(): void; 299 | target: EventTarget; 300 | timeStamp: Date; 301 | type: string; 302 | } 303 | 304 | interface ClipboardEvent extends SyntheticEvent { 305 | clipboardData: DataTransfer; 306 | } 307 | 308 | interface CompositionEvent extends SyntheticEvent { 309 | data: string; 310 | } 311 | 312 | interface DragEvent extends SyntheticEvent { 313 | dataTransfer: DataTransfer; 314 | } 315 | 316 | interface FocusEvent extends SyntheticEvent { 317 | relatedTarget: EventTarget; 318 | } 319 | 320 | interface FormEvent extends SyntheticEvent { 321 | } 322 | 323 | interface KeyboardEvent extends SyntheticEvent { 324 | altKey: boolean; 325 | charCode: number; 326 | ctrlKey: boolean; 327 | getModifierState(key: string): boolean; 328 | key: string; 329 | keyCode: number; 330 | locale: string; 331 | location: number; 332 | metaKey: boolean; 333 | repeat: boolean; 334 | shiftKey: boolean; 335 | which: number; 336 | } 337 | 338 | interface MouseEvent extends SyntheticEvent { 339 | altKey: boolean; 340 | button: number; 341 | buttons: number; 342 | clientX: number; 343 | clientY: number; 344 | ctrlKey: boolean; 345 | getModifierState(key: string): boolean; 346 | metaKey: boolean; 347 | pageX: number; 348 | pageY: number; 349 | relatedTarget: EventTarget; 350 | screenX: number; 351 | screenY: number; 352 | shiftKey: boolean; 353 | } 354 | 355 | interface TouchEvent extends SyntheticEvent { 356 | altKey: boolean; 357 | changedTouches: TouchList; 358 | ctrlKey: boolean; 359 | getModifierState(key: string): boolean; 360 | metaKey: boolean; 361 | shiftKey: boolean; 362 | targetTouches: TouchList; 363 | touches: TouchList; 364 | } 365 | 366 | interface UIEvent extends SyntheticEvent { 367 | detail: number; 368 | view: AbstractView; 369 | } 370 | 371 | interface WheelEvent extends SyntheticEvent { 372 | deltaMode: number; 373 | deltaX: number; 374 | deltaY: number; 375 | deltaZ: number; 376 | } 377 | 378 | // 379 | // Event Handler Types 380 | // ---------------------------------------------------------------------- 381 | 382 | interface EventHandler { 383 | (event: E): void; 384 | } 385 | 386 | type ReactEventHandler = EventHandler; 387 | 388 | type ClipboardEventHandler = EventHandler; 389 | type CompositionEventHandler = EventHandler; 390 | type DragEventHandler = EventHandler; 391 | type FocusEventHandler = EventHandler; 392 | type FormEventHandler = EventHandler; 393 | type KeyboardEventHandler = EventHandler; 394 | type MouseEventHandler = EventHandler; 395 | type TouchEventHandler = EventHandler; 396 | type UIEventHandler = EventHandler; 397 | type WheelEventHandler = EventHandler; 398 | 399 | // 400 | // Props / DOM Attributes 401 | // ---------------------------------------------------------------------- 402 | 403 | /** 404 | * @deprecated. This was used to allow clients to pass `ref` and `key` 405 | * to `createElement`, which is no longer necessary due to intersection 406 | * types. If you need to declare a props object before passing it to 407 | * `createElement` or a factory, use `ClassAttributes`: 408 | * 409 | * ```ts 410 | * var b: Button; 411 | * var props: ButtonProps & ClassAttributes