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 FORMS_DIR = "forms"; const FRAMEBASE_DIR = "FrameBase"; const LIB_DIR = "lib"; 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", FORMS_DIR, "frame", FRAMEBASE_DIR, "lib", LIB_DIR].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 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(component.type)) { attrs.push(`displaynulltext="${escapeXml(component.prompt)}"`); } if (component.bind && ["Edit", "MaskEdit", "TextArea"].includes(component.type)) { const [, columnId] = component.bind.split("."); attrs.push(`value="bind:${columnId}"`); } if (component.type === "Edit" && component.id.toLowerCase().includes("password")) { attrs.push('password="true"'); } if (component.type === "Combo") { attrs.push('codecolumn="code"'); attrs.push('datacolumn="label"'); } return ` <${component.type} ${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 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; return [ '', '', `
`, " ", datasets || " ", " ", " ", components, grids, " ", ` `, " ", "
", "
", "" ].join("\n"); } function renderTopFrame(appSpec) { return [ '', '', `
`, ` `, ' ', " ", ` `, " ", " ", "
", "" ].join("\n"); } function renderLeftFrame(forms) { const menuItems = forms .map( (form, index) => ` ` ) .join("\n"); return [ '', '', `
`, ' ', menuItems, " ", ' ', " ", " ", "
", "" ].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, FORMS_DIR)); ensureDir(path.join(baseOutputDir, FRAMEBASE_DIR)); ensureDir(path.join(baseOutputDir, LIB_DIR)); 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, LIB_DIR, "common.xjs"), readTemplate(path.join("common", "common.xjs.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, FORMS_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 };