Hot reload not working on webpack 5.91.0 and webpack-dev-server 5.0.4
Hot reload stopped working after upgrading from webpack 4.46.0 to 5.91.0.
Previously, hot reloading was supported using the @pmmmwh/react-refresh-webpack-plugin library.
webpack.config.js
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// paths
const SOURCE = path.resolve(__dirname, 'src');
const SINK = path.resolve(__dirname, 'build');
const PUBLIC = path.resolve(__dirname, 'public');
const MODULES = path.resolve(__dirname, 'node_modules');
const PFX = path.join(__dirname, '/certificates/development.pfx');
const APPICONS = path.join(__dirname, '/electron/icons');
// variables
const analyzeBundle = process.argv.indexOf('-a') >= 0;
const isProd = process.argv.indexOf('-p') >= 0 || process.env.NODE_ENV === 'production';
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 3001;
const HTML_OPTIONS = Object.assign(
{},
{
inject: true,
template: path.resolve(__dirname, './public/index.ejs'),
favicon: path.resolve(__dirname, './public/favicon.ico'),
},
isProd
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
);
module.exports = (env) => {
const defaultLanguage = env && env.DEFAULT_LANGUAGE ? env.DEFAULT_LANGUAGE : 'en';
const copyrightText = env && env.COPYRIGHT_TEXT ? env.COPYRIGHT_TEXT : '© Copyright placeholder';
let productName = env && env.PRODUCT_NAME;
let productEdition = env && env.PRODUCT_EDITION;
const isElectronMode = env.PACKAGE_MODE === 'electron';
if (!productEdition) {
productEdition = productName ? '' : 'Develop';
}
if (!productName) {
productName = 'ProductName';
}
let product = productName;
if (productEdition !== '') {
product += ` ${productEdition}`;
}
return {
mode: !isProd ? 'development' : 'production',
context: SOURCE,
entry: {
app: ['./index.tsx'],
vendor: ['react', 'react-dom', 'lodash', 'webrtc-adapter', '@mui/material', '@mui/icons-material', '@mui/styles', 'reflect-metadata'],
},
output: {
pathinfo: !isProd,
filename: 'static/js/[name].[hash:8].bundle.js',
chunkFilename: 'static/js/[name].[hash:8].bundle.js',
path: SINK,
publicPath: isElectronMode ? './' : '/',
clean: true,
},
optimization: {
moduleIds: 'named',
minimize: true,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
parallel: false,
include: SOURCE,
exclude: /node_modules/,
terserOptions: {
sourceMap: !isProd,
},
}),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
chunks: 'initial',
name: 'vendor',
test: 'vendor',
enforce: true,
},
},
},
},
target: !isProd ? 'web' : 'browserslist',
devtool: isProd ? 'source-map' : 'inline-cheap-module-source-map',
resolve: {
alias: {
'@mui/styles': path.join(MODULES, '/@mui/styles'),
'@App': SOURCE,
Actions: path.join(SOURCE, '/actions/'),
API: path.join(SOURCE, '/api/'),
Common: path.join(SOURCE, '/common/'),
Enums: path.join(SOURCE, '/enums/'),
Factories: path.join(SOURCE, '/factories/'),
Models: path.join(SOURCE, '/models/'),
Reducers: path.join(SOURCE, '/reducers/'),
Sagas: path.join(SOURCE, '/sagas/'),
Selectors: path.join(SOURCE, '/selectors/'),
Services: path.join(SOURCE, '/services/'),
View: path.join(SOURCE, '/view/'),
Translations: path.join(SOURCE, '/translations/'),
Store: path.join(SOURCE, '/store/'),
},
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
modules: [SOURCE, 'node_modules'],
fallback: {
url: false,
crypto: false,
},
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: MODULES,
use: [
{
loader: 'ts-loader',
options: {
compilerOptions: {
exclude: ['node_modules', 'src/__tests__'],
},
},
},
],
},
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
{
test: /\.(jpg|jpeg|png|woff|woff2|eot|ttf|svg|ogg|mp3|wav)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'static/media/[name].[ext]',
},
},
],
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: !isProd,
reloadAll: true,
},
},
'css-loader',
],
},
{
test: /\.(sa|sc)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: !isProd,
reloadAll: true,
},
},
'css-loader',
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'static/css/[name].[hash:8].css',
chunkFilename: 'static/css/[id].[hash:8].css',
}),
new HtmlWebpackPlugin(HTML_OPTIONS),
new CopyWebpackPlugin({
patterns: [
{
from: PUBLIC,
to: SINK,
globOptions: {
ignore: ['index.ejs', 'manifest.json'],
},
},
{
from: path.resolve(PUBLIC, 'manifest.json'),
to: path.resolve(SINK, 'manifest.json'),
transform(content) {
let manifest = JSON.parse(content);
manifest['name'] = `${product} Web Client`;
manifest['short_name'] = product;
return JSON.stringify(manifest, null, 2);
},
},
{
from: APPICONS,
to: path.join(SINK, '/icons'),
toType: 'dir',
},
],
}),
new WorkboxPlugin.InjectManifest({
swSrc: path.join(__dirname, '/serviceWorker/service-worker.js'),
maximumFileSizeToCacheInBytes: 50 * 1024 * 1024,
}),
new webpack.DefinePlugin({
'process.env.DEFAULT_LANGUAGE': JSON.stringify(defaultLanguage),
'process.env.COPYRIGHT_TEXT': JSON.stringify(copyrightText),
'process.env.PRODUCT': JSON.stringify(product),
}),
analyzeBundle && new BundleAnalyzerPlugin(),
!isProd && new webpack.HotModuleReplacementPlugin(),
!isProd && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
watchOptions: {
aggregateTimeout: 600,
poll: 3000,
ignored: [MODULES],
},
devServer: {
watchFiles: SOURCE,
liveReload: false,
hot: true,
static: {
watch: false,
staticOptions: {
contentBase: PUBLIC,
},
},
compress: true,
devMiddleware: {
publicPath: '/',
},
historyApiFallback: true,
host: HOST,
port: PORT,
server: {
type: 'https',
options: {
pfx: fs.readFileSync(PFX),
},
},
client: {
progress: true,
logging: 'none',
overlay: {
warnings: false,
errors: true,
},
},
},
};
};
package.json
{
"name": "product_name1.0.0",
"version": "1.0.0",
"private": true,
"main": "electron/main.js",
"productName": "ProductName",
"description": "ProductName Description",
"author": {
"name": "Company placeholder",
"email": "[email protected]"
},
"homepage": ".",
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.14",
"@mui/lab": "^5.0.0-alpha.137",
"@mui/material": "^5.15.14",
"@mui/styles": "^5.15.14",
"@mui/x-data-grid-pro": "^6.19.3",
"@mui/x-date-pickers": "^6.19.3",
"@mui/x-license-pro": "^6.10.2",
"@mui/x-tree-view": "^6.17.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@types/leaflet": "1.7.9",
"@types/leaflet-editable": "^1.2.4",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.2.0",
"@types/react-leaflet": "^3.0.0",
"@types/react-leaflet-markercluster": "^3.0.4",
"@types/resize-observer-browser": "0.1.7",
"again": "0.0.1",
"axios": "^1.6.8",
"class-validator": "0.11.0",
"classnames": "^2.5.1",
"cross-env": "7.0.3",
"dayjs": "^1.11.10",
"dnd-core": "^16.0.1",
"file-saver": "2.0.5",
"history": "4.10.1",
"inversify": "^6.0.2",
"inversify-inject-decorators": "3.1.0",
"leaflet": "1.7.1",
"leaflet-editable": "1.2.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-color": "^2.19.3",
"react-dnd": "10.0.2",
"react-dnd-html5-backend": "10.0.2",
"react-dnd-touch-backend": "10.0.2",
"react-dom": "^18.2.0",
"react-intl": "^6.6.4",
"react-leaflet": "^4.2.1",
"react-leaflet-editable": "^0.2.2",
"react-leaflet-markercluster": "^3.0.0-rc1",
"react-redux": "^7.1.3",
"react-refresh": "^0.14.0",
"react-resize-detector": "^7.1.2",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-virtualized": "^9.22.5",
"redux": "4.0.5",
"redux-devtools-extension": "^2.13.8",
"redux-first-history": "^5.2.0",
"redux-saga": "1.1.3",
"reflect-metadata": "^0.2.2",
"reselect": "^5.1.0",
"tss-react": "^4.9.6",
"typeface-roboto": "^1.1.13",
"validator": "^13.11.0",
"webrtc-adapter": "^9.0.1"
},
"scripts": {
"clean:all": "npm run clean:build && npm run clean:modules && npm run clean:electron",
"clean:build": "npx rimraf build",
"clean:modules": "npx rimraf package-lock.json node_modules",
"clean:electron": "npx rimraf dist",
"start": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack-dev-server --mode development --env PACKAGE_MODE=web --progress --color --open",
"start:no-open": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack-dev-server --mode development --env PACKAGE_MODE=electron --progress --color",
"prebuild": "npm run lint:fix",
"ci-build": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --env PACKAGE_MODE=web --display-error-details --display-cached",
"build": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --progress --env PACKAGE_MODE=web --colors --display-error-details --display-cached",
"build:electron": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --env PACKAGE_MODE=electron --progress --colors --display-error-details --display-cached",
"ci-build:electron": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --env PACKAGE_MODE=electron --colors --display-error-details --display-cached",
"build:analyze": "webpack --mode production --progress --colors --display-error-details --display-cached -a",
"test": "jest --detectOpenHandles --logHeapUsage",
"test:watch": "jest --detectOpenHandles --watch",
"test:prod": "npm run lint:fix && npm run test -- --no-cache",
"test:gui": "majestic --app",
"ci-test": "jest --detectOpenHandles --env=jsdom --collectCoverage",
"lint": "eslint \"src/**/*.{ts,tsx}\" --config .eslintrc.json",
"lint:fix": "npm run lint -- --fix",
"prettier:base": "prettier \"src/**/*.{ts,tsx}\"",
"prettier:check": "npm run prettier:base -- --list-different",
"prettier:write": "npm run prettier:base -- --write --list-different",
"electron": "electron .",
"electron:start": "concurrently \"npm run start:no-open\" \"wait-on https://localhost:3001 && electron . \"",
"electron:build-deb": "electron-builder build --linux --publish never",
"electron:build-win": "electron-builder build --win --publish never",
"electron:rebuild-win": "npm run build:electron && electron-builder build --win --publish never"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"post-commit": "git update-index --again"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"npm run prettier:write",
"npm run lint:fix",
"git add"
]
},
"devDependencies": {
"@babel/core": "7.24.3",
"@babel/preset-env": "7.24.3",
"@cfaester/enzyme-adapter-react-18": "^0.7.1",
"@testing-library/jest-dom": "5.1.1",
"@testing-library/react": "9.4.0",
"@types/classnames": "2.2.9",
"@types/enzyme": "3.10.5",
"@types/file-saver": "2.0.6",
"@types/google-maps": "3.2.1",
"@types/googlemaps": "3.39.2",
"@types/history": "4.7.5",
"@types/jest": "26.0.14",
"@types/jsdom": "12.2.4",
"@types/lodash": "4.14.149",
"@types/mocha": "10.0.5",
"@types/node": "^20.12.6",
"@types/react": "^17.0.67",
"@types/react-color": "3.0.1",
"@types/react-hot-loader": "4.1.1",
"@types/react-redux": "7.1.7",
"@types/react-resize-detector": "4.2.0",
"@types/react-router": "5.1.4",
"@types/react-router-dom": "5.1.3",
"@types/react-test-renderer": "^18.0.7",
"@types/react-virtualized": "9.21.8",
"@types/redux": "3.6.0",
"@types/redux-logger": "3.0.7",
"@types/redux-mock-store": "1.0.2",
"@types/webpack-env": "^1.18.4",
"@types/webrtc": "0.0.26",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"autoprefixer": "9.7.4",
"babel-core": "6.26.3",
"babel-preset-env": "1.7.0",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "8.2.1",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.1",
"dotenv": "8.2.0",
"electron": "26.3.0",
"electron-builder": "24.6.4",
"enzyme": "3.11.0",
"enzyme-to-json": "3.4.4",
"eslint": "7.32.0",
"eslint-config-prettier": "6.10.0",
"eslint-config-react": "1.1.7",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.3.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.0",
"husky": "4.2.3",
"identity-obj-proxy": "3.0.0",
"jest": "26.6.3",
"jest-dom": "4.0.0",
"jest-environment-jsdom": "25.1.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-enzyme": "7.1.2",
"jsdom": "16.1.0",
"lint-staged": "10.0.7",
"majestic": "1.6.2",
"mini-css-extract-plugin": "0.9.0",
"mock-browser": "0.92.14",
"mock-socket": "9.0.3",
"npm-force-resolutions": "0.0.3",
"npm-run-all": "4.1.5",
"postcss-loader": "^8.1.1",
"prettier": "^1.19.1",
"react-dnd-test-backend": "10.0.2",
"react-test-renderer": "^18.2.0",
"react-testing-library": "8.0.1",
"redux-mock-store": "1.5.4",
"redux-saga-test-plan": "4.0.0-rc.3",
"rimraf": "5.0.5",
"sass-loader": "^14.1.1",
"source-map-loader": "^5.0.0",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.10",
"testcafe": "1.8.1",
"ts-jest": "26.5.4",
"ts-loader": "^9.5.1",
"ts-node": "10.6.0",
"typescript": "^5.4.4",
"wait-on": "7.0.1",
"web-audio-test-api": "0.5.2",
"webpack": "^5.91.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-node-externals": "^3.0.0",
"workbox-webpack-plugin": "^7.0.0"
},
"build": {
"appId": "",
"productName": "Product name",
"copyright": "2023 Company placeholder",
"win": {
"icon": "/build/icons/appIcon-256.png",
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
]
},
"linux": {
"target": [
"deb",
"rpm"
],
"icon": "build/icons"
},
"deb": {},
"rpm": {},
"files": [
"build",
"electron",
"!node_modules"
],
"extraResources": [
{
"from": "build/static/media",
"to": "static/media"
}
],
"extends": null
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
index.tsx
import { createBrowserHistory } from 'history';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import 'typeface-roboto';
import 'react-virtualized/styles.css';
import 'leaflet/dist/leaflet.css';
import './index.css';
import { ElectronApiInit } from './electron/ElectronApi';
import { loadRootState } from './store/AppStateStore/AppStateStore';
import configureStore from './store/configureStore';
import { IRootState } from './store/IRootState';
import App from './view/App';
const state: IRootState = loadRootState();
const history = createBrowserHistory();
const store = configureStore(history, state);
ElectronApiInit();
const render = (): void => {
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Provider store={store}>
<App history={history} />
</Provider>
);
};
if (module.hot !== undefined) {
module.hot.accept('./view/App', render);
}
service-worker.js
workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
workbox.routing.registerNavigationRoute('/index.html');
workbox.precaching.precacheAndRoute(
self.__precacheManifest.concat([
'https://fonts.googleapis.com/css?family=Source+Serif+Pro:400,700',
'https://unpkg.com/[email protected]/umd/react.production.min.js',
'https://unpkg.com/[email protected]/umd/react-dom.production.min.js',
'https://unpkg.com/[email protected]/umd/react-router-dom.min.js',
])
);
By the way, when building the project, a warning appears:
WARNING in InjectManifest has been called multiple times, perhaps due to running webpack in --watch mode. The precache manifest generated after the first call may be inaccurate! Please see https://github.com/GoogleChrome/workbox/issues/1790 for more information.
Maybe this will somehow affect the hot reload?