diff --git a/electron/package-lock.json b/electron/package-lock.json index 9faab32c..c39fd9b0 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "chokidar": "^4.0.3", "semver": "^7.7.1", "zod": "^3.24.2" }, @@ -192,6 +193,21 @@ "node": ">=8" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -733,6 +749,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", diff --git a/electron/package.json b/electron/package.json index 13cfffa3..81b11d18 100644 --- a/electron/package.json +++ b/electron/package.json @@ -9,6 +9,7 @@ "start": "tsc && electron ." }, "dependencies": { + "chokidar": "^4.0.3", "semver": "^7.7.1", "zod": "^3.24.2" }, diff --git a/electron/src/index.ts b/electron/src/index.ts index 884ed847..6dbb3388 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -56,6 +56,12 @@ function createWindow() { ipc.install(window); window.loadURL(pageUrl); + modLoader.on("forcereload", () => { + // TODO: Find a better way to manage cache when force + // reloading (use a non-persistent session?) + window.webContents.session.clearData({ dataTypes: ["cache"] }).then(() => window.reload()); + }); + // Redirect any kind of main frame navigation to external applications window.webContents.on("will-navigate", (ev, url) => { if (url === window.webContents.getURL()) { diff --git a/electron/src/mods/loader.ts b/electron/src/mods/loader.ts index 434860ef..093df329 100644 --- a/electron/src/mods/loader.ts +++ b/electron/src/mods/loader.ts @@ -1,3 +1,4 @@ +import EventEmitter from "node:events"; import fs from "node:fs/promises"; import path from "node:path"; import { DevelopmentModLocator, DistroModLocator, ModLocator, UserModLocator } from "./locator.js"; @@ -48,14 +49,29 @@ class Mod { } } -export class ModLoader { +export class ModLoader extends EventEmitter { private mods: Mod[] = []; private readonly locators = new Map(); constructor() { + super(); + this.locators.set("user", new UserModLocator()); this.locators.set("distro", new DistroModLocator()); - this.locators.set("dev", new DevelopmentModLocator()); + + const devLocator = new DevelopmentModLocator(); + this.locators.set("dev", devLocator); + + // If requested, restart automatically when dev mods are modified + devLocator.fsWatcher?.on("all", () => this.forceReload()); + } + + /** + * Resets modloader state and reloads all mods, then triggers page reload. + */ + async forceReload() { + await this.loadMods(); + this.emit("forcereload"); } async loadMods(): Promise { diff --git a/electron/src/mods/locator.ts b/electron/src/mods/locator.ts index e992420c..66dcd562 100644 --- a/electron/src/mods/locator.ts +++ b/electron/src/mods/locator.ts @@ -1,3 +1,4 @@ +import chokidar, { FSWatcher } from "chokidar"; import { app } from "electron"; import fs from "node:fs/promises"; import path from "node:path"; @@ -10,6 +11,7 @@ const USER_MODS_DIR = path.join(userData, "mods"); const DISTRO_MODS_DIR = path.join(executableDir, "mods"); const DEV_SWITCH = "load-mod"; +const DEV_WATCH_SWITCH = "watch"; const DEV_USER_MOD_PREFIX = "@/"; export interface ModLocator { @@ -153,6 +155,7 @@ export class DistroModLocator extends DirectoryModLocator { export class DevelopmentModLocator implements ModLocator { readonly priority = 0; + readonly fsWatcher: FSWatcher | null = null; private readonly modFiles: string[] = []; private readonly disabledMods = new Set(); @@ -166,6 +169,19 @@ export class DevelopmentModLocator implements ModLocator { const resolved = switchValue.split(",").map(f => this.resolveFile(f)); this.modFiles.push(...resolved); + + const watchMode = app.commandLine.hasSwitch(DEV_WATCH_SWITCH); + if (!watchMode || this.modFiles.length === 0) { + // Skip setting up chokidar + return; + } + + this.fsWatcher = chokidar.watch(this.modFiles, { + persistent: false, + ignoreInitial: true, + awaitWriteFinish: true, + atomic: true, + }); } locateMods(): Promise {