feat: add a github link, lazy load graph and dashboard
This commit is contained in:
Родитель
3a9258bc42
Коммит
3c076dc7d7
|
@ -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.
|
|
@ -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: {
|
||||
|
|
Загрузка…
Ссылка в новой задаче