Layonara Forge — NWN Development IDE

Electron desktop application for Neverwinter Nights module development.
Clone, edit, build, and run a complete Layonara NWNX server with only
Docker required.

- React 19 + Vite frontend with Monaco editor and NWScript LSP
- Node.js + Express backend managing Docker sibling containers
- Electron shell with Docker availability check and auto-setup
- Builder image auto-builds on first use from bundled Dockerfile
- Cross-platform: Windows (.exe), macOS (.dmg), Linux (.AppImage)
- Gitea Actions CI for automated release builds
This commit is contained in:
plenarius
2026-04-21 12:14:38 -04:00
parent f39f1d818b
commit f851d8b8f2
62 changed files with 5519 additions and 5687 deletions
+131
View File
@@ -0,0 +1,131 @@
import { app, BrowserWindow, shell, dialog } from "electron";
import path from "path";
import { checkDocker, dockerDownloadUrl } from "./docker-check";
import { defaultWorkspacePath, detectNwnHome } from "./paths";
let mainWindow: BrowserWindow | null = null;
let serverPort: number;
function setEnvironment(): void {
if (!process.env.WORKSPACE_PATH) {
process.env.WORKSPACE_PATH = defaultWorkspacePath();
}
if (!process.env.NWN_HOME_PATH) {
const detected = detectNwnHome();
if (detected) process.env.NWN_HOME_PATH = detected;
}
if (!process.env.GIT_PROVIDER_URL) {
process.env.GIT_PROVIDER_URL = "https://gitea.layonara.com";
}
process.env.ELECTRON = "1";
}
async function startBackend(): Promise<number> {
const { startServer } = await import(
path.join(__dirname, "../packages/backend/dist/index.js")
);
const port = await findFreePort();
await startServer(port);
return port;
}
function findFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const net = require("net");
const srv = net.createServer();
srv.listen(0, () => {
const port = srv.address().port;
srv.close(() => resolve(port));
});
srv.on("error", reject);
});
}
function createWindow(port: number): BrowserWindow {
const win = new BrowserWindow({
width: 1280,
height: 900,
minWidth: 960,
minHeight: 600,
title: "Layonara Forge",
icon: path.join(__dirname, "../assets/icon.png"),
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadURL(`http://localhost:${port}`);
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("http")) shell.openExternal(url);
return { action: "deny" };
});
win.on("closed", () => {
mainWindow = null;
});
return win;
}
async function showDockerMissing(): Promise<boolean> {
const url = dockerDownloadUrl();
const { response } = await dialog.showMessageBox({
type: "warning",
title: "Docker Required",
message: "Layonara Forge requires Docker to build modules and run the NWN server.",
detail: "Docker Desktop must be installed and running before using the Forge.\n\nClick \"Download Docker\" to open the download page, or \"Check Again\" after installing.",
buttons: ["Download Docker", "Check Again", "Quit"],
defaultId: 0,
cancelId: 2,
});
if (response === 0) {
shell.openExternal(url);
return showDockerMissing();
}
if (response === 1) {
const status = await checkDocker();
if (status.available) return true;
return showDockerMissing();
}
return false;
}
app.whenReady().then(async () => {
setEnvironment();
const docker = await checkDocker();
if (!docker.available) {
const installed = await showDockerMissing();
if (!installed) {
app.quit();
return;
}
}
try {
serverPort = await startBackend();
} catch (err: any) {
dialog.showErrorBox(
"Forge Startup Error",
`Failed to start the backend server:\n\n${err.message}`,
);
app.quit();
return;
}
mainWindow = createWindow(serverPort);
});
app.on("window-all-closed", () => {
app.quit();
});
app.on("activate", () => {
if (mainWindow === null && serverPort) {
mainWindow = createWindow(serverPort);
}
});