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
+70 -59
View File
@@ -1,6 +1,7 @@
import express from "express";
import cors from "cors";
import { createServer } from "http";
import type { Server } from "http";
import path from "path";
import { fileURLToPath } from "url";
import { WebSocketServer } from "ws";
@@ -21,72 +22,82 @@ import { loadTlkIndex } from "./nwscript/tlk-index.js";
import { getRepoPath } from "./services/workspace.service.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const server = createServer(app);
app.use(cors());
app.use(express.json());
export function startServer(port: number): Promise<Server> {
const app = express();
const server = createServer(app);
initWebSocket(server);
app.use(cors());
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.json({
status: "ok",
wsClients: getClientCount(),
uptime: process.uptime(),
initWebSocket(server);
app.get("/api/health", (_req, res) => {
res.json({
status: "ok",
wsClients: getClientCount(),
uptime: process.uptime(),
});
});
});
app.use("/api/workspace", workspaceRouter);
app.use("/api/docker", dockerRouter);
app.use("/api/editor", editorRouter);
app.use("/api/terminal", terminalRouter);
app.use("/api/build", buildRouter);
app.use("/api/server", serverRouter);
app.use("/api/toolset", toolsetRouter);
app.use("/api/github", githubRouter);
app.use("/api/repos", reposRouter);
app.use("/api/workspace", workspaceRouter);
app.use("/api/docker", dockerRouter);
app.use("/api/editor", editorRouter);
app.use("/api/terminal", terminalRouter);
app.use("/api/build", buildRouter);
app.use("/api/server", serverRouter);
app.use("/api/toolset", toolsetRouter);
app.use("/api/github", githubRouter);
app.use("/api/repos", reposRouter);
const frontendDist = path.resolve(__dirname, "../../frontend/dist");
app.use(express.static(frontendDist));
app.get("*path", (_req, res) => {
res.sendFile(path.join(frontendDist, "index.html"));
});
const frontendDist = path.resolve(__dirname, "../../frontend/dist");
app.use(express.static(frontendDist));
app.get("*path", (_req, res) => {
res.sendFile(path.join(frontendDist, "index.html"));
});
server.on("upgrade", (request, socket, head) => {
const url = new URL(request.url || "", `http://${request.headers.host}`);
server.on("upgrade", (request, socket, head) => {
const url = new URL(request.url || "", `http://${request.headers.host}`);
if (url.pathname === "/ws/lsp") {
const lspWss = new WebSocketServer({ noServer: true });
lspWss.handleUpgrade(request, socket, head, (ws) => {
attachLspWebSocket(ws);
if (url.pathname === "/ws/lsp") {
const lspWss = new WebSocketServer({ noServer: true });
lspWss.handleUpgrade(request, socket, head, (ws) => {
attachLspWebSocket(ws);
});
return;
}
const termMatch = url.pathname.match(/^\/ws\/terminal\/(.+)$/);
if (termMatch) {
const sessionId = termMatch[1];
const termWss = new WebSocketServer({ noServer: true });
termWss.handleUpgrade(request, socket, head, (ws) => {
if (!attachWebSocket(sessionId, ws)) {
createTerminalSession(sessionId);
attachWebSocket(sessionId, ws);
}
});
return;
}
if (url.pathname === "/ws") {
handleEventUpgrade(request, socket, head);
return;
}
});
return new Promise((resolve) => {
server.listen(port, "0.0.0.0", () => {
console.log(`Layonara Forge listening on http://0.0.0.0:${port}`);
startUpstreamPolling();
const tlkPath = getRepoPath("nwn-haks", "layonara.tlk.json");
loadTlkIndex(tlkPath).then(() => console.log(`TLK index loaded`)).catch(() => {});
resolve(server);
});
return;
}
});
}
const termMatch = url.pathname.match(/^\/ws\/terminal\/(.+)$/);
if (termMatch) {
const sessionId = termMatch[1];
const termWss = new WebSocketServer({ noServer: true });
termWss.handleUpgrade(request, socket, head, (ws) => {
if (!attachWebSocket(sessionId, ws)) {
createTerminalSession(sessionId);
attachWebSocket(sessionId, ws);
}
});
return;
}
if (url.pathname === "/ws") {
handleEventUpgrade(request, socket, head);
return;
}
});
const PORT = parseInt(process.env.PORT || "3000", 10);
server.listen(PORT, "0.0.0.0", () => {
console.log(`Layonara Forge listening on http://0.0.0.0:${PORT}`);
startUpstreamPolling();
const tlkPath = getRepoPath("nwn-haks", "layonara.tlk.json");
loadTlkIndex(tlkPath).then(() => console.log(`TLK index loaded`)).catch(() => {});
});
if (!process.env.ELECTRON) {
const PORT = parseInt(process.env.PORT || "3000", 10);
startServer(PORT);
}