Files
CPLv4.0/scripts/local-cpl-sim.mjs

315 lines
9.0 KiB
JavaScript

// Local simulator that serves the exported build and replaces CGI placeholders from mocks/.
// - Serves the exported "out" folder.
// - Replaces placeholders like <%=SAV00%>, <%=SAN01%>, DANxx/DASxx etc. using mocks/device-cgi-simulator/**.
// - Supports mapping /CPL?/path to /path in the export.
import http from "http";
import fs from "fs";
import path from "path";
const PORT = process.env.PORT ? Number(process.env.PORT) : 3030;
const ROOT = path.join(process.cwd(), "out");
function exists(p) {
try {
fs.accessSync(p, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
function contentTypeByExt(ext) {
switch (ext) {
case ".html":
return "text/html; charset=utf-8";
case ".js":
return "application/javascript; charset=utf-8";
case ".json":
return "application/json; charset=utf-8";
case ".css":
return "text/css; charset=utf-8";
case ".txt":
return "text/plain; charset=utf-8";
case ".svg":
return "image/svg+xml";
case ".png":
return "image/png";
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".ico":
return "image/x-icon";
default:
return "application/octet-stream";
}
}
function isTextExt(ext) {
return [".html", ".js", ".json", ".css", ".txt", ".svg"].includes(ext);
}
function readJsonSafe(fp) {
try {
return JSON.parse(fs.readFileSync(fp, "utf8"));
} catch {
return {};
}
}
function getRelFromRoot(filePath) {
try {
const r = path.relative(ROOT, filePath).split(path.sep).join("/");
return r.startsWith("../") ? r : r;
} catch {
return "";
}
}
function tryServeSpecialMocks(res, relPath) {
const rp = relPath.replace(/^\/+/, "");
// Serve ready JS/JSON mocks for some routes
if (rp === "CPL/SERVICE/systemVoltTemp.js") {
const mockPath = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
"SERVICE",
"systemVoltTempMockData.js"
);
if (exists(mockPath)) return streamRaw(res, mockPath);
}
if (rp === "CPL/SERVICE/last20Messages.js") {
const mockPath = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
"SERVICE",
"last20MessagesMockData.js"
);
if (exists(mockPath)) return streamRaw(res, mockPath);
}
if (rp === "CPL/meldungen/messages_all.json") {
const mockPath = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
"meldungen",
"messages_all.json"
);
if (exists(mockPath)) return streamRaw(res, mockPath);
}
if (rp.startsWith("CPL/chartsData/analogInputs/")) {
const mockPath = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
rp.replace(/^CPL\//, "")
);
if (exists(mockPath)) return streamRaw(res, mockPath);
}
return false;
}
function streamRaw(res, filePath) {
const ext = path.extname(filePath).toLowerCase();
const type = contentTypeByExt(ext);
res.writeHead(200, { "Content-Type": type });
fs.createReadStream(filePath).pipe(res);
return true;
}
function resolvePlaceholders(filePath, content) {
const rel = getRelFromRoot(filePath);
const serviceSystem =
/CPL\/SERVICE\/(System\.(js|json)|system\.(js|json))$/i.test(rel);
const serviceOpcua = /CPL\/SERVICE\/opcua\.(js|json)$/i.test(rel);
const serviceDO = /CPL\/SERVICE\/digitalOutputs\.(js|json)$/i.test(rel);
let data = {};
let replacer = (m) => m;
if (serviceSystem || /\.html$/i.test(rel)) {
const baseDir = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
"SERVICE"
);
const sysJson = exists(path.join(baseDir, "SystemMockData.json"))
? path.join(baseDir, "SystemMockData.json")
: path.join(baseDir, "systemMockData.json");
data = exists(sysJson) ? readJsonSafe(sysJson) : {};
const map = {
SAV00: "win_appVersion",
SAN01: "win_deviceName",
SEM01: "win_mac1",
SEI01: "win_ip",
SES01: "win_subnet",
SEG01: "win_gateway",
SCL01: "win_cplInternalTimestamp",
STP01: "win_ntp1",
STP02: "win_ntp2",
STP03: "win_ntp3",
STT00: "win_ntpTimezone",
STA00: "win_ntpActive",
SOS01: "win_opcState",
SOC01: "win_opcSessions",
SON01: "win_opcName",
};
replacer = (m, key) => {
if (Object.prototype.hasOwnProperty.call(data, key))
return String(data[key]);
const prop = map[key];
if (prop && Object.prototype.hasOwnProperty.call(data, prop))
return String(data[prop]);
return m;
};
} else if (serviceOpcua) {
const json = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
"SERVICE",
"opcuaMockData.json"
);
data = exists(json) ? readJsonSafe(json) : {};
const map = {
SOS01: "win_opcUaZustand",
SOC01: "win_opcUaActiveClientCount",
SON01: "win_opcUaNodesetName",
};
replacer = (m, key) => {
if (Object.prototype.hasOwnProperty.call(data, key))
return String(data[key]);
const prop = map[key];
if (prop && Object.prototype.hasOwnProperty.call(data, prop))
return String(data[prop]);
return m;
};
} else if (serviceDO) {
const json = path.join(
process.cwd(),
"mocks",
"device-cgi-simulator",
"SERVICE",
"digitalOutputsMockData.json"
);
data = exists(json) ? readJsonSafe(json) : {};
replacer = (m, key) => {
// DASnn -> win_da_state[n-1]
if (/^DAS\d{2}$/i.test(key)) {
const idx = parseInt(key.slice(3), 10) - 1;
const arr = data.win_da_state;
if (Array.isArray(arr) && idx >= 0 && idx < arr.length)
return String(arr[idx]);
}
// DANnn -> string label (ensure proper quoting in JS array)
if (/^DAN\d{2}$/i.test(key)) {
const idx = parseInt(key.slice(3), 10) - 1;
const arr = data.win_da_bezeichnung;
if (Array.isArray(arr) && idx >= 0 && idx < arr.length)
return JSON.stringify(arr[idx] ?? "");
}
return m;
};
} else {
// Default: no change
replacer = (m) => m;
}
return content.replace(/<%=([A-Z0-9_]+)%>/g, replacer);
}
function sendFileWithReplace(res, filePath) {
const ext = path.extname(filePath).toLowerCase();
const type = contentTypeByExt(ext);
if (isTextExt(ext)) {
try {
let content = fs.readFileSync(filePath, "utf8");
content = resolvePlaceholders(filePath, content);
res.writeHead(200, { "Content-Type": type });
res.end(content);
return;
} catch {
// fall through to stream as binary if read fails
}
}
res.writeHead(200, { "Content-Type": type });
fs.createReadStream(filePath).pipe(res);
}
function notFound(res) {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not Found");
}
const server = http.createServer((req, res) => {
try {
const url = new URL(req.url, `http://localhost:${PORT}`);
const pathname = decodeURIComponent(url.pathname);
const rawQuery = req.url.includes("?") ? req.url.split("?")[1] : "";
// CPL? mapping: /CPL?/CPL/... -> /CPL/...
if (pathname === "/CPL" && rawQuery) {
const q = decodeURIComponent(rawQuery);
if (q.startsWith("/")) {
const rel = q.replace(/^\/+/, "");
// Special mocks
if (tryServeSpecialMocks(res, rel)) return;
const target = path.join(ROOT, rel);
if (exists(target) && fs.statSync(target).isFile()) {
return sendFileWithReplace(res, target);
}
if (exists(target) && fs.statSync(target).isDirectory()) {
const indexFile = path.join(target, "index.html");
if (exists(indexFile)) return sendFileWithReplace(res, indexFile);
}
return notFound(res);
}
// Non-file commands: just 200 OK
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("OK");
return;
}
// Static serving from export
let filePath = path.join(ROOT, pathname);
if (pathname.endsWith("/")) {
filePath = path.join(filePath, "index.html");
if (exists(filePath)) return sendFileWithReplace(res, filePath);
}
// Special mocks outside CPL?
const relStatic = pathname.replace(/^\/+/, "");
if (tryServeSpecialMocks(res, relStatic)) return;
if (exists(filePath) && fs.statSync(filePath).isFile()) {
return sendFileWithReplace(res, filePath);
}
const htmlVariant = filePath + ".html";
if (exists(htmlVariant)) {
return sendFileWithReplace(res, htmlVariant);
}
// Fallback: out/index.html
const fallback = path.join(ROOT, "index.html");
if (exists(fallback)) return sendFileWithReplace(res, fallback);
return notFound(res);
} catch (err) {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end(
"Internal Server Error\n" + (err && err.stack ? err.stack : String(err))
);
}
});
server.listen(PORT, () => {
console.log(`Local CPL simulator running on http://localhost:${PORT}`);
console.log(`Serving from: ${ROOT}`);
console.log("Replacing CGI placeholders using mocks/ where available");
});