const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const repoRoot = path.resolve(__dirname, "../..");
const specDir = path.join(repoRoot, "spec", "nexacro");
const templateDir = path.join(repoRoot, "templates", "nexacro");
const outputDir = path.join(repoRoot, "client", "nexacro-src");
const previewDir = path.join(repoRoot, "client", "nexacro-deploy");
const UTF8_BOM = "\uFEFF";
const FRAME_TOP_HEIGHT = 56;
const FRAME_LEFT_WIDTH = 240;
const BASE_DIR = "Base";
const FRAMEBASE_DIR = "FrameBase";
function readYaml(filePath) {
return yaml.load(fs.readFileSync(filePath, "utf8"));
}
function loadSpecs(baseDir = specDir) {
const appSpec = readYaml(path.join(baseDir, "app.yaml"));
const formFiles = fs
.readdirSync(baseDir)
.filter((file) => file.endsWith(".yaml") && file !== "app.yaml")
.sort();
const forms = formFiles.map((file) => readYaml(path.join(baseDir, file)));
return { appSpec, forms };
}
function readTemplate(relativePath) {
return fs.readFileSync(path.join(templateDir, relativePath), "utf8");
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function removePathIfExists(targetPath) {
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { recursive: true, force: true });
}
}
function writeGeneratedFile(filePath, content, options = {}) {
const includeBom = options.bom !== false;
const payload = includeBom ? `${UTF8_BOM}${content}` : content;
fs.writeFileSync(filePath, payload);
}
function cleanGeneratedOutput(baseOutputDir, projectName) {
[
`${projectName}.xprj`,
"application.xadl",
"Application_Desktop.xadl",
"environment.xml",
"typedefinition.xml",
"appvariables.xml",
`${projectName}.xprj.bak`,
"$Geninfo$.geninfo",
".DS_Store"
].forEach((file) => removePathIfExists(path.join(baseOutputDir, file)));
["forms", BASE_DIR, "frame", FRAMEBASE_DIR, "lib"].forEach((dir) =>
removePathIfExists(path.join(baseOutputDir, dir))
);
}
function renderTemplate(template, values) {
return Object.entries(values).reduce((content, [key, value]) => {
return content.replaceAll(`{{${key}}}`, value);
}, template);
}
function escapeXml(value = "") {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function indentBlock(content, size = 4) {
const indent = " ".repeat(size);
return content
.split("\n")
.map((line) => (line ? `${indent}${line}` : line))
.join("\n");
}
function toXfdlDataset(dataset) {
const columns = (dataset.columns || [])
.map((column) => ` `)
.join("\n");
return [
` `,
" ",
columns,
" ",
" "
].join("\n");
}
function componentTag(component, index = 0) {
const componentType = component.type === "FileUpload" ? "Edit" : component.type;
const attrs = [
`id="${component.id}"`,
`taborder="${index}"`,
`left="${component.left ?? 0}"`,
`top="${component.top ?? 0}"`,
`width="${component.width ?? 120}"`,
`height="${component.height ?? 32}"`
];
if (component.text) {
attrs.push(`text="${escapeXml(component.text)}"`);
}
if (component.prompt && ["Edit", "MaskEdit", "TextArea"].includes(componentType)) {
attrs.push(`displaynulltext="${escapeXml(component.prompt)}"`);
}
if (componentType === "Edit" && component.id.toLowerCase().includes("password")) {
attrs.push('password="true"');
}
if (componentType === "Combo") {
attrs.push('codecolumn="code"');
attrs.push('datacolumn="label"');
}
if (component.type === "FileUpload") {
attrs.push('readonly="true"');
}
return ` <${componentType} ${attrs.join(" ")}/>`;
}
function gridFormats(grid) {
const columns = grid.columns || [];
const width = Math.max(120, Math.floor((grid.width || 800) / Math.max(columns.length, 1)));
const columnXml = columns.map(() => ``).join("");
const headerCells = columns
.map((column, index) => ` | `)
.join("");
const bodyCells = columns
.map((column, index) => ` | `)
.join("");
return `${columnXml}|
${headerCells}${bodyCells}`;
}
function gridTag(grid) {
return [
` `,
` `,
` ${gridFormats(grid)}`,
" "
].join("\n");
}
function buildFormScript(form) {
const transactions = (form.transactions || [])
.map(
(transaction) =>
`this.${transaction.id} = function(obj, e)\n{\n this.gfnShowMessage("${escapeXml(transaction.id)} -> ${escapeXml(transaction.endpoint)}");\n};`
)
.join("\n\n");
const actions = (form.actions || [])
.map(
(action) =>
`this.${action.id} = function(obj, e)\n{\n this.gfnShowMessage("${escapeXml(action.label)}");\n};`
)
.join("\n\n");
return [
"this.Form_onload = function(obj, e)",
"{",
` this.gfnShowMessage("${escapeXml(form.title)} loaded");`,
"};",
"",
transactions,
"",
actions
]
.join("\n")
.trim();
}
function renderMessageStatics(form, layout) {
return (form.messages || [])
.map(
(message, index) =>
` `
)
.join("\n");
}
function renderXfdl(form, appSpec) {
const datasets = (form.datasets || []).map(toXfdlDataset).join("\n");
const components = (form.components || []).map((component, index) => componentTag(component, index)).join("\n");
const grids = (form.grids || []).map(gridTag).join("\n");
const layout = form.layout || appSpec.layout;
const messages = renderMessageStatics(form, layout);
return [
'',
'',
` ",
"",
""
].join("\n");
}
function renderTopFrame(appSpec) {
return [
'',
'',
` ",
"",
""
].join("\n");
}
function renderLeftFrame(forms) {
const menuItems = forms
.map(
(form, index) =>
` `
)
.join("\n");
return [
'',
'',
` ",
"",
""
].join("\n");
}
function renderWorkFrame(appSpec, defaultFormId) {
const workWidth = Math.max(appSpec.layout.width - FRAME_LEFT_WIDTH, 800);
const workHeight = Math.max(appSpec.layout.height - FRAME_TOP_HEIGHT, 640);
return [
'',
'',
` ",
"",
""
].join("\n");
}
function generateProjectFiles(appSpec, forms, baseOutputDir = outputDir) {
cleanGeneratedOutput(baseOutputDir, appSpec.projectName);
ensureDir(baseOutputDir);
ensureDir(path.join(baseOutputDir, BASE_DIR));
ensureDir(path.join(baseOutputDir, FRAMEBASE_DIR));
ensureDir(path.join(baseOutputDir, "_extlib_"));
writeGeneratedFile(
path.join(baseOutputDir, `${appSpec.projectName}.xprj`),
renderTemplate(readTemplate("project.xml.tpl"), {
projectName: appSpec.projectName
})
);
writeGeneratedFile(
path.join(baseOutputDir, "Application_Desktop.xadl"),
renderTemplate(readTemplate("application.xadl.tpl"), {
appTitle: appSpec.appTitle,
width: String(appSpec.layout.width),
height: String(appSpec.layout.height),
topHeight: String(FRAME_TOP_HEIGHT),
leftWidth: String(FRAME_LEFT_WIDTH)
})
);
writeGeneratedFile(
path.join(baseOutputDir, "environment.xml"),
renderTemplate(readTemplate("environment.xml.tpl"), {
themeId: appSpec.themeId
})
);
writeGeneratedFile(path.join(baseOutputDir, "typedefinition.xml"), readTemplate("typedefinition.xml.tpl"));
writeGeneratedFile(path.join(baseOutputDir, "appvariables.xml"), readTemplate("appvariables.xml.tpl"), { bom: false });
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Top.xfdl"), renderTopFrame(appSpec));
writeGeneratedFile(path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Left.xfdl"), renderLeftFrame(forms));
const defaultForm = forms.find((form) => form.route === "login") || forms[0];
writeGeneratedFile(
path.join(baseOutputDir, FRAMEBASE_DIR, "Form_Work.xfdl"),
renderWorkFrame(appSpec, defaultForm?.formId || "frmLogin")
);
forms.forEach((form) => {
writeGeneratedFile(path.join(baseOutputDir, BASE_DIR, `${form.formId}.xfdl`), renderXfdl(form, appSpec));
});
}
function previewHtml(appSpec) {
return `
${escapeXml(appSpec.previewTitle)}
`;
}
function previewScript(forms) {
const manifest = JSON.stringify(
forms.map((form) => ({
formId: form.formId,
title: form.title,
route: form.route,
authority: form.authority,
messages: form.messages || []
})),
null,
2
);
return `window.HANWHA_FORMS = ${manifest};
`;
}
function previewCss() {
return readTemplate("preview.css.tpl");
}
function previewAppJs(appSpec) {
return renderTemplate(readTemplate("preview-app.js.tpl"), {
appTitle: appSpec.appTitle
});
}
function generatePreview(appSpec, forms, basePreviewDir = previewDir) {
ensureDir(path.join(basePreviewDir, "assets"));
ensureDir(path.join(basePreviewDir, "sample-data"));
fs.writeFileSync(path.join(basePreviewDir, "index.html"), previewHtml(appSpec));
fs.writeFileSync(path.join(basePreviewDir, "assets", "forms.js"), previewScript(forms));
fs.writeFileSync(path.join(basePreviewDir, "assets", "styles.css"), previewCss());
const appJs = `${fs.readFileSync(path.join(basePreviewDir, "assets", "forms.js"), "utf8")}\n${previewAppJs(appSpec)}`;
fs.writeFileSync(path.join(basePreviewDir, "assets", "app.js"), appJs);
}
function generate() {
const { appSpec, forms } = loadSpecs();
generateProjectFiles(appSpec, forms);
generatePreview(appSpec, forms);
}
if (require.main === module) {
generate();
}
module.exports = {
loadSpecs,
generateProjectFiles,
generatePreview,
generate,
renderXfdl
};