This commit is contained in:
Connor Peet 2019-06-05 19:01:00 -07:00
Родитель 41041d0b3d
Коммит 4ed7bd27be
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: CF8FD2EA0DBC61BD
23 изменённых файлов: 185 добавлений и 111 удалений

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

@ -8,7 +8,7 @@
"test": "mocha --opts mocha.opts && npm run test:fmt && npm run test:lint",
"test:fmt": "prettier --list-different \"src/**/*.{tsx,ts}\" || echo \"Run npm run fmt to fix formatting on these files\"",
"test:lint": "tslint --project tsconfig.json \"src/**/*.{ts,tsx}\"",
"prepare": "tsc",
"build": "webpack --config webpack.production.js",
"start": "webpack-dev-server"
},
"repository": {

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

@ -9,12 +9,13 @@ import {
getTreeShakablePercent,
} from '../stat-reducers';
import { ChangedModuleGraph } from './graphs/changed-module-graph.component';
import { TreeShakeHint } from './hints/hints.component';
import { ModuleTable } from './module-table.component';
import { CounterPanel } from './panels/counter-panel.component';
import { NodeModulePanel } from './panels/node-module-panel.component';
import { PanelArrangement } from './panels/panel-arrangement.component';
import { Placeholder } from './placeholder.component';
import { formatPercent } from './util';
import { TreeShakeHint } from './hints/hints.component';
export const DashboardChunkPage: React.FC<{
chunk: number;
@ -23,6 +24,7 @@ export const DashboardChunkPage: React.FC<{
}> = ({ first, last, chunk }) => {
const firstObj = first.chunks!.find(c => c.id === chunk);
const lastSize = last.chunks!.find(c => c.id === chunk);
const nodeModules = compareNodeModules(first, last);
return (
<>
@ -68,12 +70,13 @@ export const DashboardChunkPage: React.FC<{
<h2>Node Modules</h2>
<PanelArrangement>
{compareNodeModules(first, last)
{nodeModules
.sort((a, b) => (b.new ? b.new.totalSize : 0) - (a.new ? a.new.totalSize : 0))
.map(comparison => (
<NodeModulePanel comparison={comparison} key={comparison.name} inChunk={chunk} />
))}
</PanelArrangement>
{nodeModules.length === 0 && <Placeholder>Huzzah! You have no dependencies!</Placeholder>}
</div>
<div className="col-xs-12 col-sm-6">
<h2>Module List</h2>

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

@ -1,6 +1,5 @@
import * as filesize from 'filesize';
import * as React from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { Stats } from 'webpack';
import {
getAverageChunkSize,
@ -17,96 +16,97 @@ import { CounterPanel } from './panels/counter-panel.component';
import { PanelArrangement } from './panels/panel-arrangement.component';
import { formatDuration, formatPercent } from './util';
import * as tabStyles from './dashboard-tabs.component.scss';
import { ChunkGraph } from './graphs/chunk-graph.component';
import { TreeShakeHint, WhatIsAnEntrypoint, AverageChunkSize, TotalModules } from './hints/hints.component';
import {
AverageChunkSize,
TotalModules,
TreeShakeHint,
WhatIsAnEntrypoint,
} from './hints/hints.component';
export const DashboardOverview: React.FC<{
first: Stats.ToJsonOutput;
last: Stats.ToJsonOutput;
}> = ({ first, last }) => {
return (
<div className="row" style={{ padding: 1 }}>
<div className="col-xs-12 col-sm-6">
<h2>Suggestions</h2>
<OverviewSuggestions first={first} last={last} />
<>
<div className="row" style={{ padding: 1 }}>
<div className="col-xs-12 col-sm-6">
<h2>Suggestions</h2>
<OverviewSuggestions first={first} last={last} />
<h2 style={{ marginTop: 64 }}>Stats</h2>
<PanelArrangement>
<CounterPanel
title="Total Size"
value={getTotalChunkSize(last)}
oldValue={getTotalChunkSize(first)}
formatter={filesize}
/>
<CounterPanel
title="Download Time (3 Mbps)"
value={(getTotalChunkSize(last) / (3500 * 128)) * 1000}
oldValue={(getTotalChunkSize(first) / (3500 * 128)) * 1000}
formatter={formatDuration}
/>
<CounterPanel
title="Avg. Chunk Size"
value={getAverageChunkSize(last)}
oldValue={getAverageChunkSize(first)}
hint={AverageChunkSize}
formatter={filesize}
/>
<CounterPanel
title="Entrypoint Size"
hint={WhatIsAnEntrypoint}
value={getEntryChunkSize(last)}
oldValue={getEntryChunkSize(first)}
formatter={filesize}
/>
<CounterPanel
title="Total Modules"
hint={TotalModules}
value={getTotalModuleCount(last)}
oldValue={getTotalModuleCount(first)}
/>
<CounterPanel
title="Node Module Size"
value={getNodeModuleSize(last)}
oldValue={getNodeModuleSize(first)}
formatter={filesize}
/>
<CounterPanel
title="Tree-Shaken Node Modules"
hint={TreeShakeHint}
value={getTreeShakablePercent(last)}
oldValue={getTreeShakablePercent(first)}
formatter={formatPercent}
/>
<CounterPanel
title="Node Module Count"
value={getNodeModuleCount(last)}
oldValue={getNodeModuleCount(first)}
/>
<CounterPanel
title="Build Time"
value={last.time!}
oldValue={first.time!}
formatter={formatDuration}
/>
</PanelArrangement>
<h2 style={{ marginTop: 64 }}>Stats</h2>
<PanelArrangement>
<CounterPanel
title="Total Size"
value={getTotalChunkSize(last)}
oldValue={getTotalChunkSize(first)}
formatter={filesize}
/>
<CounterPanel
title="Download Time (3 Mbps)"
value={(getTotalChunkSize(last) / (3500 * 128)) * 1000}
oldValue={(getTotalChunkSize(first) / (3500 * 128)) * 1000}
formatter={formatDuration}
/>
<CounterPanel
title="Avg. Chunk Size"
value={getAverageChunkSize(last)}
oldValue={getAverageChunkSize(first)}
hint={AverageChunkSize}
formatter={filesize}
/>
<CounterPanel
title="Entrypoint Size"
hint={WhatIsAnEntrypoint}
value={getEntryChunkSize(last)}
oldValue={getEntryChunkSize(first)}
formatter={filesize}
/>
<CounterPanel
title="Total Modules"
hint={TotalModules}
value={getTotalModuleCount(last)}
oldValue={getTotalModuleCount(first)}
/>
<CounterPanel
title="Node Module Size"
value={getNodeModuleSize(last)}
oldValue={getNodeModuleSize(first)}
formatter={filesize}
/>
<CounterPanel
title="Tree-Shaken Node Modules"
hint={TreeShakeHint}
value={getTreeShakablePercent(last)}
oldValue={getTreeShakablePercent(first)}
formatter={formatPercent}
/>
<CounterPanel
title="Node Module Count"
value={getNodeModuleCount(last)}
oldValue={getNodeModuleCount(first)}
/>
<CounterPanel
title="Build Time"
value={last.time!}
oldValue={first.time!}
formatter={formatDuration}
/>
</PanelArrangement>
</div>
<div className="col-xs-12 col-sm-6">
<h2>Module List</h2>
<ModuleTable first={first} last={last} />
</div>
</div>
<div className="col-xs-12 col-sm-6">
<Tabs className={tabStyles.tabs}>
<TabList>
<Tab>Chunk Graph</Tab>
<Tab>Module List</Tab>
</TabList>
<TabPanel>
<ChunkGraph stats={last} previous={first} />
</TabPanel>
<TabPanel>
<ModuleTable first={first} last={last} />
</TabPanel>
</Tabs>
</div>
</div>
<h2>
Chunk Graph
<small>
Larger chunks are shown in red, smaller ones in green. Click on a chunk to drill down.
</small>
</h2>
<ChunkGraph stats={last} previous={first} />
</>
);
};

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

@ -44,7 +44,7 @@ export class BaseGraph extends React.PureComponent<IProps> {
container,
boxSelectionEnabled: false,
autounselectify: true,
layout: { name: 'fcose', animate: false } as any,
layout: { name: 'fcose', animate: false, nodeSeparation: 150, quality: 'proof' } as any,
elements: { nodes: this.props.nodes, edges: this.props.edges },
style: [
{
@ -69,7 +69,7 @@ export class BaseGraph extends React.PureComponent<IProps> {
style: {
width: 1.5,
'line-color': '#5c2686',
'arrow-scale': 0.5,
'arrow-scale': 0.3,
'source-arrow-color': '#5c2686',
'source-arrow-shape': 'triangle',
'curve-style': 'bezier',
@ -238,7 +238,7 @@ export const expandModuleComparison = (
reasons = reasons.concat(node.new.reasons as any);
}
if (reasons.some(r => r.type.includes('entry'))) {
if (reasons.some(r => r.type && r.type.includes('entry'))) {
entries.push(node.identifier);
}

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

@ -3,6 +3,7 @@ import * as React from 'react';
import { Redirect } from 'react-router';
import { Stats } from 'webpack';
import { compareAllModules } from '../../stat-reducers';
import { Placeholder } from '../placeholder.component';
import { BaseGraph, expandModuleComparison } from './base-graph.component';
interface IProps {
@ -24,7 +25,7 @@ export class ChangedModuleGraph extends React.PureComponent<IProps, IState> {
public render() {
return this.state.redirect ? (
<Redirect to={this.state.redirect} push={true} />
) : (
) : this.state.nodes.length ? (
<BaseGraph
edges={this.state.edges}
nodes={this.state.nodes}
@ -33,6 +34,8 @@ export class ChangedModuleGraph extends React.PureComponent<IProps, IState> {
height={window.innerHeight * 0.9}
onClick={this.onClick}
/>
) : (
<Placeholder>No changes made in this bundle.</Placeholder>
);
}

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

@ -30,7 +30,7 @@ export class ChunkGraph extends React.PureComponent<IProps, IState> {
nodes={this.state.nodes}
rootNode={this.state.entries}
width="100%"
height={500}
height={0.9 * window.innerHeight}
onClick={this.onClick}
/>
);

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

@ -1,8 +1,8 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as styles from './hint-button.component.scss';
import { IoIosInformationCircleOutline } from 'react-icons/io';
import { classes } from '../util';
import * as styles from './hint-button.component.scss';
export class HintButton extends React.PureComponent<
{ hint: React.ComponentType<{}>; className?: string },

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

@ -111,7 +111,8 @@ export const DependentModules: React.FC = () => (
<>
<h2>Dependent Modules</h2>
<p>
This is the number of files, in your code or other dependency, that depend on this module. This number lets you easily see if you added or reduced coupling on this dependency.
This is the number of files, in your code or other dependency, that depend on this module.
This number lets you easily see if you added or reduced coupling on this dependency.
</p>
</>
);

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

@ -65,7 +65,9 @@ const ImportReason: React.FC<{ reason: Stats.Reason }> = ({ reason }) => {
<div className={styles.filename}>{reason.module}</div>
<div className={styles.fakeLine}>
<em>{reason.loc.split(':')[0]}</em>
{reason.type.includes('harmony') ? `import "${request}"` : `require("${request}")`}
{reason.type && reason.type.includes('harmony')
? `import "${request}"`
: `require("${request}")`}
</div>
</div>
);

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

@ -1,10 +1,10 @@
import * as filesize from 'filesize';
import * as React from 'react';
import { Stats } from 'webpack';
import { DependentModules, TotalNodeModuleSize, UniqueEntrypoints } from './hints/hints.component';
import { CounterPanel } from './panels/counter-panel.component';
import { PanelArrangement } from './panels/panel-arrangement.component';
import { color } from './util';
import { TotalNodeModuleSize, UniqueEntrypoints, DependentModules } from './hints/hints.component';
/**
* Prints the list of modules that import the target modules.

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

@ -48,7 +48,7 @@ export class ModuleTable extends React.PureComponent<IProps, IState> {
<Table
rowHeight={30}
width={this.state.width}
maxHeight={(window.innerHeight * 2) / 3}
maxHeight={700}
headerHeight={40}
rowsCount={this.state.diffs.length}
className={styles.plot}

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

@ -1,7 +1,7 @@
import * as React from 'react';
import { HintButton } from '../hints/hint-button.component';
import { classes } from '../util';
import * as styles from './panels.component.scss';
import { HintButton } from '../hints/hint-button.component';
export const enum ArrowDirection {
Up,

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

@ -0,0 +1,5 @@
.placeholder {
text-align: center;
padding: 50px;
color: rgba(#fff, 0.3);
}

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

@ -1,3 +1,6 @@
import * as React from 'react';
import * as styles from './placeholder.component.scss';
export const Placeholder: React.FC = props => <div>{props.children}</div>;
export const Placeholder: React.FC = props => (
<div className={styles.placeholder}>{props.children}</div>
);

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

@ -2,10 +2,12 @@ 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 { ScrollToTop } from './scroll-to-top.component';
export const Root: React.FC = () => (
<Router>
<Route path="/" exact component={EnterUrls} />
<Route path="/dashboard" component={Dashboard} />
<ScrollToTop />
</Router>
);

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

@ -0,0 +1,16 @@
import * as React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
class ScrollToTopComponent extends React.PureComponent<RouteComponentProps<{}>> {
public componentDidUpdate(prevProps: RouteComponentProps<{}>) {
if (this.props.location !== prevProps.location) {
window.scrollTo(0, 0);
}
}
public render() {
return null;
}
}
export const ScrollToTop = withRouter(ScrollToTopComponent);

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

@ -31,9 +31,8 @@ const store = createStore(
composeWithDevTools(applyMiddleware(workerMiddlware, epicMw)),
);
worker.onmessage = ev => {
console.log('data', ev.data);
store.dispatch(ev.data);
}
};
epicMw.run(epics);

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

@ -1,6 +1,7 @@
import { Base64 } from 'js-base64';
import { combineEpics, Epic as PlainEpic } from 'redux-observable';
import { of } from 'rxjs';
import { catchError, filter, map, mergeMap } from 'rxjs/operators';
import { EMPTY, of } from 'rxjs';
import { catchError, delay, filter, map, mergeMap } from 'rxjs/operators';
import { isActionOf } from 'typesafe-actions';
import { CompareAction, doAnalysis, fetchBundlephobiaData, loadAllUrls } from './actions';
import { IAppState } from './reducer';
@ -13,13 +14,31 @@ export interface IServices {
type Epic = PlainEpic<CompareAction, CompareAction, IAppState, IServices>;
const seedFromQueryStringEpic: Epic = () => {
const prefix = 'urls=';
const query = window.location.search;
const index = query.indexOf(prefix);
if (index === -1) {
return EMPTY;
}
return of(
loadAllUrls({
urls: query
.slice(index + prefix.length)
.split(',')
.map(Base64.decode),
}),
).pipe(delay(100));
};
const loadAllUrlsEpic: Epic = actions =>
actions.pipe(
filter(isActionOf(loadAllUrls)),
mergeMap(action => action.payload.urls.map(url => doAnalysis.request({ url }))),
);
const loadBundlephobiaInfo: Epic = (actions, _, { bundlephobia }) =>
const loadBundlephobiaInfoEpic: Epic = (actions, _, { bundlephobia }) =>
actions.pipe(
filter(isActionOf(fetchBundlephobiaData.request)),
mergeMap(action =>
@ -32,4 +51,8 @@ const loadBundlephobiaInfo: Epic = (actions, _, { bundlephobia }) =>
),
);
export const epics = combineEpics(loadAllUrlsEpic, loadBundlephobiaInfo);
export const epics = combineEpics(
loadAllUrlsEpic,
loadBundlephobiaInfoEpic,
seedFromQueryStringEpic,
);

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

@ -10,7 +10,13 @@ import {
import { createSelector } from 'reselect';
import { getType } from 'typesafe-actions';
import { Stats } from 'webpack';
import { clearLoadedBundles, CompareAction, doAnalysis, fetchBundlephobiaData } from './actions';
import {
clearLoadedBundles,
CompareAction,
doAnalysis,
fetchBundlephobiaData,
loadAllUrls,
} from './actions';
import { IBundlephobiaStats } from './services/bundlephobia-api';
/**
@ -47,6 +53,11 @@ export const reducer = (state = initialState, action: CompareAction): IAppState
...state,
bundles: getBundleUrls(state).reduce((acc, url) => ({ ...acc, [url]: idleRetrieval }), {}),
};
case getType(loadAllUrls):
return {
...state,
bundles: action.payload.urls.reduce((acc, url) => ({ ...acc, [url]: idleRetrieval }), {}),
};
case getType(doAnalysis.request):
return {
...state,

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

@ -137,8 +137,10 @@ export const getWebpackModules = (stats: Stats.ToJsonOutput, filterToChunk?: num
*/
export const getImportType = (importedModule: Stats.FnModules) =>
(importedModule.reasons as any).reduce(
(flags: number, reason: { type: string }) =>
(flags |= reason.type.includes('cjs')
(flags: number, reason: Stats.Reason) =>
(flags |= !reason.type
? ImportType.Unknown
: reason.type.includes('cjs')
? ImportType.CommonJs
: reason.type.includes('harmony')
? ImportType.EsModule

2
src/client/types/webpack.d.ts поставляемый
Просмотреть файл

@ -8,7 +8,7 @@ declare module 'webpack' {
moduleIdentifier: string;
module: string;
moduleName: string;
type: string;
type: string | null;
loc: string;
userRequest: string;
}

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

@ -1,10 +1,10 @@
import { decode } from 'msgpack-lite';
import { inflate } from 'pako';
import { from, Observable } from 'rxjs';
import { Stats } from 'webpack';
import { CompareAction, doAnalysis } from '../redux/actions';
import { ErrorCode } from '../redux/reducer';
import { Semaphore } from './semaphore';
import { inflate } from 'pako';
import { decode } from 'msgpack-lite';
const downloadSemaphore = new Semaphore(1);
@ -26,7 +26,6 @@ export function download(url: string): Observable<CompareAction> {
},
});
} catch (e) {
console.log('failed', e.stack);
return doAnalysis.failure({
url,
statusCode: 500,

5
webpack.production.js Normal file
Просмотреть файл

@ -0,0 +1,5 @@
module.exports = {
...require('./webpack.config'),
mode: 'production',
devtool: false,
};