I am working on a project that involves React server-side rendering (SSR) with webpack and SWC as the transpiler. I am aiming to implement auto-refresh functionality, but it’s not working as expected, and I can’t pinpoint the issue.
Below is my configuration and setup:
webpack.config.ts
import path from "node:path";
import { readFileSync } from "node:fs";
import webpack from "webpack";
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
import { WebpackManifestPlugin } from "webpack-manifest-plugin";
import webpackDevMiddleware from "webpack-dev-middleware";
import webpackHotMiddleware from "@gatsbyjs/webpack-hot-middleware";
import type { Request, Response, NextFunction } from "express";
const swcConfig = JSON.parse(
readFileSync(path.resolve(process.cwd(), "swc.config.json"), "utf-8")
);
const isDevMode = process.env.NODE_ENV === "development";
const webpackSwcConfig = {
...swcConfig,
jsc: {
...swcConfig.jsc,
transform: {
react: {
runtime: "automatic",
// Enable fast refresh in dev
refresh: isDevMode,
},
},
},
module: {
// Set module type to get code splitting.
// Code splitting does not work for type: "commonjs".
type: "nodenext",
},
};
const contenthash = isDevMode ? "" : ".[contenthash:8]";
const webpackConfig: webpack.Configuration = {
mode: isDevMode ? "development" : "production",
entry: {
main: ["./react/index.tsx"],
},
output: {
clean: true,
path: path.resolve(__dirname, "./public"),
filename: `[name]${contenthash}.js`,
chunkFilename: `[name].chunk${contenthash}.js`,
publicPath: "/",
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
},
module: {
rules: [
{
test: /.(js|ts|tsx)$/,
exclude: /node_modules/,
include: [path.join(__dirname, "react")], // only bundle files in this directory
use: {
loader: "swc-loader",
options: webpackSwcConfig,
},
},
],
},
plugins: [
new WebpackManifestPlugin({
fileName: "webpack-stats.json",
writeToFileEmit: true,
isInitial: true,
}),
],
optimization: {
moduleIds: "deterministic", // Now, despite any new local dependencies, our vendor hash should stay consistent between builds
runtimeChunk: true, // see https://webpack.js.org/guides/build-performance/#minimal-entry-chunk
},
};
if (isDevMode) {
webpackConfig.entry = {
main: ["@gatsbyjs/webpack-hot-middleware/client", "./react/index.tsx"],
};
webpackConfig.output = {
...webpackConfig.output,
hotUpdateChunkFilename: "[id].hot-update.js",
hotUpdateMainFilename: "[runtime].hot-update.json",
};
webpackConfig.plugins = [
...(webpackConfig.plugins || []),
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin({
overlay: {
sockIntegration: "whm",
},
}),
];
webpackConfig.cache = {
// https://webpack.js.org/configuration/other-options/#cache
type: "filesystem",
cacheDirectory: path.resolve(__dirname, ".tmp"),
name: "dev-react-cache",
};
}
const compiler = webpack(webpackConfig);
if (isDevMode) {
const dirName = path.resolve(__dirname, "./react/");
compiler.hooks.afterEmit.tap("cleanup-the-require-cache", () => {
// After webpack rebuild, clear the files from the require cache,
// so that next server side render will be in sync
Object.keys(require.cache)
.filter((key) => key.includes(dirName))
.forEach((key) => delete require.cache[key]);
});
}
export const devMiddleware = isDevMode
? webpackDevMiddleware(compiler, {
serverSideRender: true,
publicPath: webpackConfig.output?.publicPath || "/",
})
: (req: Request, res: Response, next: NextFunction) => next();
export const hotMiddleware = isDevMode
? webpackHotMiddleware(compiler, {
log: false,
path: "/__webpack_hmr",
heartbeat: 10 * 1000,
})
: (req: Request, res: Response, next: NextFunction) => next();
export default webpackConfig;
swc.config.json
{
"jsc": {
"externalHelpers": true,
"parser": {
"syntax": "typescript",
"tsx": true,
"dynamicImport": true
},
"transform": {
"react": {
"runtime": "automatic",
"refresh": true
}
},
"keepClassNames": true,
"experimental": {
"plugins": [
[
"@swc/plugin-styled-components",
{
"displayName": true,
"ssr": true
}
]
]
},
"baseUrl": ".",
"paths": {
"@pages/*": ["pages/*"],
"@components/*": ["components/*"]
}
},
"module": {
"type": "commonjs"
},
"sourceMaps": true
}
dev/server.js
const path = require("node:path");
require("dotenv-defaults/config");
require("@swc/register")({
extensions: [".js", ".ts", ".tsx"],
configFile: path.resolve(__dirname, "../swc.config.json"),
});
require("../server").default;
app.ts
import express from "express";
import path from "path";
import { getAllRoutes } from "./utils/routes.utils";
import renderToStream from "./stream";
import devMiddleware from "./devMiddleware";
const PORT = 3000;
const app = express();
if (process.env.NODE_ENV === "development") {
devMiddleware(app);
}
app.use(express.json());
const setup = async () => {
const serverRoutes = await getAllRoutes("react/pages");
console.log("Server routes", serverRoutes);
app.use(express.static(path.join(__dirname, "public")));
app.get("*", (req, res) => renderToStream(req, res, serverRoutes));
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
};
export default setup;
import { Express } from "express";
import webpack from "webpack";
import webpackDevMiddleware from "webpack-dev-middleware";
import webpackHotMiddleware from "webpack-hot-middleware";
import path from "path";
import config from "../webpack.config";
const isDev = process.env.NODE_ENV === "development";
const compiler = webpack(config);
const dirName = path.resolve(__dirname, "../react/");
if (isDev) {
compiler.hooks.afterEmit.tap("cleanup-the-require-cache", () => {
Object.keys(require.cache)
.filter((key) => key.startsWith(dirName))
.forEach((key) => delete require.cache[key]);
});
}
const addDevMiddleware = (app: Express): void => {
if (isDev) {
app.use(
webpackDevMiddleware(compiler, {
serverSideRender: true,
publicPath: config.output?.publicPath || "/",
})
);
app.use(
webpackHotMiddleware(compiler, {
log: console.log,
path: "/__webpack_hmr",
heartbeat: 10 * 1000,
})
);
}
};
export default addDevMiddleware;
Question
Despite setting up webpack and SWC for hot reloading and refreshing in development mode, changes do not reflect in real-time on the browser as expected. Has anyone encountered this issue before or can identify misconfigurations in my setup that might be causing this problem?
Thank you in advance for your help!