feat: not so wip any more
This commit is contained in:
Родитель
41041d0b3d
Коммит
4ed7bd27be
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
...require('./webpack.config'),
|
||||
mode: 'production',
|
||||
devtool: false,
|
||||
};
|
Загрузка…
Ссылка в новой задаче