392 lines
13 KiB
JavaScript
392 lines
13 KiB
JavaScript
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) => ` <Column id="${column.id}" type="${column.type || "STRING"}" size="256"/>`)
|
|
.join("\n");
|
|
|
|
return [
|
|
` <Dataset id="${dataset.id}">`,
|
|
" <ColumnInfo>",
|
|
columns,
|
|
" </ColumnInfo>",
|
|
" </Dataset>"
|
|
].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(() => `<Column size="${width}"/>`).join("");
|
|
const headerCells = columns
|
|
.map((column, index) => `<Cell col="${index}" text="${escapeXml(column.text || column.id)}"/>`)
|
|
.join("");
|
|
const bodyCells = columns
|
|
.map((column, index) => `<Cell col="${index}" text="bind:${column.id}"/>`)
|
|
.join("");
|
|
|
|
return `<Formats><Format id="default"><Columns>${columnXml}</Columns><Rows><Row size="32" band="head"/><Row size="28"/></Rows><Band id="head">${headerCells}</Band><Band id="body">${bodyCells}</Band></Format></Formats>`;
|
|
}
|
|
|
|
function gridTag(grid) {
|
|
return [
|
|
` <Static id="sta_${grid.id}" taborder="0" left="${grid.left}" top="${grid.top - 28}" width="${grid.width}" height="24" text="${escapeXml(grid.title)}"/>`,
|
|
` <Grid id="${grid.id}" taborder="1" left="${grid.left}" top="${grid.top}" width="${grid.width}" height="${grid.height}" binddataset="${grid.dataset}">`,
|
|
` ${gridFormats(grid)}`,
|
|
" </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 [
|
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
'<FDL version="2.1">',
|
|
` <Form id="${form.formId}" titletext="${escapeXml(form.title)}" width="${layout.width}" height="${layout.height}" onload="Form_onload">`,
|
|
" <Objects>",
|
|
datasets || " <Dataset id=\"dsEmpty\"><ColumnInfo><Column id=\"dummy\" type=\"STRING\" size=\"1\"/></ColumnInfo></Dataset>",
|
|
" </Objects>",
|
|
" <Script type=\"xscript5.1\"><![CDATA[",
|
|
indentBlock(readTemplate(path.join("common", "common.xjs.tpl")).trimEnd(), 0),
|
|
"",
|
|
buildFormScript(form),
|
|
" ]]></Script>",
|
|
components,
|
|
grids,
|
|
" <Layouts>",
|
|
` <Layout width="${layout.width}" height="${layout.height}" screenid="Desktop_screen"/>`,
|
|
" </Layouts>",
|
|
" </Form>",
|
|
"</FDL>",
|
|
""
|
|
].join("\n");
|
|
}
|
|
|
|
function renderTopFrame(appSpec) {
|
|
return [
|
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
'<FDL version="2.1">',
|
|
` <Form id="Form_Top" width="${appSpec.layout.width}" height="${FRAME_TOP_HEIGHT}" titletext="Form_Top">`,
|
|
` <Static id="staTitle" taborder="0" left="24" top="14" width="420" height="28" text="${escapeXml(appSpec.appTitle)}"/>`,
|
|
' <Static id="staGuide" taborder="1" left="460" top="18" width="420" height="20" text="AI generated Nexacro source for Studio review"/>',
|
|
" <Layouts>",
|
|
` <Layout width="${appSpec.layout.width}" height="${FRAME_TOP_HEIGHT}" screenid="Desktop_screen"/>`,
|
|
" </Layouts>",
|
|
" </Form>",
|
|
"</FDL>",
|
|
""
|
|
].join("\n");
|
|
}
|
|
|
|
function renderLeftFrame(forms) {
|
|
const menuItems = forms
|
|
.map(
|
|
(form, index) =>
|
|
` <Static id="staMenu${index}" taborder="${index + 1}" left="20" top="${72 + index * 42}" width="180" height="24" text="${escapeXml(`${index + 1}. ${form.title}`)}"/>`
|
|
)
|
|
.join("\n");
|
|
|
|
return [
|
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
'<FDL version="2.1">',
|
|
` <Form id="Form_Left" width="${FRAME_LEFT_WIDTH}" height="840" titletext="Form_Left">`,
|
|
' <Static id="staMenuTitle" taborder="0" left="20" top="24" width="180" height="28" text="Demo Forms"/>',
|
|
menuItems,
|
|
" <Layouts>",
|
|
' <Layout width="240" height="840" screenid="Desktop_screen"/>',
|
|
" </Layouts>",
|
|
" </Form>",
|
|
"</FDL>",
|
|
""
|
|
].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 [
|
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
'<FDL version="2.1">',
|
|
` <Form id="Form_Work" width="${workWidth}" height="${workHeight}" titletext="Form_Work">`,
|
|
` <Div id="divWork" taborder="0" left="0" top="0" width="${workWidth}" height="${workHeight}" url="Forms::${defaultFormId}.xfdl"/>`,
|
|
" <Layouts>",
|
|
` <Layout width="${workWidth}" height="${workHeight}" screenid="Desktop_screen"/>`,
|
|
" </Layouts>",
|
|
" </Form>",
|
|
"</FDL>",
|
|
""
|
|
].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 `<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>${escapeXml(appSpec.previewTitle)}</title>
|
|
<link rel="stylesheet" href="/assets/styles.css" />
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script src="/assets/app.js" defer></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
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
|
|
};
|