feat: add a github link, lazy load graph and dashboard

This commit is contained in:
Connor Peet 2019-06-07 10:58:44 -07:00
Родитель 3a9258bc42
Коммит 3c076dc7d7
12 изменённых файлов: 238 добавлений и 181 удалений

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
package-lock.json сгенерированный
Просмотреть файл

@ -6416,6 +6416,12 @@
"scheduler": "^0.13.6"
}
},
"react-github-corner": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-github-corner/-/react-github-corner-2.3.0.tgz",
"integrity": "sha512-yVh8wtAfVA5CQYn8ygyxtsuCMoyiSEBVnzQJ3ynyWxZ1BzT2MHop4baZn/PnUnU9Yh8mwhLS5Y1RVxM5zNotGw==",
"dev": true
},
"react-icons": {
"version": "3.7.0",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/react-icons/-/react-icons-3.7.0.tgz",

Просмотреть файл

@ -71,6 +71,7 @@
"prettier": "^1.17.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-github-corner": "^2.3.0",
"react-icons": "^3.7.0",
"react-redux": "^7.0.3",
"react-router-dom": "^5.0.0",

Просмотреть файл

@ -97,6 +97,6 @@ class DashboardComponent extends React.PureComponent<IProps> {
);
}
export const Dashboard = connect((state: IAppState) => ({
export default connect((state: IAppState) => ({
stats: getKnownStats(state),
}))(DashboardComponent);

Просмотреть файл

@ -1,5 +1,6 @@
import { IRetrievalError, RetrievalState } from '@mixer/retrieval';
import * as React from 'react';
import GithubCorner from 'react-github-corner';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { clearLoadedBundles, loadAllUrls } from '../redux/actions';
@ -80,10 +81,13 @@ class EnterUrlsComponent extends React.PureComponent<IProps, IState> {
}
return (
<div className={styles.entry}>
<h1>Webpack Bundle Comparsion</h1>
{contents}
</div>
<>
<GithubCorner href="https://github.com/mixer/webpack-bundle-compare" />
<div className={styles.entry}>
<h1>Webpack Bundle Comparsion</h1>
{contents}
</div>
</>
);
}

Просмотреть файл

@ -1,15 +1,8 @@
import * as cytoscape from 'cytoscape';
import * as filesize from 'filesize';
import { Base64 } from 'js-base64';
import * as React from 'react';
import { IoIosContract, IoIosExpand } from 'react-icons/io';
import {
getReasons,
IWebpackModuleComparisonOutput,
normalizeIdentifier,
} from '../../stat-reducers';
import { formatPercentageDifference } from '../util';
import * as styles from './base-graph.component.scss';
import { filterUnattachedEdges } from './graph-tool';
// tslint:disable-next-line
cytoscape.use(require('cytoscape-fcose'));
@ -32,7 +25,7 @@ const enum FilterState {
const nodeHideThreshold = 100;
const labelHideThreshold = 100;
export class BaseGraph extends React.PureComponent<IProps, { filter: FilterState }> {
export default class BaseGraph extends React.PureComponent<IProps, { filter: FilterState }> {
public state = { filter: FilterState.NoFilter };
private readonly container = React.createRef<HTMLDivElement>();
@ -225,161 +218,3 @@ export class BaseGraph extends React.PureComponent<IProps, { filter: FilterState
}
};
}
export const filterUnattachedEdges = (
nodes: cytoscape.NodeDefinition[],
edges: cytoscape.EdgeDefinition[],
) => {
const nodeIds = new Set();
for (const node of nodes) {
nodeIds.add(node.data.id);
}
const outEdges: cytoscape.EdgeDefinition[] = [];
for (const edge of edges) {
if (nodeIds.has(edge.data.source) && nodeIds.has(edge.data.target)) {
outEdges.push(edge);
}
}
return outEdges;
};
export const fileSizeNode = ({
fromSize,
toSize,
area,
...options
}: cytoscape.NodeDataDefinition & {
fromSize: number;
toSize: number;
area: number;
}): cytoscape.NodeDataDefinition => {
const hue = fromSize < toSize ? 0 : fromSize > toSize ? 110 : 55;
const saturation = 40 + Math.min(60, (Math.abs(toSize - fromSize) / (toSize || 1)) * 100);
return {
...options,
label: `${options.label} (${filesize(toSize)}), ${formatPercentageDifference(
fromSize,
toSize,
)}`,
fontColor: fromSize !== toSize ? '#fff' : '#666',
bgColor: fromSize !== toSize ? `hsl(${hue}, ${saturation}%, 50%)` : '#666',
width: Math.round(2 * Math.sqrt(area / Math.PI)),
height: Math.round(2 * Math.sqrt(area / Math.PI)),
};
};
export const expandNode = <T extends { identifier: string }>({
roots,
getReasons: getReasonsFn,
createNode,
maxDepth = Infinity,
limit = 1000,
}: {
roots: T[];
limit?: number;
maxDepth?: number;
getReasons(node: T): T[];
createNode(node: T, id: string): cytoscape.NodeDataDefinition;
}) => {
const queue = roots.map(node => ({ node, depth: 0 }));
const nodes: cytoscape.NodeDefinition[] = [];
const sources = new Set<string>(roots.map(q => q.identifier));
const edges: cytoscape.EdgeDefinition[] = [];
let needsFiltering = false;
while (queue.length > 0) {
const { node, depth } = queue.pop()!;
if (depth > maxDepth) {
needsFiltering = true;
break;
}
if (--limit === 0) {
needsFiltering = true;
break;
}
const sourceEncoded = Base64.encodeURI(node.identifier);
for (const found of getReasonsFn(node)) {
const foundEncoded = Base64.encodeURI(found.identifier);
if (!sources.has(found.identifier)) {
sources.add(found.identifier);
queue.push({ node: found, depth: depth + 1 });
}
edges.push({
data: {
id: `edge${sourceEncoded}to${foundEncoded}`,
source: sourceEncoded,
target: foundEncoded,
},
});
}
nodes.push({
data: {
...createNode(node, sourceEncoded),
depth,
},
});
}
nodes.sort((a, b) => a.data.depth - b.data.depth);
return { nodes, edges: needsFiltering ? filterUnattachedEdges(nodes, edges) : edges };
};
export const expandModuleComparison = (
comparisons: { [name: string]: IWebpackModuleComparisonOutput },
roots: IWebpackModuleComparisonOutput[],
) => {
const maxBubbleArea = 150;
const minBubbleArea = 30;
const allComparisons = Object.values(comparisons);
const maxSize = allComparisons.reduce((max, cmp) => Math.max(max, cmp.toSize), 0);
const entries: string[] = [];
const { nodes, edges } = expandNode({
roots,
getReasons(node) {
const output: IWebpackModuleComparisonOutput[] = [];
for (const fnModule of [node.old, node.new]) {
if (!fnModule) {
continue;
}
for (const reason of getReasons(fnModule)) {
const other = comparisons[normalizeIdentifier(reason.moduleIdentifier)];
if (other) {
output.push(other);
}
if (reason.type && reason.type.includes('entry')) {
entries.push(Base64.encodeURI(node.identifier));
}
}
}
return output;
},
createNode(node, id) {
const weight = node.toSize / maxSize;
const area = Math.max(minBubbleArea, maxBubbleArea * weight);
return fileSizeNode({
id,
label: node.name,
area,
fromSize: node.fromSize,
toSize: node.toSize,
});
},
});
return { nodes, edges, entries };
};

Просмотреть файл

@ -6,7 +6,7 @@ import { Stats } from 'webpack';
import { compareAllModules, getNodeModuleFromIdentifier } from '../../stat-reducers';
import { Placeholder } from '../placeholder.component';
import { linkToModule, linkToNodeModule } from '../util';
import { BaseGraph, expandModuleComparison } from './base-graph.component';
import { expandModuleComparison, LazyBaseGraph } from './graph-tool';
interface IProps {
previous: Stats.ToJsonOutput;
@ -26,7 +26,7 @@ export const ChangedModuleGraph = withRouter(
public render() {
return this.state.nodes.length ? (
<BaseGraph
<LazyBaseGraph
edges={this.state.edges}
nodes={this.state.nodes}
rootNode={this.state.entries}

Просмотреть файл

@ -2,7 +2,7 @@ import * as cytoscape from 'cytoscape';
import * as React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { Stats } from 'webpack';
import { BaseGraph, fileSizeNode } from './base-graph.component';
import { fileSizeNode, LazyBaseGraph } from './graph-tool';
interface IProps {
previous: Stats.ToJsonOutput;
@ -23,7 +23,7 @@ export const ChunkGraph = withRouter(
public render() {
return (
<BaseGraph
<LazyBaseGraph
edges={this.state.edges}
nodes={this.state.nodes}
rootNode={this.state.entries}

Просмотреть файл

@ -12,7 +12,7 @@ import {
replaceLoaderInIdentifier,
} from '../../stat-reducers';
import { color, linkToModule, linkToNodeModule } from '../util';
import { BaseGraph, expandModuleComparison } from './base-graph.component';
import { expandModuleComparison, LazyBaseGraph } from './graph-tool';
interface IProps {
previous: Stats.ToJsonOutput;
@ -51,7 +51,7 @@ const createDependentGraph = <P extends {}>(
public render() {
return (
<BaseGraph
<LazyBaseGraph
edges={this.state.edges}
nodes={this.state.nodes}
rootNode={this.state.entries}

Просмотреть файл

@ -0,0 +1,178 @@
import * as cytoscape from 'cytoscape';
import * as filesize from 'filesize';
import { Base64 } from 'js-base64';
import * as React from 'react';
import {
getReasons,
IWebpackModuleComparisonOutput,
normalizeIdentifier,
} from '../../stat-reducers';
import { IndefiniteProgressBar } from '../progress-bar.component';
import { formatPercentageDifference } from '../util';
import BaseGraph from './base-graph.component';
export const filterUnattachedEdges = (
nodes: cytoscape.NodeDefinition[],
edges: cytoscape.EdgeDefinition[],
) => {
const nodeIds = new Set();
for (const node of nodes) {
nodeIds.add(node.data.id);
}
const outEdges: cytoscape.EdgeDefinition[] = [];
for (const edge of edges) {
if (nodeIds.has(edge.data.source) && nodeIds.has(edge.data.target)) {
outEdges.push(edge);
}
}
return outEdges;
};
export const fileSizeNode = ({
fromSize,
toSize,
area,
...options
}: cytoscape.NodeDataDefinition & {
fromSize: number;
toSize: number;
area: number;
}): cytoscape.NodeDataDefinition => {
const hue = fromSize < toSize ? 0 : fromSize > toSize ? 110 : 55;
const saturation = 40 + Math.min(60, (Math.abs(toSize - fromSize) / (toSize || 1)) * 100);
return {
...options,
label: `${options.label} (${filesize(toSize)}), ${formatPercentageDifference(
fromSize,
toSize,
)}`,
fontColor: fromSize !== toSize ? '#fff' : '#666',
bgColor: fromSize !== toSize ? `hsl(${hue}, ${saturation}%, 50%)` : '#666',
width: Math.round(2 * Math.sqrt(area / Math.PI)),
height: Math.round(2 * Math.sqrt(area / Math.PI)),
};
};
export const expandNode = <T extends { identifier: string }>({
roots,
getReasons: getReasonsFn,
createNode,
maxDepth = Infinity,
limit = 1000,
}: {
roots: T[];
limit?: number;
maxDepth?: number;
getReasons(node: T): T[];
createNode(node: T, id: string): cytoscape.NodeDataDefinition;
}) => {
const queue = roots.map(node => ({ node, depth: 0 }));
const nodes: cytoscape.NodeDefinition[] = [];
const sources = new Set<string>(roots.map(q => q.identifier));
const edges: cytoscape.EdgeDefinition[] = [];
let needsFiltering = false;
while (queue.length > 0) {
const { node, depth } = queue.pop()!;
if (depth > maxDepth) {
needsFiltering = true;
break;
}
if (--limit === 0) {
needsFiltering = true;
break;
}
const sourceEncoded = Base64.encodeURI(node.identifier);
for (const found of getReasonsFn(node)) {
const foundEncoded = Base64.encodeURI(found.identifier);
if (!sources.has(found.identifier)) {
sources.add(found.identifier);
queue.push({ node: found, depth: depth + 1 });
}
edges.push({
data: {
id: `edge${sourceEncoded}to${foundEncoded}`,
source: sourceEncoded,
target: foundEncoded,
},
});
}
nodes.push({
data: {
...createNode(node, sourceEncoded),
depth,
},
});
}
nodes.sort((a, b) => a.data.depth - b.data.depth);
return { nodes, edges: needsFiltering ? filterUnattachedEdges(nodes, edges) : edges };
};
export const expandModuleComparison = (
comparisons: { [name: string]: IWebpackModuleComparisonOutput },
roots: IWebpackModuleComparisonOutput[],
) => {
const maxBubbleArea = 150;
const minBubbleArea = 30;
const allComparisons = Object.values(comparisons);
const maxSize = allComparisons.reduce((max, cmp) => Math.max(max, cmp.toSize), 0);
const entries: string[] = [];
const { nodes, edges } = expandNode({
roots,
getReasons(node) {
const output: IWebpackModuleComparisonOutput[] = [];
for (const fnModule of [node.old, node.new]) {
if (!fnModule) {
continue;
}
for (const reason of getReasons(fnModule)) {
const other = comparisons[normalizeIdentifier(reason.moduleIdentifier)];
if (other) {
output.push(other);
}
if (reason.type && reason.type.includes('entry')) {
entries.push(Base64.encodeURI(node.identifier));
}
}
}
return output;
},
createNode(node, id) {
const weight = node.toSize / maxSize;
const area = Math.max(minBubbleArea, maxBubbleArea * weight);
return fileSizeNode({
id,
label: node.name,
area,
fromSize: node.fromSize,
toSize: node.toSize,
});
},
});
return { nodes, edges, entries };
};
const BaseGraphDeferred = React.lazy(() => import('./base-graph.component'));
export const LazyBaseGraph: React.FC<React.ComponentPropsWithoutRef<typeof BaseGraph>> = props => (
<React.Suspense fallback={<IndefiniteProgressBar />}>
<BaseGraphDeferred {...props} />
</React.Suspense>
);

Просмотреть файл

@ -1,13 +1,23 @@
import * as React from 'react';
import { HashRouter as Router, Route } from 'react-router-dom';
import { Dashboard } from './dashboard.component';
import { EnterUrls } from './enter-urls.component';
import { IndefiniteProgressBar } from './progress-bar.component';
import { ScrollToTop } from './scroll-to-top.component';
const importDashboard = () => import('./dashboard.component');
const DashboardDeferred = React.lazy(importDashboard);
setTimeout(importDashboard, 500); // start it loading in the background while we enter urls
const LazyDashboard: React.FC = () => (
<React.Suspense fallback={<IndefiniteProgressBar />}>
<DashboardDeferred />
</React.Suspense>
);
export const Root: React.FC = () => (
<Router>
<Route path="/" exact component={EnterUrls} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/dashboard" component={LazyDashboard} />
<ScrollToTop />
</Router>
);

Просмотреть файл

@ -2,6 +2,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const { DefinePlugin } = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const { BundleComparisonPlugin } = require('./');
module.exports = {
mode: 'development',
@ -45,6 +46,7 @@ module.exports = {
],
},
plugins: [
new BundleComparisonPlugin(),
new HtmlWebpackPlugin({
title: 'Webpack Bundle Compare',
}),
@ -52,7 +54,7 @@ module.exports = {
new DefinePlugin({
INITIAL_FILES: process.env.WBC_FILES
? JSON.stringify(process.env.WBC_FILES.split(','))
: JSON.stringify(['public/samples/spectrum1.msg.gz', 'public/samples/spectrum2.msg.gz']),
: JSON.stringify(['public/samples/spectrum1.msp.gz', 'public/samples/spectrum2.msp.gz']),
}),
],
devServer: {