From 3745190fbc98011775bd31eea8f62d2cd4410ccf Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Tue, 13 Jan 2026 16:32:43 +0530 Subject: [PATCH 1/8] expose gc --- src/scripts/serve.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/scripts/serve.js b/src/scripts/serve.js index 9e2b8a18..76bb3b30 100644 --- a/src/scripts/serve.js +++ b/src/scripts/serve.js @@ -13,7 +13,9 @@ function serve() { const argumentsObject = arrayToObject(commandLineArguments) const dirname = path.resolve(__dirname, "../../") - const command = `cross-env APPLICATION=${name || "catalyst_app"} node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ${process.cwd()}/${BUILD_OUTPUT_PATH}/startServer.js` + const command = `cross-env APPLICATION=${name || "catalyst_app"} node -r ./dist/scripts/loadScriptsBeforeServerStarts.js --expose-gc --trace-gc --trace-gc-verbose --inspect=0.0.0.0:9229 ${process.cwd()}/${BUILD_OUTPUT_PATH}/startServer.js` + + // const command = `cross-env APPLICATION=${name || "catalyst_app"} node -r ./dist/scripts/loadScriptsBeforeServerStarts.js --inspect=0.0.0.0:9229 ${process.cwd()}/${BUILD_OUTPUT_PATH}/startServer.js` spawnSync(command, [], { cwd: dirname, From 7b28d85c58ba04e4f404813e118e9f46b02fc52e Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Wed, 21 Jan 2026 23:08:55 +0530 Subject: [PATCH 2/8] fix fast refresh --- src/scripts/start.js | 8 +- src/server/renderer/index.js | 8 +- src/server/startServer.js | 82 +++++++++++++-------- src/webpack/babel.config.client.js | 3 + src/webpack/base.babel.js | 12 ++- src/webpack/development.client.babel.js | 98 ++----------------------- 6 files changed, 80 insertions(+), 131 deletions(-) diff --git a/src/scripts/start.js b/src/scripts/start.js index 5b2f88d6..30b5bec6 100644 --- a/src/scripts/start.js +++ b/src/scripts/start.js @@ -13,12 +13,17 @@ function start() { const argumentsObject = arrayToObject(commandLineArguments) const dirname = path.resolve(__dirname, "../../") + // Run three separate processes for better memory isolation: + // 1. Client DevServer (development.client.babel) + // 2. SSR Watcher (ssr.watcher) + // 3. Node Server (startServer) const command = ` node ./dist/scripts/checkVersion - npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/webpack/development.client.babel --no-warnings=ExperimentalWarning --no-warnings=BABEL & npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/server/startServer.js --extensions .js,.ts,.jsx,.tsx --watch-path=${process.env.PWD}/server --watch-path=${process.env.PWD}/src --ignore='__IGNORE__' --no-warnings=ExperimentalWarning --no-warnings=BABEL + npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/webpack/development.client.babel --no-warnings=ExperimentalWarning --no-warnings=BABEL & npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/server/startServer.js --extensions .js,.ts,.jsx,.tsx --ignore='__IGNORE__' --no-warnings=ExperimentalWarning --no-warnings=BABEL ` if (isWindows) { + // Client DevServer spawn( `node ./dist/scripts/checkVersion && start /b npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/webpack/development.client.babel --no-warnings=ExperimentalWarning --no-warnings=BABEL`, [], @@ -38,6 +43,7 @@ function start() { } ) + // Node Server spawn( `node ./dist/scripts/checkVersion && npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/server/startServer.js --watch-path=${process.cwd()}/server --watch-path=${process.cwd()}/src --ignore='__IGNORE__' --no-warnings=ExperimentalWarning --no-warnings=BABEL`, [], diff --git a/src/server/renderer/index.js b/src/server/renderer/index.js index 6afd8923..907b1010 100644 --- a/src/server/renderer/index.js +++ b/src/server/renderer/index.js @@ -3,14 +3,8 @@ import express from "express" const router = express.Router() router.use(function rendererMiddleware(req, res, next) { - let handler = "" - - if (process.env.NODE_ENV === "production") { - handler = require("./handler").default - } else { - handler = require("../../../.catalyst-dev/server/renderer/handler.development.js").default - } + let handler = require("./handler").default if (res.locals.rendererWrapper) { logger.debug({ message: "Handler wrapped" }) res.locals.rendererWrapper(handler)(req, res, next) diff --git a/src/server/startServer.js b/src/server/startServer.js index a8a373fa..280e8332 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -54,14 +54,8 @@ process.on("unhandledRejection", (err) => console.log("unhandledRejection in Cat const port = process.env.NODE_SERVER_PORT ?? 3005 const host = process.env.NODE_SERVER_HOSTNAME ?? "localhost" -let statsPath = path.join( - __dirname, - `../../`, - ".catalyst-dev", - "/server", - "/renderer", - "handler.development.js" -) +let statsPath = path.join(__dirname, "../../", `loadable-stats.json`) + if (env === "production") { statsPath = path.join(process.env.src_path, `${process.env.BUILD_OUTPUT_PATH}/public/loadable-stats.json`) @@ -70,18 +64,21 @@ if (env === "production") { const watcher = chokidar.watch(statsPath, { persistent: true }) let serverInstance = null -const restartServer = () => { - const server = require("./expressServer.js").default - const { APPLICATION, NODE_SERVER_HOSTNAME, NODE_SERVER_PORT } = process.env - - serverInstance = server.listen({ port, host }) - console.log("Server Restarted!") - console.log(`You can now view ${APPLICATION} in the browser.`) - console.log(util.format("Local:", cyan(`http://${NODE_SERVER_HOSTNAME}:${NODE_SERVER_PORT}`))) +const clearServerCache = () => { + const projectPath = process.env.src_path // or your mweb path + Object.keys(require.cache).forEach((key) => { + // Clear all files from your project, except node_modules dependencies + if(key.startsWith(projectPath) || key.includes('catalyst-core')) { + delete require.cache[key] + } + }) } const startServer = () => { + + clearServerCache() + const server = require("./expressServer.js").default serverInstance = server.listen({ port, host }, (error) => { @@ -118,24 +115,22 @@ const startServer = () => { if (fs.existsSync(statsPath)) { // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. watcher.on("change", () => { - watcher.close() - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } + clearServerCache() }) // this block will start the server when your files have been compiled for production and lodable-stats.json exists. watcher.on("add", () => { - if (env === "production") { - watcher.close() + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { startServer() } + // } }) } else { // this block will start the server in development environment for the first time when loadable-stats.json does not exists. watcher.on("add", () => { - watcher.close() + console.log("loadable-stats.json added first time") + // watcher.close() if (serverInstance) { serverInstance.close(() => startServer()) } else { @@ -143,8 +138,37 @@ if (fs.existsSync(statsPath)) { } }) } -if (fs.existsSync(statsPath)) { - if (env === "development") { - restartServer() +const serverPath = path.join(process.env.src_path, 'server') + +const serverWatcher = chokidar.watch(serverPath, { + persistent: true, + ignoreInitial: true, // Don't trigger on initial scan + ignored: /node_modules/ +}) + +serverWatcher.on('change', (filePath) => { + console.log(`Server file changed: ${filePath}`) + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() } -} +}) + +serverWatcher.on('add', (filePath) => { + console.log(`Server file added: ${filePath}`) + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } +}) + +serverWatcher.on('unlink', (filePath) => { + console.log(`Server file removed: ${filePath}`) + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } +}) diff --git a/src/webpack/babel.config.client.js b/src/webpack/babel.config.client.js index 4c4dad38..7bf59b44 100644 --- a/src/webpack/babel.config.client.js +++ b/src/webpack/babel.config.client.js @@ -38,6 +38,9 @@ export default { test: { presets: ["@babel/preset-react"], }, + development: { + plugins: ["react-refresh/babel"], + }, }, ignore: ["__TEST__"], } diff --git a/src/webpack/base.babel.js b/src/webpack/base.babel.js index 1e03e42b..babf7b76 100644 --- a/src/webpack/base.babel.js +++ b/src/webpack/base.babel.js @@ -69,6 +69,16 @@ if (IS_DEV_COMMAND === "true" && !isDev) { export default { context: path.resolve(process.env.src_path), mode: isDev ? "development" : "production", + // Use filesystem cache to reduce memory pressure and improve rebuild performance + cache: isDev + ? { + type: "filesystem", + buildDependencies: { + config: [__filename], + }, + cacheDirectory: path.join(process.env.src_path, "node_modules/catalyst-core/.cache/webpack"), + } + : false, entry: { app: [path.resolve(process.env.src_path, "./client/index.js")], }, @@ -115,7 +125,6 @@ export default { path.resolve(process.env.src_path, "./src/static/css/base"), ], use: [ - isDev && "css-hot-loader", !isSSR && MiniCssExtractPlugin.loader, { loader: "css-loader", @@ -154,7 +163,6 @@ export default { path.resolve(process.env.src_path, "./src/static/css/base"), ], use: [ - isDev && "css-hot-loader", !isSSR && MiniCssExtractPlugin.loader, { loader: "css-loader" }, { loader: "postcss-loader" }, diff --git a/src/webpack/development.client.babel.js b/src/webpack/development.client.babel.js index dce3c662..ae6b9c86 100644 --- a/src/webpack/development.client.babel.js +++ b/src/webpack/development.client.babel.js @@ -1,14 +1,10 @@ import webpack from "webpack" -import merge, { mergeWithCustomize, customizeArray, customizeObject } from "webpack-merge" +import merge from "webpack-merge" import WebpackDevServer from "webpack-dev-server" import LoadablePlugin from "@loadable/webpack-plugin" import MiniCssExtractPlugin from "mini-css-extract-plugin" import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin" import path from "path" -import nodeExternals from "webpack-node-externals" -import rootWorkspacePath from "app-root-path" -// Import the catalystResultMap for SSR support -import { catalystResultMap } from "../scripts/registerAliases.js" import catalystConfig from "@catalyst/root/config.json" import baseConfig from "@catalyst/webpack/base.babel.js" @@ -18,7 +14,8 @@ const { WEBPACK_DEV_SERVER_PORT, WEBPACK_DEV_SERVER_HOSTNAME } = process.env // Create client config const webpackClientConfig = merge(baseConfig, { - devtool: "inline-source-map", + // Use eval-cheap-module-source-map for better performance and lower memory usage + devtool: "eval-cheap-module-source-map", stats: "none", infrastructureLogging: { level: "none", @@ -79,70 +76,8 @@ const webpackClientConfig = merge(baseConfig, { }, }) -// Create SSR config -const webpackSSRConfig = mergeWithCustomize({ - customizeArray: customizeArray({ - entry: "replace", - optimization: "replace", - plugins: "prepend", - }), - customizeObject: customizeObject({ - entry: "replace", - optimization: "replace", - plugins: "prepend", - }), -})(baseConfig, { - mode: "development", - stats: "none", - target: "node", - entry: { - handler: path.resolve(__dirname, "..", "./server/renderer/handler.js"), - }, - externals: [ - /\.(html|png|gif|jpg)$/, - nodeExternals({ - modulesDir: path.resolve(process.env.src_path, "./node_modules"), - allowlist: customWebpackConfig.transpileModules ? customWebpackConfig.transpileModules : [], - }), - nodeExternals({ - modulesDir: path.join(rootWorkspacePath.path, "./node_modules"), - allowlist: customWebpackConfig.transpileModules ? customWebpackConfig.transpileModules : [], - }), - ], - resolve: { - alias: catalystResultMap, - }, - output: { - path: path.join(__dirname, "../..", ".catalyst-dev", "/server", "/renderer"), - chunkFilename: catalystConfig.chunkFileName, - filename: "handler.development.js", - libraryTarget: "commonjs", - }, - plugins: [ - new LoadablePlugin({ - filename: "loadable-stats.json", - writeToDisk: { - filename: path.join(__dirname, "../..", ".catalyst-dev", "/server", "/renderer"), - }, - }), - new MiniCssExtractPlugin({ - filename: catalystConfig.cssChunkFileName, - ignoreOrder: true, - }), - ...customWebpackConfig.ssrPlugins, - ].filter(Boolean), -}) - -// Create separate compiler for SSR that writes to disk -const ssrCompiler = webpack(webpackSSRConfig) -const watchInstance = ssrCompiler.watch({}, (err) => { - if (err) { - console.error(err) - return - } -}) - // Create dev server for client-side only +// Note: SSR compiler now runs in a separate process (ssr.watcher.js) for better memory management let devServer = new WebpackDevServer( { port: WEBPACK_DEV_SERVER_PORT, @@ -173,28 +108,8 @@ devServer.startCallback(() => { // Cleanup on exit const cleanup = () => { - // Close webpack watch - watchInstance.close(() => { - // Delete the development handler file - try { - // Delete the file - require("fs").unlinkSync( - path.join( - __dirname, - "../..", - ".catalyst-dev", - "/server", - "/renderer", - "handler.development.js" - ) - ) - // Try to remove the renderer directory - require("fs").rmdirSync(path.join(process.env.src_path, ".catalyst-dev", "/renderer")) - // Try to remove the parent directory - require("fs").rmdirSync(path.join(process.env.src_path, ".catalyst-dev")) - } catch (err) { - // Ignore errors during cleanup - } + console.log("[Client] Shutting down client dev server...") + devServer.stop().then(() => { process.exit() }) } @@ -202,4 +117,3 @@ const cleanup = () => { // Handle various ways the process might exit process.on("SIGINT", cleanup) // Ctrl+C process.on("SIGTERM", cleanup) // kill -process.on("exit", cleanup) // normal exit From 26c9b7052976769d6197dbf9250731322cd1126c Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Mon, 26 Jan 2026 19:17:49 +0530 Subject: [PATCH 3/8] add check for prod --- src/scripts/serve.js | 2 +- src/server/startServer.js | 101 +++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/src/scripts/serve.js b/src/scripts/serve.js index 76bb3b30..4023c0d7 100644 --- a/src/scripts/serve.js +++ b/src/scripts/serve.js @@ -13,7 +13,7 @@ function serve() { const argumentsObject = arrayToObject(commandLineArguments) const dirname = path.resolve(__dirname, "../../") - const command = `cross-env APPLICATION=${name || "catalyst_app"} node -r ./dist/scripts/loadScriptsBeforeServerStarts.js --expose-gc --trace-gc --trace-gc-verbose --inspect=0.0.0.0:9229 ${process.cwd()}/${BUILD_OUTPUT_PATH}/startServer.js` + const command = `cross-env APPLICATION=${name || "catalyst_app"} node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ${process.cwd()}/${BUILD_OUTPUT_PATH}/startServer.js` // const command = `cross-env APPLICATION=${name || "catalyst_app"} node -r ./dist/scripts/loadScriptsBeforeServerStarts.js --inspect=0.0.0.0:9229 ${process.cwd()}/${BUILD_OUTPUT_PATH}/startServer.js` diff --git a/src/server/startServer.js b/src/server/startServer.js index 280e8332..5c76e757 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -54,7 +54,7 @@ process.on("unhandledRejection", (err) => console.log("unhandledRejection in Cat const port = process.env.NODE_SERVER_PORT ?? 3005 const host = process.env.NODE_SERVER_HOSTNAME ?? "localhost" -let statsPath = path.join(__dirname, "../../", `loadable-stats.json`) +let statsPath = path.join(__dirname, "../../", `loadable-stats.json`) if (env === "production") { @@ -69,16 +69,13 @@ const clearServerCache = () => { const projectPath = process.env.src_path // or your mweb path Object.keys(require.cache).forEach((key) => { // Clear all files from your project, except node_modules dependencies - if(key.startsWith(projectPath) || key.includes('catalyst-core')) { - delete require.cache[key] - } + if (key.startsWith(projectPath) || key.includes('catalyst-core')) { + delete require.cache[key] + } }) } const startServer = () => { - - clearServerCache() - const server = require("./expressServer.js").default serverInstance = server.listen({ port, host }, (error) => { @@ -112,63 +109,67 @@ const startServer = () => { }) } -if (fs.existsSync(statsPath)) { - // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. - watcher.on("change", () => { - clearServerCache() +if (process.env.NODE_ENV === "development") { + if (fs.existsSync(statsPath)) { + // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. + watcher.on("change", () => { + clearServerCache() + }) + // this block will start the server when your files have been compiled for production and lodable-stats.json exists. + watcher.on("add", () => { + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + // } + }) + } else { + // this block will start the server in development environment for the first time when loadable-stats.json does not exists. + watcher.on("add", () => { + console.log("loadable-stats.json added first time") + // watcher.close() + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + }) + } + const serverPath = path.join(process.env.src_path, 'server') + + const serverWatcher = chokidar.watch(serverPath, { + persistent: true, + ignoreInitial: true, // Don't trigger on initial scan + ignored: /node_modules/ }) - // this block will start the server when your files have been compiled for production and lodable-stats.json exists. - watcher.on("add", () => { + + serverWatcher.on('change', (filePath) => { + console.log(`Server file changed: ${filePath}`) if (serverInstance) { serverInstance.close(() => startServer()) } else { startServer() } - // } }) -} else { - // this block will start the server in development environment for the first time when loadable-stats.json does not exists. - watcher.on("add", () => { - console.log("loadable-stats.json added first time") - // watcher.close() + + serverWatcher.on('add', (filePath) => { + console.log(`Server file added: ${filePath}`) if (serverInstance) { serverInstance.close(() => startServer()) } else { startServer() } }) -} -const serverPath = path.join(process.env.src_path, 'server') - -const serverWatcher = chokidar.watch(serverPath, { - persistent: true, - ignoreInitial: true, // Don't trigger on initial scan - ignored: /node_modules/ -}) -serverWatcher.on('change', (filePath) => { - console.log(`Server file changed: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } -}) - -serverWatcher.on('add', (filePath) => { - console.log(`Server file added: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } -}) - -serverWatcher.on('unlink', (filePath) => { - console.log(`Server file removed: ${filePath}`) - if (serverInstance) { + serverWatcher.on('unlink', (filePath) => { + console.log(`Server file removed: ${filePath}`) + if (serverInstance) { serverInstance.close(() => startServer()) } else { startServer() - } -}) + } + }) +}else{ + startServer() +} \ No newline at end of file From ddaef7a32bd729db72fff795d814dbd94203705e Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Wed, 25 Feb 2026 15:10:55 +0530 Subject: [PATCH 4/8] bug fix --- src/server/startServer.js | 153 +++++++++++++++--------- src/webpack/development.client.babel.js | 40 ++++++- 2 files changed, 130 insertions(+), 63 deletions(-) diff --git a/src/server/startServer.js b/src/server/startServer.js index 5c76e757..c2e29f7f 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -1,4 +1,5 @@ import fs from "fs" +import net from "net" import path from "path" import util from "node:util" import chokidar from "chokidar" @@ -56,7 +57,6 @@ const host = process.env.NODE_SERVER_HOSTNAME ?? "localhost" let statsPath = path.join(__dirname, "../../", `loadable-stats.json`) - if (env === "production") { statsPath = path.join(process.env.src_path, `${process.env.BUILD_OUTPUT_PATH}/public/loadable-stats.json`) } @@ -69,7 +69,7 @@ const clearServerCache = () => { const projectPath = process.env.src_path // or your mweb path Object.keys(require.cache).forEach((key) => { // Clear all files from your project, except node_modules dependencies - if (key.startsWith(projectPath) || key.includes('catalyst-core')) { + if (key.startsWith(projectPath) || key.includes("catalyst-core")) { delete require.cache[key] } }) @@ -109,67 +109,102 @@ const startServer = () => { }) } -if (process.env.NODE_ENV === "development") { - if (fs.existsSync(statsPath)) { - // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. - watcher.on("change", () => { - clearServerCache() - }) - // this block will start the server when your files have been compiled for production and lodable-stats.json exists. - watcher.on("add", () => { - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } - // } - }) - } else { - // this block will start the server in development environment for the first time when loadable-stats.json does not exists. - watcher.on("add", () => { - console.log("loadable-stats.json added first time") - // watcher.close() - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } - }) - } - const serverPath = path.join(process.env.src_path, 'server') - - const serverWatcher = chokidar.watch(serverPath, { - persistent: true, - ignoreInitial: true, // Don't trigger on initial scan - ignored: /node_modules/ - }) - - serverWatcher.on('change', (filePath) => { - console.log(`Server file changed: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } +const checkPortAvailability = (port, host) => { + return new Promise((resolve, reject) => { + const tester = net + .createServer() + .once("error", (err) => { + tester.close(() => { + if (err.code === "EADDRINUSE") { + reject( + new Error( + `Port ${port} is already in use on ${host}. Please free the port or set a different NODE_SERVER_PORT.` + ) + ) + } else { + reject(err) + } + }) + }) + .once("listening", () => { + tester.close(() => resolve()) + }) + .listen(port, host) }) +} - serverWatcher.on('add', (filePath) => { - console.log(`Server file added: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) +checkPortAvailability(port, host) + .then(() => { + console.log("Port is available") + if (process.env.NODE_ENV === "development") { + if (fs.existsSync(statsPath)) { + console.log("loadable-stats.json exists") + // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. + watcher.on("change", () => { + clearServerCache() + }) + // this block will start the server when your files have been compiled for production and lodable-stats.json exists. + // watcher.on("add", () => { + console.log("loadable-stats.json exists, starting server") + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + // } + // }) + } else { + console.log("loadable-stats.json does not exist, creating one") + // this block will start the server in development environment for the first time when loadable-stats.json does not exists. + watcher.on("add", () => { + console.log("loadable-stats.json added first time") + // watcher.close() + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + }) + } + const serverPath = path.join(process.env.src_path, "server") + + const serverWatcher = chokidar.watch(serverPath, { + persistent: true, + ignoreInitial: true, // Don't trigger on initial scan + ignored: /node_modules/, + }) + + serverWatcher.on("change", (filePath) => { + console.log(`Server file changed: ${filePath}`) + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + }) + + serverWatcher.on("add", (filePath) => { + console.log(`Server file added: ${filePath}`) + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + }) + + serverWatcher.on("unlink", (filePath) => { + console.log(`Server file removed: ${filePath}`) + if (serverInstance) { + serverInstance.close(() => startServer()) + } else { + startServer() + } + }) } else { startServer() } }) - - serverWatcher.on('unlink', (filePath) => { - console.log(`Server file removed: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } + .catch((err) => { + console.error(`\n[Catalyst] Server startup failed: ${err.message}\n`) + process.exit(1) }) -}else{ - startServer() -} \ No newline at end of file diff --git a/src/webpack/development.client.babel.js b/src/webpack/development.client.babel.js index ae6b9c86..bb9523fb 100644 --- a/src/webpack/development.client.babel.js +++ b/src/webpack/development.client.babel.js @@ -1,3 +1,4 @@ +import net from "net" import webpack from "webpack" import merge from "webpack-merge" import WebpackDevServer from "webpack-dev-server" @@ -101,10 +102,41 @@ let devServer = new WebpackDevServer( webpack(webpackClientConfig) ) -devServer.startCallback(() => { - console.log("Catalyst is compiling your files.") - console.log("Please wait until bundling is finished.\n") -}) +const checkPortAvailability = (port, host) => { + return new Promise((resolve, reject) => { + const tester = net + .createServer() + .once("error", (err) => { + tester.close(() => { + if (err.code === "EADDRINUSE") { + reject( + new Error( + `Port ${port} is already in use on ${host}. Please free the port or set a different WEBPACK_DEV_SERVER_PORT.` + ) + ) + } else { + reject(err) + } + }) + }) + .once("listening", () => { + tester.close(() => resolve()) + }) + .listen(port, host) + }) +} + +checkPortAvailability(WEBPACK_DEV_SERVER_PORT, WEBPACK_DEV_SERVER_HOSTNAME) + .then(() => { + devServer.startCallback(() => { + console.log("Catalyst is compiling your files.") + console.log("Please wait until bundling is finished.\n") + }) + }) + .catch((err) => { + console.error(`\n[Catalyst] Dev server startup failed: ${err.message}\n`) + process.exit(1) + }) // Cleanup on exit const cleanup = () => { From 026f1b1887b9237b6eb86edfd9a644c1cc5ec4ac Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Wed, 25 Feb 2026 15:42:39 +0530 Subject: [PATCH 5/8] remove logs --- src/server/startServer.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/server/startServer.js b/src/server/startServer.js index c2e29f7f..5169d963 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -138,14 +138,14 @@ checkPortAvailability(port, host) console.log("Port is available") if (process.env.NODE_ENV === "development") { if (fs.existsSync(statsPath)) { - console.log("loadable-stats.json exists") + // console.log("loadable-stats.json exists") // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. watcher.on("change", () => { clearServerCache() }) // this block will start the server when your files have been compiled for production and lodable-stats.json exists. // watcher.on("add", () => { - console.log("loadable-stats.json exists, starting server") + // console.log("loadable-stats.json exists, starting server") if (serverInstance) { serverInstance.close(() => startServer()) } else { @@ -154,10 +154,10 @@ checkPortAvailability(port, host) // } // }) } else { - console.log("loadable-stats.json does not exist, creating one") + // console.log("loadable-stats.json does not exist, creating one") // this block will start the server in development environment for the first time when loadable-stats.json does not exists. watcher.on("add", () => { - console.log("loadable-stats.json added first time") + // console.log("loadable-stats.json added first time") // watcher.close() if (serverInstance) { serverInstance.close(() => startServer()) @@ -174,8 +174,8 @@ checkPortAvailability(port, host) ignored: /node_modules/, }) - serverWatcher.on("change", (filePath) => { - console.log(`Server file changed: ${filePath}`) + serverWatcher.on("change", () => { + // console.log(`Server file changed: ${filePath}`) if (serverInstance) { serverInstance.close(() => startServer()) } else { @@ -183,8 +183,8 @@ checkPortAvailability(port, host) } }) - serverWatcher.on("add", (filePath) => { - console.log(`Server file added: ${filePath}`) + serverWatcher.on("add", () => { + // console.log(`Server file added: ${filePath}`) if (serverInstance) { serverInstance.close(() => startServer()) } else { @@ -192,8 +192,8 @@ checkPortAvailability(port, host) } }) - serverWatcher.on("unlink", (filePath) => { - console.log(`Server file removed: ${filePath}`) + serverWatcher.on("unlink", () => { + // console.log(`Server file removed: ${filePath}`) if (serverInstance) { serverInstance.close(() => startServer()) } else { From 7ce181618fda22d509ed6886e7eed22de67642e9 Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Wed, 25 Feb 2026 16:02:01 +0530 Subject: [PATCH 6/8] fix: remove --watch-path from Windows dev server, unify hot-reload across platforms Replaced Node.js native --watch-path flags on Windows with chokidar-based watching (already used on Mac/Linux), eliminating the double-trigger bug where a single /server file change caused both a process-level restart and an in-process Express restart simultaneously. Also fixes EADDRINUSE race condition by introducing restartServer() which nulls serverInstance before close(), preventing concurrent chokidar events from each queuing independent startServer() callbacks on the same socket. Co-Authored-By: Claude Sonnet 4.6 --- src/scripts/start.js | 2 +- src/server/startServer.js | 157 ++++++++++++++++++++++++-------------- 2 files changed, 99 insertions(+), 60 deletions(-) diff --git a/src/scripts/start.js b/src/scripts/start.js index 30b5bec6..95590a3b 100644 --- a/src/scripts/start.js +++ b/src/scripts/start.js @@ -45,7 +45,7 @@ function start() { // Node Server spawn( - `node ./dist/scripts/checkVersion && npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/server/startServer.js --watch-path=${process.cwd()}/server --watch-path=${process.cwd()}/src --ignore='__IGNORE__' --no-warnings=ExperimentalWarning --no-warnings=BABEL`, + `node ./dist/scripts/checkVersion && npx babel-node -r ./dist/scripts/loadScriptsBeforeServerStarts.js ./dist/server/startServer.js --extensions .js,.ts,.jsx,.tsx --ignore='__IGNORE__' --no-warnings=ExperimentalWarning --no-warnings=BABEL`, [], { cwd: dirname, diff --git a/src/server/startServer.js b/src/server/startServer.js index 5169d963..1d06e550 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -1,3 +1,20 @@ +/** + * startServer.js + * + * Entry point for the Catalyst Node.js server process. Handles: + * 1. Process-level error and signal handlers + * 2. Port availability check before binding + * 3. Hot-reload in development: + * - Watches loadable-stats.json (rebuilt by webpack on every client-side change) + * to know when a new bundle is ready, then clears the require cache so the + * next request picks up the latest server bundle. + * - Watches the app's /server directory and fully restarts the Express server + * whenever server-side source files change. + * 4. Safe server restart logic that prevents EADDRINUSE by nulling the instance + * reference before closing, ensuring concurrent watcher events don't each + * queue an independent startServer() callback on the same closing socket. + */ + import fs from "fs" import net from "net" import path from "path" @@ -10,24 +27,30 @@ import { safeCall } from "@catalyst/server/utils/validator.js" const env = process.env.NODE_ENV || "development" -// function defined by user which needs to run before server starts +// Run any app-defined pre-start hook (e.g. seed config, connect to DB) safeCall(preServerInit) +// ─── Process-level error handlers ──────────────────────────────────────────── + process.on("uncaughtException", (err, origin) => { console.log(process.stderr.fd) console.log(`Caught exception: ${err}\n` + `Exception origin: ${origin}`) }) +process.on("uncaughtExceptionMonitor", (err, origin) => { + console.log(err, origin) +}) + +process.on("unhandledRejection", (err) => console.log("unhandledRejection in Catalyst", safeStringify(err))) + +// Graceful shutdown on Ctrl-C process.on("SIGINT", function (data) { console.log("SIGINT") console.log(data) process.exit(0) }) -process.on("uncaughtExceptionMonitor", (err, origin) => { - console.log(err, origin) -}) - +// Parent process can send "shutdown" to trigger a clean exit (used by cluster managers) process.on("message", function (msg) { if (msg == "shutdown") { console.log("Closing all connections...") @@ -38,8 +61,8 @@ process.on("message", function (msg) { } }) -// if (env === "development") { -// Add better stack tracing for promises in dev mode +// ─── Helpers ────────────────────────────────────────────────────────────────── + function safeStringify(err) { try { return JSON.stringify(err) @@ -49,32 +72,71 @@ function safeStringify(err) { } } -process.on("unhandledRejection", (err) => console.log("unhandledRejection in Catalyst", safeStringify(err))) -// } +// ─── Configuration ──────────────────────────────────────────────────────────── const port = process.env.NODE_SERVER_PORT ?? 3005 const host = process.env.NODE_SERVER_HOSTNAME ?? "localhost" +// loadable-stats.json is emitted by webpack (@loadable/webpack-plugin) after every +// successful client-side build. Its presence signals that at least one build has +// completed and the server can safely start serving SSR responses. let statsPath = path.join(__dirname, "../../", `loadable-stats.json`) if (env === "production") { statsPath = path.join(process.env.src_path, `${process.env.BUILD_OUTPUT_PATH}/public/loadable-stats.json`) } +// Watcher on loadable-stats.json — used to detect completed webpack rebuilds const watcher = chokidar.watch(statsPath, { persistent: true }) +// Holds the active http.Server instance. Kept at module scope so restartServer() +// can close the old instance before creating a new one. let serverInstance = null +// ─── Cache management ───────────────────────────────────────────────────────── + +/** + * Purges all app and framework modules from Node's require cache. + * Called after every webpack rebuild so that the next require("./expressServer") + * loads the freshly compiled server bundle instead of the stale cached version. + * node_modules are intentionally left in cache to avoid re-evaluating them on + * every hot reload. + */ const clearServerCache = () => { - const projectPath = process.env.src_path // or your mweb path + const projectPath = process.env.src_path Object.keys(require.cache).forEach((key) => { - // Clear all files from your project, except node_modules dependencies if (key.startsWith(projectPath) || key.includes("catalyst-core")) { delete require.cache[key] } }) } +// ─── Server lifecycle ───────────────────────────────────────────────────────── + +/** + * Safely restarts the Express server. + * + * Nulls out `serverInstance` before calling close() so that any watcher events + * that fire concurrently (e.g. an editor writing multiple files on save) hit the + * `!serverInstance` guard and schedule a single startServer() — rather than each + * queuing their own close() callback that would all call startServer() once the + * socket finally closes, causing EADDRINUSE on the second and later attempts. + */ +const restartServer = () => { + if (!serverInstance) { + startServer() + return + } + const closing = serverInstance + serverInstance = null + closing.close(() => startServer()) +} + +/** + * Requires and starts the Express server. + * expressServer.js is re-required on every call so that, combined with + * clearServerCache(), hot-reloaded changes are always picked up. + */ const startServer = () => { const server = require("./expressServer.js").default @@ -83,7 +145,6 @@ const startServer = () => { if (error) { console.log("An error occured while starting the Application server : ", error) - // function defined by user which needs to run if server fails safeCall(onServerError) return } @@ -109,6 +170,13 @@ const startServer = () => { }) } +// ─── Port check ─────────────────────────────────────────────────────────────── + +/** + * Verifies the target port is free before we attempt to bind. + * Probing with a temporary server gives a clear, actionable error message + * instead of a cryptic EADDRINUSE from Express. + */ const checkPortAvailability = (port, host) => { return new Promise((resolve, reject) => { const tester = net @@ -133,39 +201,33 @@ const checkPortAvailability = (port, host) => { }) } +// ─── Startup ────────────────────────────────────────────────────────────────── + checkPortAvailability(port, host) .then(() => { console.log("Port is available") + if (process.env.NODE_ENV === "development") { if (fs.existsSync(statsPath)) { - // console.log("loadable-stats.json exists") - // if loadable-stats.json exist this block will start the server in development environment. This happens in dev environment when loadable stats already exists and developer is making changes to the files. lodable-stats.json will be updated after every change. + // loadable-stats.json already exists (e.g. dev server restarted mid-session): + // start immediately and clear the module cache on every subsequent webpack rebuild + // so SSR always uses the latest client chunks. watcher.on("change", () => { clearServerCache() }) - // this block will start the server when your files have been compiled for production and lodable-stats.json exists. - // watcher.on("add", () => { - // console.log("loadable-stats.json exists, starting server") - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } - // } - // }) + startServer() } else { - // console.log("loadable-stats.json does not exist, creating one") - // this block will start the server in development environment for the first time when loadable-stats.json does not exists. + // First boot — webpack hasn't finished the initial build yet. + // Wait for loadable-stats.json to be created before starting the server, + // otherwise @loadable/server would fail trying to read chunk metadata. watcher.on("add", () => { - // console.log("loadable-stats.json added first time") - // watcher.close() - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } + restartServer() }) } + + // Watch the app's /server directory for source changes. + // Any modification, addition, or deletion of a server-side file triggers + // a full server restart so the new code is loaded via a fresh require(). const serverPath = path.join(process.env.src_path, "server") const serverWatcher = chokidar.watch(serverPath, { @@ -174,32 +236,9 @@ checkPortAvailability(port, host) ignored: /node_modules/, }) - serverWatcher.on("change", () => { - // console.log(`Server file changed: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } - }) - - serverWatcher.on("add", () => { - // console.log(`Server file added: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } - }) - - serverWatcher.on("unlink", () => { - // console.log(`Server file removed: ${filePath}`) - if (serverInstance) { - serverInstance.close(() => startServer()) - } else { - startServer() - } - }) + serverWatcher.on("change", () => restartServer()) + serverWatcher.on("add", () => restartServer()) + serverWatcher.on("unlink", () => restartServer()) } else { startServer() } From 2c9a23e4c2eefd4797b225f19576389d3f91fc7e Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Wed, 25 Feb 2026 16:40:55 +0530 Subject: [PATCH 7/8] clear server cache --- src/server/startServer.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/server/startServer.js b/src/server/startServer.js index 1d06e550..fd5bbb9a 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -102,10 +102,10 @@ let serverInstance = null * node_modules are intentionally left in cache to avoid re-evaluating them on * every hot reload. */ -const clearServerCache = () => { +const clearServerCache = (filePath = "") => { const projectPath = process.env.src_path Object.keys(require.cache).forEach((key) => { - if (key.startsWith(projectPath) || key.includes("catalyst-core")) { + if (key.startsWith(projectPath) || key.includes("catalyst-core") || key.includes(filePath)) { delete require.cache[key] } }) @@ -221,7 +221,7 @@ checkPortAvailability(port, host) // Wait for loadable-stats.json to be created before starting the server, // otherwise @loadable/server would fail trying to read chunk metadata. watcher.on("add", () => { - restartServer() + startServer() }) } @@ -236,7 +236,10 @@ checkPortAvailability(port, host) ignored: /node_modules/, }) - serverWatcher.on("change", () => restartServer()) + serverWatcher.on("change", (filePath) => { + clearServerCache(filePath) + restartServer() + }) serverWatcher.on("add", () => restartServer()) serverWatcher.on("unlink", () => restartServer()) } else { From d88f1966b599c1a44632160f9d4d1d9b47c00eb7 Mon Sep 17 00:00:00 2001 From: utkarsh-1mg Date: Wed, 25 Feb 2026 16:59:18 +0530 Subject: [PATCH 8/8] clear server cache --- bin/catalyst.js | 6 +++--- src/scripts/cleanCache.js | 30 ++++++++++++++++++++++++++++++ src/server/startServer.js | 2 -- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/scripts/cleanCache.js diff --git a/bin/catalyst.js b/bin/catalyst.js index b5e7817b..7cc22024 100755 --- a/bin/catalyst.js +++ b/bin/catalyst.js @@ -6,11 +6,11 @@ process.on("unhandledRejection", (err) => { const { spawnSync } = require("node:child_process") const args = process.argv.slice(2) const scriptIndex = args.findIndex( - (x) => x === "build" || x === "start" || x === "serve" || x === "devBuild" || x === "devServe" + (x) => x === "build" || x === "start" || x === "serve" || x === "devBuild" || x === "devServe" || x === "cleanCache" ) const script = scriptIndex === -1 ? args[0] : args[scriptIndex] const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [] -if (["build", "start", "serve", "devBuild", "devServe"].includes(script)) { +if (["build", "start", "serve", "devBuild", "devServe", "cleanCache"].includes(script)) { const result = spawnSync( process.execPath, nodeArgs.concat(require.resolve("../dist/scripts/" + script)).concat(args.slice(scriptIndex + 1)), @@ -34,5 +34,5 @@ if (["build", "start", "serve", "devBuild", "devServe"].includes(script)) { } process.exit(result.status) } else { - console.log('Unknown script "' + script + '".') + console.log('Unknown script "' + script + '". Available: build, start, serve, devBuild, devServe, cleanCache') } diff --git a/src/scripts/cleanCache.js b/src/scripts/cleanCache.js new file mode 100644 index 00000000..6bafcafd --- /dev/null +++ b/src/scripts/cleanCache.js @@ -0,0 +1,30 @@ +const fs = require("fs") +const path = require("path") + +function cleanCache() { + const catalystCacheDir = path.join(process.cwd(), "node_modules/catalyst-core") + const webpackCacheDir = path.join(catalystCacheDir, ".cache/webpack") + const loadableStats = path.join(catalystCacheDir, "loadable-stats.json") + + let cleaned = false + + if (fs.existsSync(webpackCacheDir)) { + fs.rmSync(webpackCacheDir, { recursive: true, force: true }) + console.log("[Catalyst] Removed webpack filesystem cache.") + cleaned = true + } + + if (fs.existsSync(loadableStats)) { + fs.rmSync(loadableStats) + console.log("[Catalyst] Removed loadable-stats.json.") + cleaned = true + } + + if (!cleaned) { + console.log("[Catalyst] Nothing to clean — cache is already empty.") + } else { + console.log("[Catalyst] Clean complete. Next dev start will be a full rebuild.") + } +} + +cleanCache() diff --git a/src/server/startServer.js b/src/server/startServer.js index fd5bbb9a..15ae6dbc 100644 --- a/src/server/startServer.js +++ b/src/server/startServer.js @@ -205,8 +205,6 @@ const checkPortAvailability = (port, host) => { checkPortAvailability(port, host) .then(() => { - console.log("Port is available") - if (process.env.NODE_ENV === "development") { if (fs.existsSync(statsPath)) { // loadable-stats.json already exists (e.g. dev server restarted mid-session):