diff --git a/package.json b/package.json index 79a166b..c1c857d 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/client/components/dashboard-chunk-page.component.tsx b/src/client/components/dashboard-chunk-page.component.tsx index 3005ff2..5a11b87 100644 --- a/src/client/components/dashboard-chunk-page.component.tsx +++ b/src/client/components/dashboard-chunk-page.component.tsx @@ -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<{

Node Modules

- {compareNodeModules(first, last) + {nodeModules .sort((a, b) => (b.new ? b.new.totalSize : 0) - (a.new ? a.new.totalSize : 0)) .map(comparison => ( ))} + {nodeModules.length === 0 && Huzzah! You have no dependencies!}

Module List

diff --git a/src/client/components/dashboard-overview.tsx b/src/client/components/dashboard-overview.tsx index aa7c998..d1136e7 100644 --- a/src/client/components/dashboard-overview.tsx +++ b/src/client/components/dashboard-overview.tsx @@ -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 ( -
-
-

Suggestions

- + <> +
+
+

Suggestions

+ -

Stats

- - - - - - - - - - - +

Stats

+ + + + + + + + + + + +
+
+

Module List

+ +
-
- - - Chunk Graph - Module List - - - - - - - - - - -
-
+

+ Chunk Graph + + Larger chunks are shown in red, smaller ones in green. Click on a chunk to drill down. + +

+ + ); }; diff --git a/src/client/components/graphs/base-graph.component.tsx b/src/client/components/graphs/base-graph.component.tsx index 85dba06..51cf812 100644 --- a/src/client/components/graphs/base-graph.component.tsx +++ b/src/client/components/graphs/base-graph.component.tsx @@ -44,7 +44,7 @@ export class BaseGraph extends React.PureComponent { 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 { 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); } diff --git a/src/client/components/graphs/changed-module-graph.component.tsx b/src/client/components/graphs/changed-module-graph.component.tsx index 953310a..941c0ef 100644 --- a/src/client/components/graphs/changed-module-graph.component.tsx +++ b/src/client/components/graphs/changed-module-graph.component.tsx @@ -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 { public render() { return this.state.redirect ? ( - ) : ( + ) : this.state.nodes.length ? ( { height={window.innerHeight * 0.9} onClick={this.onClick} /> + ) : ( + No changes made in this bundle. ); } diff --git a/src/client/components/graphs/chunk-graph.component.tsx b/src/client/components/graphs/chunk-graph.component.tsx index b96b5da..dfd06d5 100644 --- a/src/client/components/graphs/chunk-graph.component.tsx +++ b/src/client/components/graphs/chunk-graph.component.tsx @@ -30,7 +30,7 @@ export class ChunkGraph extends React.PureComponent { nodes={this.state.nodes} rootNode={this.state.entries} width="100%" - height={500} + height={0.9 * window.innerHeight} onClick={this.onClick} /> ); diff --git a/src/client/components/hints/hint-button.component.tsx b/src/client/components/hints/hint-button.component.tsx index 7915b0f..ca7ad54 100644 --- a/src/client/components/hints/hint-button.component.tsx +++ b/src/client/components/hints/hint-button.component.tsx @@ -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 }, diff --git a/src/client/components/hints/hints.component.tsx b/src/client/components/hints/hints.component.tsx index 34b4dac..5396c33 100644 --- a/src/client/components/hints/hints.component.tsx +++ b/src/client/components/hints/hints.component.tsx @@ -111,7 +111,8 @@ export const DependentModules: React.FC = () => ( <>

Dependent Modules

- 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.

); diff --git a/src/client/components/imports-list.component.tsx b/src/client/components/imports-list.component.tsx index 3fca22a..5920ae6 100644 --- a/src/client/components/imports-list.component.tsx +++ b/src/client/components/imports-list.component.tsx @@ -65,7 +65,9 @@ const ImportReason: React.FC<{ reason: Stats.Reason }> = ({ reason }) => {
{reason.module}
{reason.loc.split(':')[0]} - {reason.type.includes('harmony') ? `import "${request}"` : `require("${request}")`} + {reason.type && reason.type.includes('harmony') + ? `import "${request}"` + : `require("${request}")`}
); diff --git a/src/client/components/imports-stats-row.component.tsx b/src/client/components/imports-stats-row.component.tsx index 5c1b2a8..b57576d 100644 --- a/src/client/components/imports-stats-row.component.tsx +++ b/src/client/components/imports-stats-row.component.tsx @@ -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. diff --git a/src/client/components/module-table.component.tsx b/src/client/components/module-table.component.tsx index e05d565..7619bb1 100644 --- a/src/client/components/module-table.component.tsx +++ b/src/client/components/module-table.component.tsx @@ -48,7 +48,7 @@ export class ModuleTable extends React.PureComponent {
{props.children}
; +export const Placeholder: React.FC = props => ( +
{props.children}
+); diff --git a/src/client/components/root.component.tsx b/src/client/components/root.component.tsx index f4f3720..6bc9bb5 100644 --- a/src/client/components/root.component.tsx +++ b/src/client/components/root.component.tsx @@ -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 = () => ( + ); diff --git a/src/client/components/scroll-to-top.component.tsx b/src/client/components/scroll-to-top.component.tsx new file mode 100644 index 0000000..41aa40b --- /dev/null +++ b/src/client/components/scroll-to-top.component.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; + +class ScrollToTopComponent extends React.PureComponent> { + public componentDidUpdate(prevProps: RouteComponentProps<{}>) { + if (this.props.location !== prevProps.location) { + window.scrollTo(0, 0); + } + } + + public render() { + return null; + } +} + +export const ScrollToTop = withRouter(ScrollToTopComponent); diff --git a/src/client/index.tsx b/src/client/index.tsx index b20cc18..fbd3141 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -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); diff --git a/src/client/redux/epics.ts b/src/client/redux/epics.ts index fd78cc7..3973bd5 100644 --- a/src/client/redux/epics.ts +++ b/src/client/redux/epics.ts @@ -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; +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, +); diff --git a/src/client/redux/reducer.ts b/src/client/redux/reducer.ts index ffba590..dffefda 100644 --- a/src/client/redux/reducer.ts +++ b/src/client/redux/reducer.ts @@ -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, diff --git a/src/client/stat-reducers.ts b/src/client/stat-reducers.ts index 8008b66..9f68380 100644 --- a/src/client/stat-reducers.ts +++ b/src/client/stat-reducers.ts @@ -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 diff --git a/src/client/types/webpack.d.ts b/src/client/types/webpack.d.ts index f9b1bb1..9620931 100644 --- a/src/client/types/webpack.d.ts +++ b/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; } diff --git a/src/client/worker/download.ts b/src/client/worker/download.ts index 0c67c9c..94c5b8f 100644 --- a/src/client/worker/download.ts +++ b/src/client/worker/download.ts @@ -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 { }, }); } catch (e) { - console.log('failed', e.stack); return doAnalysis.failure({ url, statusCode: 500, diff --git a/webpack.production.js b/webpack.production.js new file mode 100644 index 0000000..30f6788 --- /dev/null +++ b/webpack.production.js @@ -0,0 +1,5 @@ +module.exports = { + ...require('./webpack.config'), + mode: 'production', + devtool: false, +};