f851d8b8f2
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
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
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";
|
|
import { initWebSocket, getClientCount, handleUpgrade as handleEventUpgrade } from "./services/ws.service.js";
|
|
import workspaceRouter from "./routes/workspace.js";
|
|
import dockerRouter from "./routes/docker.js";
|
|
import editorRouter from "./routes/editor.js";
|
|
import terminalRouter from "./routes/terminal.js";
|
|
import buildRouter from "./routes/build.js";
|
|
import serverRouter from "./routes/server.js";
|
|
import toolsetRouter from "./routes/toolset.js";
|
|
import githubRouter from "./routes/github.js";
|
|
import reposRouter from "./routes/repos.js";
|
|
import { attachWebSocket, createTerminalSession } from "./services/terminal.service.js";
|
|
import { attachLspWebSocket } from "./services/lsp.service.js";
|
|
import { startUpstreamPolling } from "./services/git.service.js";
|
|
import { loadTlkIndex } from "./nwscript/tlk-index.js";
|
|
import { getRepoPath } from "./services/workspace.service.js";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
export function startServer(port: number): Promise<Server> {
|
|
const app = express();
|
|
const server = createServer(app);
|
|
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
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);
|
|
|
|
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}`);
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!process.env.ELECTRON) {
|
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
startServer(PORT);
|
|
}
|