(
56 |
57 | {text}
58 |
59 | )
60 | },
61 | {
62 | title: 'Run',
63 | dataIndex: 'name',
64 | key: 'name',
65 | render: (text, record) => (
66 |
67 | {text}
68 |
69 | )
70 | },
71 | {
72 | title: 'Pages',
73 | dataIndex: 'pages',
74 | key: 'pages',
75 | render: (text, record) => record.pages.length
76 | },
77 | {
78 | title: 'Actions',
79 | key: 'actions',
80 | render: (text, record) => (
81 |
82 | {
85 | this.deleteRun(record.id)
86 | }}
87 | >
88 |
89 |
90 |
91 | )
92 | },
93 | ]}
94 | dataSource={runs}
95 | rowKey="id"
96 | pagination={false}
97 | // scroll={{ y: '100vh' }}
98 | scroll={{ y: 'max-content' }}
99 | />
100 | ) : (
101 | {loading}
102 | )}
103 |
104 |
105 | )
106 | }
107 | }
108 |
109 | RunList.propTypes = {
110 | listRunsAction: PropTypes.func.isRequired,
111 | runs: PropTypes.array.isRequired // eslint-disable-line
112 | }
113 |
114 | const mapStateToProps = (state) => ({
115 | runs: state.runs
116 | })
117 |
118 | const mapDispatchToProps = (dispatch) => ({
119 | listRunsAction: listRuns(dispatch)
120 | })
121 |
122 | export default connect(mapStateToProps, mapDispatchToProps)(RunList)
123 |
--------------------------------------------------------------------------------
/src/screens/run_result.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { Redirect } from 'react-router-dom'
4 | import PropTypes from 'prop-types'
5 | import {
6 | Table,
7 | Input,
8 | Button,
9 | Menu,
10 | Dropdown,
11 | Icon,
12 | Modal,
13 | Tooltip,
14 | } from 'antd'
15 | import Lightbox from 'react-image-lightbox'
16 |
17 | import { listRuns } from '../actions'
18 | import Layout from '../layout'
19 |
20 | // const path = window.require('path')
21 | const { remote } = window.require('electron')
22 | const { shell } = remote
23 | const storage = remote.require('./storage')
24 |
25 | const fs = remote.require('fs')
26 |
27 | const getImageBase64Data = (filepath) => {
28 | if (typeof filepath !== 'string') return
29 | if (!fs.existsSync(filepath)) return
30 | const imgBase64 = fs.readFileSync(filepath).toString('base64')
31 | return 'data:image/png;base64,' + imgBase64
32 | }
33 |
34 | const loading =
35 |
36 | class RunResult extends Component {
37 | constructor(props) {
38 | super(props)
39 |
40 | this.state = {
41 | redirect: false,
42 | redirectTo: null,
43 | run: null,
44 | columns: [],
45 | lightboxImages: [],
46 | lightboxImageIndex: 0,
47 | lightboxIsVisible: false,
48 | renamedRunName: '',
49 | isInRenameRunMode: false,
50 | }
51 | }
52 |
53 | componentDidMount() {
54 | const { listRunsAction } = this.props
55 | listRunsAction()
56 | this.getRun()
57 | }
58 |
59 | componentDidUpdate(previousProps) {
60 | const { runs } = this.props
61 | if (runs !== previousProps.runs) {
62 | this.getRun()
63 | }
64 | }
65 |
66 | getRun() {
67 | const { runs } = this.props
68 | const { runId } = this.props.match.params // eslint-disable-line
69 |
70 | // const runs = listRunsAction()
71 | let runObj = null
72 | runs.forEach((run) => {
73 | if (parseInt(run.id, 10) === parseInt(runId, 10)) {
74 | runObj = run
75 | }
76 | })
77 |
78 | // Get base64 of the images read from the FS
79 | const lightboxImages = []
80 | if (runObj && runObj.pages) {
81 | runObj.pages = runObj.pages.map((page) => {
82 | const updatedPage = { ...page }
83 | Object.keys(page.screenshots).forEach((resolution) => {
84 | updatedPage.screenshots[resolution].imageb64 = getImageBase64Data(page.screenshots[resolution].file)
85 | lightboxImages.push(updatedPage.screenshots[resolution].imageb64)
86 | })
87 | return updatedPage
88 | })
89 | }
90 |
91 | this.setState({
92 | run: runObj,
93 | lightboxImages
94 | })
95 | this.setTableColumns(runObj)
96 | }
97 |
98 | // eslint-disable-next-line
99 | getResolutionColumnMeta(screenshotResName) {
100 | return {
101 | title: screenshotResName.substr(0, 1).toUpperCase() + screenshotResName.substr(1),
102 | dataIndex: 'screenshots.' + screenshotResName,
103 | key: 'screenshots.' + screenshotResName,
104 | render: (text, record) => (
105 |
106 | {record.screenshots[screenshotResName].status === 'success' && (
107 |
108 | {record.screenshots[screenshotResName].imageb64 ? (
109 |
110 | {/* eslint-disable-next-line */}
111 |
{
116 | // eslint-disable-next-line
117 | this.openLightboxWithScreenshot(record.screenshots[screenshotResName].imageb64)
118 | }}
119 | />
120 |
121 | ) : (
122 |
123 | {/* eslint-disable-next-line */}
124 |
125 |
126 |
127 |
128 | )}
129 |
130 | )}
131 | {record.screenshots[screenshotResName].status === 'pending' && (
132 | loading
133 | )}
134 | {record.screenshots[screenshotResName].status === 'failed' && (
135 |
136 | )}
137 |
138 | )
139 | }
140 | }
141 |
142 | setTableColumns(run) {
143 | if (!run) return
144 |
145 | const columns = [
146 | {
147 | title: 'URL',
148 | dataIndex: 'url',
149 | key: 'url',
150 | render: (text) => (
151 |
152 | {text}
153 | {/* open the link in OS browser from electron: http://bit.ly/38gOO9g */}
154 |
155 | ),
156 | },
157 | ]
158 |
159 | if (run.pages[0] && run.pages[0].screenshots) {
160 | if (run.pages[0].screenshots.desktopLarge) columns.push(this.getResolutionColumnMeta('desktopLarge'))
161 | if (run.pages[0].screenshots.desktop) columns.push(this.getResolutionColumnMeta('desktop'))
162 | if (run.pages[0].screenshots.tabletLandscape) columns.push(this.getResolutionColumnMeta('tabletLandscape'))
163 | if (run.pages[0].screenshots.tabletPortrait) columns.push(this.getResolutionColumnMeta('tabletPortrait'))
164 | if (run.pages[0].screenshots.mobile) columns.push(this.getResolutionColumnMeta('mobile'))
165 | }
166 |
167 | console.log('--> run', run)
168 |
169 | if (run.options && run.options.lighthouse) {
170 | columns.push({
171 | title: 'Performance',
172 | dataIndex: 'lhr-performance',
173 | key: 'lhr-performance',
174 | render: (text, record) => (
175 |
176 | {(record.lhrScores && record.lhrScores.performance)
177 | ? record.lhrScores.performance * 100 : loading}
178 |
179 | )
180 | })
181 | columns.push({
182 | title: 'SEO',
183 | dataIndex: 'lhr-seo',
184 | key: 'lhr-seo',
185 | render: (text, record) => (
186 |
187 | {(record.lhrScores && record.lhrScores.seo)
188 | ? record.lhrScores.seo * 100 : loading}
189 |
190 | )
191 | })
192 | columns.push({
193 | title: 'Accessibility',
194 | dataIndex: 'lhr-accessibility',
195 | key: 'lhr-accessibility',
196 | render: (text, record) => (
197 |
198 | {(record.lhrScores && record.lhrScores.accessibility)
199 | ? record.lhrScores.accessibility * 100 : loading}
200 |
201 | )
202 | })
203 | columns.push({
204 | title: 'LH Report',
205 | dataIndex: 'lhr-report',
206 | key: 'lhr-report',
207 | render: (text, record) => (record.lhrHtmlPath ? (
208 |
209 | {
212 | if (record.lhrHtmlPath) shell.showItemInFolder(record.lhrHtmlPath)
213 | }}
214 | >
215 |
216 |
217 |
218 | ) : loading)
219 | })
220 | }
221 |
222 | // columns.push({
223 | // key: 'actions',
224 | // render: () => (
225 | //
226 | //
227 | //
228 | //
229 | //
230 | // )
231 | // })
232 |
233 | // console.log('--> columns', columns)
234 |
235 | this.setState({ columns })
236 | }
237 |
238 | openLightboxWithScreenshot(b64) {
239 | const { lightboxImages } = this.state
240 | const lightboxImageIndex = lightboxImages.indexOf(b64)
241 | this.setState({
242 | lightboxIsVisible: true,
243 | lightboxImageIndex
244 | })
245 | }
246 |
247 | deleteRun() {
248 | const { run } = this.state
249 | const { listRunsAction } = this.props
250 |
251 | Modal.confirm({
252 | content: 'Are you sure you want to delete this run?',
253 | okText: 'Delete',
254 | okType: 'danger',
255 | onOk: () => {
256 | storage.deleteRun(run.id)
257 | listRunsAction()
258 | this.setState({
259 | redirect: true,
260 | redirectTo: '/',
261 | })
262 | }
263 | })
264 | }
265 |
266 | renameRun() {
267 | const { listRunsAction } = this.props
268 | const { run, renamedRunName } = this.state
269 | const updatedRun = { ...run }
270 | updatedRun.name = renamedRunName
271 | storage.updateRun(run.id, updatedRun)
272 | this.setState({
273 | run: updatedRun,
274 | isInRenameRunMode: false
275 | })
276 | listRunsAction()
277 | }
278 |
279 | openRunFolder() {
280 | const { run } = this.state
281 | if (run.pages && run.pages[0] && run.pages[0].screenshots) {
282 | const aScreenshotFile = Object.values(run.pages[0].screenshots)[0].file
283 | // const runFolderPath = path.dirname(aScreenshotFile)
284 | shell.showItemInFolder(aScreenshotFile)
285 | }
286 | }
287 |
288 | render() {
289 | const {
290 | redirect,
291 | redirectTo,
292 | run,
293 | columns,
294 | lightboxImages,
295 | lightboxImageIndex,
296 | lightboxIsVisible,
297 | isInRenameRunMode
298 | } = this.state
299 |
300 | if (redirect) {
301 | return (
302 |
303 | )
304 | }
305 |
306 | return (
307 |
308 |
309 | {run && (
310 |
318 |
327 | {!isInRenameRunMode && (
328 |
329 | {run.name}
330 | {
334 | this.setState({ isInRenameRunMode: true })
335 | }}
336 | >
337 |
338 |
339 |
340 | )}
341 | {isInRenameRunMode && (
342 |
343 |
344 | { this.setState({ renamedRunName: e.target.value }) }}
347 | style={{ width: '50%' }}
348 | />
349 | Rename
350 | {
352 | this.setState({ isInRenameRunMode: false })
353 | }}
354 | >
355 |
356 |
357 |
358 |
359 | )}
360 |
361 |
371 |
376 | Open Screenshots Folder
377 |
378 | {/*
379 |
380 | */}
381 |
385 | {/* Export */}
386 |
389 | Delete
390 |
391 |
392 | )}
393 | >
394 |
395 |
396 |
397 |
398 |
399 |
400 | )}
401 | {run && run.pages ? (
402 |
410 | ) : (
411 | Loading...
412 | )}
413 |
414 | {lightboxIsVisible && (
415 |
this.setState({ lightboxIsVisible: false })}
421 | onMovePrevRequest={() => {
422 | this.setState({
423 | // eslint-disable-next-line
424 | lightboxImageIndex: (lightboxImageIndex + lightboxImages.length - 1) % lightboxImages.length,
425 | })
426 | }}
427 | onMoveNextRequest={() => {
428 | this.setState({
429 | lightboxImageIndex: (lightboxImageIndex + 1) % lightboxImages.length,
430 | })
431 | }}
432 | />
433 | )}
434 |
435 | )
436 | }
437 | }
438 |
439 | RunResult.propTypes = {
440 | listRunsAction: PropTypes.func.isRequired,
441 | runs: PropTypes.array.isRequired // eslint-disable-line
442 | }
443 |
444 | const mapStateToProps = (state) => ({
445 | runs: state.runs
446 | })
447 |
448 | const mapDispatchToProps = (dispatch) => ({
449 | listRunsAction: listRuns(dispatch)
450 | })
451 |
452 | export default connect(mapStateToProps, mapDispatchToProps)(RunResult)
453 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 |
4 | import rootReducer from './reducers'
5 |
6 | const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // eslint-disable-line
7 |
8 | export default createStore(
9 | rootReducer,
10 | storeEnhancers(applyMiddleware(thunk))
11 | )
12 |
--------------------------------------------------------------------------------
/src/styles/ant.vars.scss:
--------------------------------------------------------------------------------
1 | $primary-color: #1890ff; // primary color for all components
2 | $link-color: #1890ff; // link color
3 | $success-color: #52c41a; // success state color
4 | $warning-color: #faad14; // warning state color
5 | $error-color: #f5222d; // error state color
6 | $font-size-base: 14px; // major text font size
7 | $heading-color: rgba(0, 0, 0, 0.85); // heading text color
8 | $text-color: rgba(0, 0, 0, 0.65); // major text color
9 | $text-color-secondary: rgba(0, 0, 0, 0.45); // secondary text color
10 | $disabled-color: rgba(0, 0, 0, 0.25); // disable state color
11 | $border-radius-base: 5px; // major border radius
12 | $border-color-base: #d9d9d9; // major border color
13 | $box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // major shadow for layers
14 |
--------------------------------------------------------------------------------
/src/styles/app.global.scss:
--------------------------------------------------------------------------------
1 | @import "./ant.vars.scss";
2 |
3 | body {
4 | background-color: #ffffff;
5 | }
6 |
7 | .ant-menu-inline-collapsed {
8 | width: 50px;
9 | }
10 |
11 | .ant-menu-inline-collapsed > .ant-menu-item,
12 | .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title {
13 | padding: 0 18px !important;
14 | }
15 |
16 | .ant-layout-sider {
17 | .ant-layout-sider-trigger {
18 | border-top: 1px solid #efefef;
19 | }
20 | }
21 |
22 | .darkmode-layer, .darkmode-toggle {
23 | z-index: 10000;
24 | }
25 |
26 | .darkmode--activated {
27 | // ignore dark mode for elements
28 | img.screenshot,
29 | img.screenshot-tarcker-icon {
30 | mix-blend-mode: difference;
31 | }
32 |
33 | // force dark mode
34 | .ant-btn.ant-btn-default,
35 | .ant-layout-sider .anticon,
36 | .ant-menu-submenu-popup .ant-menu-sub {
37 | mix-blend-mode: normal;
38 | }
39 |
40 | .ant-layout-sider,
41 | .ant-layout-sider-trigger,
42 | .ant-layout-sider .ant-menu,
43 | .ant-menu-submenu-popup .ant-menu-sub {
44 | background-color: #eeeeee;
45 | }
46 |
47 | .ReactModalPortal {
48 | position: absolute;
49 | z-index: 100000000;
50 | }
51 | }
52 |
53 | .screenshotContainer {
54 | display: inline-block;
55 | max-height: 150px;
56 | overflow: hidden;
57 | box-shadow: rgba(0,0,0,0.2) 0 1px 2px;
58 | transition: all 0.3s ease-in-out;
59 |
60 | &:hover {
61 | box-shadow: rgba(0,0,0,0.2) 2px 10px 20px;
62 | }
63 | }
64 |
65 | .screenshot {
66 | max-width: 100px;
67 | }
68 |
69 | .aboutPage {
70 | main.ant-layout-content {
71 | text-align: center;
72 | }
73 |
74 | .appicon {
75 | padding: 50px;
76 | }
77 |
78 | .credits {
79 | margin-bottom: 50px;
80 |
81 | .ant-col.wrapper-col {
82 | text-align: left;
83 | margin-top: 30px;
84 | padding-top: 30px;
85 | border-top: 1px solid #efefef;
86 | }
87 | }
88 | }
89 |
90 | .pageWithGradientBg main.ant-layout-content {
91 | background: linear-gradient(0deg, rgba(255,255,255,1) 30%, rgba(215,243,255,1) 100%) !important;
92 | }
93 |
94 | .support-bar {
95 | max-width: 700px;
96 | margin: 0 auto;
97 | border-top: 1px solid #efefef;
98 | padding: 40px;
99 | text-align: center;
100 | font-size: 0.9em;
101 | }
102 |
--------------------------------------------------------------------------------
/storage.js:
--------------------------------------------------------------------------------
1 | const Store = require('electron-store')
2 |
3 | const store = new Store()
4 |
5 | const KEYS = {
6 | RUNS: 'runs',
7 | RUNS_NEXT_ID: 'runs_next_id',
8 | LAST_RUN_OBJ: 'last_run_obj',
9 | }
10 |
11 | const getNextRunId = () => (parseInt(store.get(KEYS.RUNS_NEXT_ID), 10) || 1)
12 |
13 | const saveRun = (runData) => {
14 | const runs = store.get(KEYS.RUNS) || []
15 | const newRunId = getNextRunId()
16 | const newRunObj = {
17 | id: newRunId,
18 | progress: 0,
19 | ...runData
20 | }
21 | runs.push(newRunObj)
22 | store.set(KEYS.RUNS, runs)
23 | store.set(KEYS.RUNS_NEXT_ID, newRunId + 1)
24 | return newRunObj
25 | }
26 |
27 | const listRuns = () => store.get(KEYS.RUNS)
28 |
29 | const getRun = (runId) => {
30 | const runs = listRuns()
31 | let foundRun = null
32 | runs.forEach((run) => {
33 | if (parseInt(run.id, 10) === parseInt(runId, 10)) {
34 | foundRun = run
35 | }
36 | })
37 | return foundRun
38 | }
39 |
40 | const updateRun = (runId, updatedData) => {
41 | const runs = listRuns()
42 | const updatedRuns = []
43 | runs.forEach((run) => {
44 | updatedRuns.push({
45 | ...run,
46 | ...(parseInt(run.id, 10) === parseInt(runId, 10) ? updatedData : {})
47 | })
48 | })
49 | store.set(KEYS.RUNS, updatedRuns)
50 | return true
51 | }
52 |
53 | const deleteRun = (runId) => {
54 | const runs = listRuns()
55 | const updatedRuns = []
56 | runs.forEach((run) => {
57 | if (parseInt(run.id, 10) !== parseInt(runId, 10)) {
58 | updatedRuns.push(run)
59 | }
60 | })
61 | store.set(KEYS.RUNS, updatedRuns)
62 | return true
63 | }
64 |
65 | const clearRuns = () => {
66 | store.set(KEYS.RUNS, null)
67 | store.set(KEYS.RUNS_NEXT_ID, null)
68 | }
69 |
70 | const getLastRunObj = () => store.get(KEYS.LAST_RUN_OBJ)
71 | const saveLastRunObj = (runData) => store.set(KEYS.LAST_RUN_OBJ, runData)
72 |
73 | module.exports = {
74 | getNextRunId,
75 | saveRun,
76 | listRuns,
77 | getRun,
78 | updateRun,
79 | deleteRun,
80 | clearRuns,
81 | getLastRunObj,
82 | saveLastRunObj
83 | }
84 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import { spawn } from 'child_process'
2 | import path from 'path'
3 | import webpack from 'webpack'
4 | import merge from 'webpack-merge'
5 | import HtmlWebpackPlugin from 'html-webpack-plugin'
6 | import postcssPresetEnv from 'postcss-preset-env'
7 | import AntdScssThemePlugin from 'antd-scss-theme-plugin'
8 | import BabiliPlugin from 'babili-webpack-plugin'
9 | import TerserPlugin from 'terser-webpack-plugin'
10 | import UglifyJsPlugin from 'uglifyjs-webpack-plugin'
11 | import BrotliPlugin from 'brotli-webpack-plugin'
12 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'
13 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
14 |
15 | const host = '0.0.0.0'
16 | const port = 3100
17 | const src = path.resolve(__dirname, 'src')
18 |
19 | const isDev = process.env.NODE_ENV === 'development'
20 |
21 | const cssModuleLoader = {
22 | loader: 'css-loader',
23 | options: {
24 | importLoaders: 2,
25 | modules: true,
26 | camelCase: true,
27 | sourceMap: isDev,
28 | localIdentName: isDev
29 | ? '[folder]__[name]__[local]__[hash:base64:5]'
30 | : '[hash:base64:5]'
31 | }
32 | }
33 |
34 | const cssLoader = {
35 | loader: 'css-loader',
36 | options: {
37 | importLoaders: 2,
38 | modules: false,
39 | sourceMap: isDev
40 | }
41 | }
42 |
43 | const postCssLoader = {
44 | loader: 'postcss-loader',
45 | options: {
46 | ident: 'postcss',
47 | sourceMap: isDev,
48 | plugins: () => [postcssPresetEnv()]
49 | }
50 | }
51 |
52 | const sassLoader = {
53 | loader: 'sass-loader',
54 | options: {
55 | sourceMap: isDev
56 | }
57 | }
58 |
59 | const lessLoader = AntdScssThemePlugin.themify({
60 | loader: 'less-loader',
61 | options: {
62 | sourceMap: isDev,
63 | javascriptEnabled: true
64 | }
65 | })
66 |
67 | const sassHotLoader = {
68 | loader: 'css-hot-loader'
69 | }
70 |
71 | const sassHotModuleLoader = {
72 | loader: 'css-hot-loader',
73 | options: {
74 | cssModule: true
75 | }
76 | }
77 |
78 | const assetsLoader = {
79 | loader: 'file-loader?name=[name]__[hash:base64:5].[ext]'
80 | }
81 |
82 | const babelLoader = [
83 | {
84 | loader: 'thread-loader'
85 | },
86 | {
87 | loader: 'babel-loader',
88 | options: {
89 | cacheDirectory: true
90 | }
91 | }
92 | ]
93 |
94 | const babelDevLoader = babelLoader.concat([
95 | 'react-hot-loader/webpack',
96 | 'eslint-loader'
97 | ])
98 |
99 | const config = {
100 | target: 'electron-renderer',
101 | base: {
102 | module: {
103 | rules: [
104 | {
105 | test: /\.(js|jsx)$/,
106 | use: isDev ? babelDevLoader : babelLoader,
107 | exclude: /node_modules/
108 | },
109 | {
110 | test: /\.(jpe?g|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$/,
111 | use: [assetsLoader]
112 | },
113 | {
114 | test: /\.global|vars\.scss$/,
115 | use: [
116 | sassHotLoader,
117 | MiniCssExtractPlugin.loader,
118 | cssLoader,
119 | postCssLoader,
120 | sassLoader
121 | ]
122 | },
123 | {
124 | test: /\.scss$/,
125 | exclude: /\.global|vars\.scss$/,
126 | use: [
127 | sassHotModuleLoader,
128 | MiniCssExtractPlugin.loader,
129 | cssModuleLoader,
130 | postCssLoader,
131 | sassLoader
132 | ]
133 | },
134 | {
135 | test: /\.(less|css)$/,
136 | use: [
137 | sassHotLoader,
138 | MiniCssExtractPlugin.loader,
139 | cssLoader,
140 | lessLoader
141 | ]
142 | }
143 | ]
144 | },
145 | plugins: [
146 | new webpack.DefinePlugin({
147 | NODE_ENV: process.env.NODE_ENV
148 | }),
149 | new AntdScssThemePlugin(
150 | path.join(__dirname, 'src', 'styles/ant.vars.scss')
151 | ),
152 | new MiniCssExtractPlugin({
153 | filename: isDev ? '[name].css' : '[name].[chunkhash].css',
154 | chunkFilename: isDev ? '[id].css' : '[name].[chunkhash].css',
155 | reload: false
156 | }),
157 | new HtmlWebpackPlugin({
158 | template: 'public/index.html',
159 | minify: {
160 | collapseWhitespace: !isDev
161 | }
162 | })
163 | ]
164 | },
165 | development: {
166 | mode: 'development',
167 | plugins: [new webpack.HotModuleReplacementPlugin()],
168 | entry: [
169 | 'react-hot-loader/patch',
170 | 'webpack/hot/only-dev-server',
171 | src
172 | ],
173 | devtool: 'cheap-module-source-map',
174 | cache: true,
175 | devServer: {
176 | host,
177 | port,
178 | hot: true,
179 | contentBase: 'public',
180 | compress: true,
181 | inline: true,
182 | lazy: false,
183 | // stats: 'errors-only',
184 | historyApiFallback: {
185 | verbose: true,
186 | disableDotRule: false
187 | },
188 | headers: { 'Access-Control-Allow-Origin': '*' },
189 | stats: {
190 | colors: true,
191 | chunks: false,
192 | children: false
193 | },
194 | before() {
195 | spawn('electron', ['.'], {
196 | shell: true,
197 | env: process.env,
198 | stdio: 'inherit'
199 | })
200 | .on('close', () => process.exit(0))
201 | .on('error', (spawnError) => console.error(spawnError))
202 | }
203 | },
204 | optimization: {
205 | namedModules: true
206 | },
207 | resolve: {
208 | extensions: ['.js', '.jsx', '.json'],
209 | modules: [].concat(src, ['node_modules']),
210 | alias: {
211 | 'react-dom': '@hot-loader/react-dom'
212 | }
213 | }
214 | },
215 | production: {
216 | mode: 'production',
217 | entry: {
218 | app: src
219 | },
220 | plugins: [
221 | new BrotliPlugin({
222 | asset: '[path].br[query]',
223 | test: /\.(js|css|html|svg)$/,
224 | threshold: 10240,
225 | minRatio: 0.8
226 | })
227 | ],
228 | output: {
229 | path: path.join(__dirname, '/dist'),
230 | filename: '[name].[chunkhash].js'
231 | },
232 | optimization: {
233 | minimizer: [
234 | new UglifyJsPlugin(),
235 | new TerserPlugin(),
236 | new BabiliPlugin(),
237 | new OptimizeCSSAssetsPlugin()
238 | ],
239 | splitChunks: {
240 | cacheGroups: {
241 | commons: {
242 | test: /[\\/]node_modules[\\/]/,
243 | name: 'vendors',
244 | chunks: 'all'
245 | }
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
252 | export default merge(config.base, config[process.env.NODE_ENV])
253 |
--------------------------------------------------------------------------------