From baf8d0118b7861a1d087c8298e87409e79b908ea Mon Sep 17 00:00:00 2001
From: Frankly George <54015-franklygeorge@users.noreply.gitgud.io>
Date: Mon, 25 Mar 2024 01:57:41 +0000
Subject: [PATCH] Watcher live reload

---
 devTools/scripts/advancedCompiler.js  |  3 +++
 devTools/scripts/setup.js             | 11 ++++++++
 devTools/scripts/watcher.js           | 38 ++++++++++++++++++++++-----
 devTools/scripts/watcherLiveReload.js | 20 ++++++++++++++
 gulpfile.js                           | 18 +++++++++++++
 5 files changed, 83 insertions(+), 7 deletions(-)
 create mode 100644 devTools/scripts/watcherLiveReload.js

diff --git a/devTools/scripts/advancedCompiler.js b/devTools/scripts/advancedCompiler.js
index a888e473cb8..c16d95165c7 100644
--- a/devTools/scripts/advancedCompiler.js
+++ b/devTools/scripts/advancedCompiler.js
@@ -89,6 +89,9 @@ if (settings.compilerMode === "simple") {
 	if (settings.compilerFilenamePmodVersion === true) {
 		command += ` --pmodversion`;
 	}
+	if (settings.WatcherLiveReload === true) {
+		command += ` --inject-live-reload`;
+	}
 	command += ` --filename=${args.filename}`;
 }
 
diff --git a/devTools/scripts/setup.js b/devTools/scripts/setup.js
index 43d52ff6cc2..1ae3eee36ad 100644
--- a/devTools/scripts/setup.js
+++ b/devTools/scripts/setup.js
@@ -47,6 +47,7 @@ const args = yargs(hideBin(process.argv))
  * @property {boolean} checksOnlyChangedTypescript If true then we will only check changed lines
  * @property {-1|0|1} precommitHookEnabled 0 = Disabled, 1 = Enabled, -1 = temporarily disabled
  * @property {string} FCHostPath Path to FCHost's directory
+ * @property {boolean} WatcherLiveReload If true then the watcher will trigger a live reload of FC each time it gets re-compiled
  */
 
 // TODO:@franklygeorge Do we want an extensions.json file for VSCode?
@@ -79,6 +80,7 @@ const settings = {
 	checksOnlyChangedTypescript: true,
 	precommitHookEnabled: 1,
 	FCHostPath: "FCHost/fchost/Release",
+	WatcherLiveReload: false,
 };
 
 // create settings.json if it doesn't exist
@@ -455,6 +457,10 @@ async function MiscSettings() {
 	} else {
 		choices.push("Automatically pulling upstream pregmod-master branch. Sanity checks can report changed lines");
 	}
+	choices.push(settings.WatcherLiveReload
+		? "Watcher is triggering a live reload on each successful build"
+		: "Watcher is not triggering a live reload on each successful build"
+	);
 
 	choices.push("Back");
 
@@ -492,6 +498,11 @@ async function MiscSettings() {
 		} else {
 			settings.fetchUpstreamBranch += 1;
 		}
+	} else if (
+		MiscMenuChoice === "Watcher is triggering a live reload on each successful build" ||
+		MiscMenuChoice === "Watcher is not triggering a live reload on each successful build"
+	) {
+		settings.WatcherLiveReload = !settings.WatcherLiveReload;
 	} else if (MiscMenuChoice === "Back") {
 		return;
 	}
diff --git a/devTools/scripts/watcher.js b/devTools/scripts/watcher.js
index 3395f342970..a4bd1603cb6 100644
--- a/devTools/scripts/watcher.js
+++ b/devTools/scripts/watcher.js
@@ -7,6 +7,7 @@ import jetpack from "fs-jetpack";
 import watch from "node-watch";
 import {execSync, spawn} from "child_process";
 import * as path from "path";
+import * as http from "http";
 
 const batSh = (process.platform === "win32") ? "bat" : "sh";
 
@@ -20,6 +21,22 @@ execSync("node devTools/scripts/setup.js --settings");
 /** @type {import("./setup.js").Settings} */
 const settings = jetpack.read("settings.json", "json");
 
+let needsReloaded = false;
+
+if (settings.WatcherLiveReload === true) {
+	// start http server for live reloading
+	let app = http.createServer((req, res) => {
+		res.setHeader("Content-Type", "application/json");
+		res.setHeader("Access-Control-Allow-Origin", "*");
+		res.end(JSON.stringify({needsReloaded: needsReloaded}, null, 2));
+		needsReloaded = false;
+	});
+	app.listen(16969);
+	console.log("Live reload server listening on port 15959");
+} else {
+	console.log("Live reloading is disabled");
+}
+
 /**
  * Builds FC using the advanced compiler
  */
@@ -29,15 +46,23 @@ function build() {
 	if (buildProcess !== undefined) {
 		buildProcess.kill();
 	}
-	buildProcess = spawn("node", ["devTools/scripts/advancedCompiler.js", "--filename=FC_pregmod.watcher.html", "--no-interaction"], {
-		stdio: ['inherit', 'inherit', 'inherit'],
-	  });
+	buildProcess = spawn(
+		"node",
+		["devTools/scripts/advancedCompiler.js", "--filename=FC_pregmod.watcher.html", "--no-interaction"],
+		{
+			stdio: ['inherit', 'inherit', 'inherit'],
+		}
+	);
 
-	buildProcess.on('exit', function (code) {
+	buildProcess.on('exit', function(code) {
 		if (code === null) {
 			return;
 		} else if (code === 0) {
 			console.log(`Saving changes in "setup.${batSh}" will toggle a rebuild`);
+			if (settings.WatcherLiveReload === true) {
+				// trigger a reload
+				needsReloaded = true;
+			}
 		} else {
 			console.log('Compiler exited with code:', code);
 		}
@@ -46,6 +71,7 @@ function build() {
 
 const watcher = watch(".", {
 	recursive: true,
+	// eslint-disable-next-line jsdoc/require-jsdoc
 	filter(f, skip) {
 		if (
 			f.startsWith("src") ||
@@ -71,8 +97,6 @@ const watcher = watch(".", {
 	}
 });
 
-// TODO:@franklygeorge optional launching of a live reloading web server
-
 watcher.on("change", function(event, filename) {
 	filename = filename.toString();
 
@@ -87,4 +111,4 @@ console.log("Watching for changes");
 console.log("");
 
 // run build when we first startup
-build()
+build();
diff --git a/devTools/scripts/watcherLiveReload.js b/devTools/scripts/watcherLiveReload.js
new file mode 100644
index 00000000000..a6c8ada97e5
--- /dev/null
+++ b/devTools/scripts/watcherLiveReload.js
@@ -0,0 +1,20 @@
+/** @file This is injected into FC to allow the watchers live reload functionality */
+
+const liveReloadUrl = "http://localhost:16969";
+
+function checkForLiveReload() {
+	try {
+		$.getJSON(liveReloadUrl, function(data) {
+			if (data !== undefined && "needsReloaded" in data && data["needsReloaded"] === true) {
+				// reload
+				location.reload();
+			}
+		});
+	} catch (e) {
+		// fail silently
+		// this can happen for many reasons. The main one being that the server has been closed while FC has been left open
+	}
+}
+
+// query server every 2 seconds and see if we need to reload
+window.setInterval(checkForLiveReload, 2000);
diff --git a/gulpfile.js b/gulpfile.js
index d63dee4f59c..92beb2c5693 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -92,6 +92,11 @@ const args = yargs(hideBin(process.argv))
 		description: 'The filename to save the compiled HTML file as',
 		default: cfg.output,
 	})
+	.option('inject-live-reload', {
+		type: 'boolean',
+		description: 'Injects code used by the watcher to live reload FC',
+		default: false,
+	})
 	// commands should exist as exported gulp tasks
 	.command('html', "Build FC")
 	.command('themes', "Build themes")
@@ -215,6 +220,19 @@ function processScripts(srcGlob, destDir, destFileName) {
 	const addSourcemaps = !args.release;
 	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf("*")));
 
+	if (args.injectLiveReload === true && destFileName === "module-script.js") {
+		const liveReloadScriptPath = "devTools/scripts/watcherLiveReload.js";
+		if (jetpack.exists(liveReloadScriptPath) === "file") {
+			log.info("Injecting live reload code");
+			if (typeof srcGlob === "string") {
+				srcGlob = [srcGlob];
+			}
+			srcGlob.push(liveReloadScriptPath);
+		} else {
+			log.error(`Live reload script is missing from "${liveReloadScriptPath}"!`);
+		}
+	}
+
 	return gulp.src(srcGlob)
 		.pipe(args.debug
 			? noop()
-- 
GitLab