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 [
'',
'',
` ",
"",
""
].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, 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 `:root {
--bg: #f6f8fb;
--surface: #ffffff;
--surface-alt: #eef4ff;
--line: #d8dfeb;
--text: #10203a;
--muted: #5c6d86;
--accent: #f57c23;
--accent-soft: #ffe6d1;
--blue: #1f5fbf;
--danger: #c53b3b;
--success: #237c52;
--shadow: 0 16px 40px rgba(16, 32, 58, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Pretendard", "Noto Sans KR", sans-serif;
background:
radial-gradient(circle at top right, rgba(245, 124, 35, 0.15), transparent 18rem),
linear-gradient(180deg, #fbfcff 0%, var(--bg) 100%);
color: var(--text);
}
a { color: var(--blue); text-decoration: none; }
button, input, select { font: inherit; }
button {
cursor: pointer;
border: 0;
border-radius: 12px;
padding: 12px 16px;
background: var(--accent);
color: #fff;
font-weight: 700;
}
button.secondary {
background: #fff;
color: var(--text);
border: 1px solid var(--line);
}
button.ghost {
background: var(--surface-alt);
color: var(--blue);
}
input, select {
width: 100%;
border: 1px solid var(--line);
border-radius: 12px;
padding: 12px 14px;
background: #fff;
}
.shell {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
}
.sidebar {
padding: 28px 22px;
background: rgba(255, 255, 255, 0.8);
border-right: 1px solid rgba(216, 223, 235, 0.7);
backdrop-filter: blur(16px);
}
.brand {
margin-bottom: 28px;
}
.brand h1 {
margin: 0;
font-size: 28px;
line-height: 1.1;
}
.brand p {
margin: 10px 0 0;
color: var(--muted);
font-size: 14px;
}
.nav-list {
display: grid;
gap: 10px;
}
.nav-item {
padding: 14px 16px;
border-radius: 14px;
background: transparent;
border: 1px solid transparent;
text-align: left;
color: var(--text);
}
.nav-item.active {
background: var(--surface-alt);
border-color: #c9dafd;
}
.main {
padding: 28px;
}
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
margin-bottom: 24px;
}
.topbar h2 {
margin: 0;
font-size: 32px;
}
.status-card,
.panel,
.hero-card {
background: var(--surface);
border: 1px solid rgba(216, 223, 235, 0.7);
border-radius: 24px;
box-shadow: var(--shadow);
}
.hero-card {
padding: 26px;
margin-bottom: 24px;
background:
linear-gradient(135deg, rgba(245, 124, 35, 0.08), rgba(31, 95, 191, 0.08)),
var(--surface);
}
.hero-card p {
margin: 8px 0 0;
color: var(--muted);
}
.grid-2,
.grid-3,
.grid-4 {
display: grid;
gap: 18px;
}
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.status-card {
padding: 20px;
}
.status-card .label {
font-size: 13px;
color: var(--muted);
}
.status-card .value {
font-size: 30px;
margin-top: 10px;
font-weight: 800;
}
.panel {
padding: 22px;
margin-bottom: 18px;
}
.panel h3 {
margin-top: 0;
margin-bottom: 14px;
}
.stack {
display: grid;
gap: 12px;
}
.row {
display: grid;
grid-template-columns: 140px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.row label {
font-size: 14px;
color: var(--muted);
}
.row.actions {
grid-template-columns: 1fr;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.notice {
border-radius: 16px;
padding: 14px 16px;
background: var(--accent-soft);
color: #7a4313;
margin-bottom: 16px;
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 12px 10px;
border-bottom: 1px solid #ecf0f6;
font-size: 14px;
}
th {
color: var(--muted);
font-weight: 700;
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.pill.ACCEPTED, .pill.SUCCESS { background: rgba(35, 124, 82, 0.12); color: var(--success); }
.pill.REJECTED, .pill.ERROR, .pill.FAILED { background: rgba(197, 59, 59, 0.12); color: var(--danger); }
.pill.REQUESTED, .pill.PROCESSING, .pill.INFO { background: rgba(31, 95, 191, 0.12); color: var(--blue); }
.login-box {
max-width: 420px;
}
.muted {
color: var(--muted);
font-size: 14px;
}
.footer-links {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
@media (max-width: 1100px) {
.shell { grid-template-columns: 1fr; }
.sidebar { border-right: 0; border-bottom: 1px solid rgba(216, 223, 235, 0.7); }
.grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; }
}
`;
}
function previewAppJs(appSpec) {
return `const state = {
currentRoute: "login",
session: null,
master: null,
uploads: null,
runs: null,
reports: null,
selectedBatchId: null
};
const formMap = new Map(window.HANWHA_FORMS.map((form) => [form.route, form]));
async function api(path, options = {}) {
const response = await fetch(path, {
credentials: "same-origin",
headers: {
...(options.body instanceof FormData ? {} : { "Content-Type": "application/json" }),
...(options.headers || {})
},
...options
});
if (!response.ok) {
const payload = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(payload.message || "요청 처리에 실패했습니다.");
}
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
return response.json();
}
return response.blob();
}
function formatValue(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
return value;
}
function table(columns, rows, options = {}) {
const body = rows.length
? rows
.map(
(row) => \`\${columns
.map((column) => {
const value = options.render ? options.render(column, row[column.id], row) : row[column.id];
return \`| \${formatValue(value)} | \`;
})
.join("")}
\`
)
.join("")
: \`| 데이터가 없습니다. |
\`;
return \`\${columns
.map((column) => \`| \${column.text} | \`)
.join("")}
\${body}
\`;
}
function pill(value) {
return \`\${value}\`;
}
function renderNav() {
return window.HANWHA_FORMS.map((form) => {
const active = state.currentRoute === form.route ? "active" : "";
const locked = form.authority !== "PUBLIC" && !state.session;
return \`\`;
}).join("");
}
function heroContent() {
const form = formMap.get(state.currentRoute);
const note = form?.messages?.[0]?.text || "Spec driven preview";
return \`\`;
}
function renderLogin() {
return \`
\${heroContent()}
\`;
}
function renderMaster() {
const datasets = state.master?.datasets || {};
const entities = datasets.entities || [];
const accounts = datasets.accounts || [];
const fxRates = datasets.fxRates || [];
const ownerships = datasets.ownerships || [];
return \`
\${heroContent()}
지분율 수
\${ownerships.length}
법인정보
\${table([{ id: "entityCode", text: "법인코드" }, { id: "entityName", text: "법인명" }, { id: "baseCurrency", text: "통화" }], entities)}
계정코드
\${table([{ id: "accountCode", text: "계정" }, { id: "accountName", text: "계정명" }, { id: "accountCategory", text: "분류" }, { id: "internalTradeYn", text: "내부거래" }], accounts)}
환율
\${table([{ id: "fiscalPeriod", text: "회계기간" }, { id: "currencyCode", text: "통화" }, { id: "rateToKrw", text: "환산율" }], fxRates)}
지분율
\${table([{ id: "parentEntityCode", text: "모법인" }, { id: "childEntityCode", text: "자법인" }, { id: "ownershipRatio", text: "지분율" }], ownerships)}
\`;
}
function renderUploads() {
const datasets = state.uploads?.datasets || {};
const batches = datasets.uploadBatches || [];
const issues = datasets.validationIssues || [];
return \`
\${heroContent()}
업로드 이력
\${table(
[
{ id: "id", text: "배치ID" },
{ id: "templateCode", text: "템플릿" },
{ id: "fiscalPeriod", text: "회계기간" },
{ id: "statusCode", text: "상태" },
{ id: "originalFilename", text: "파일명" },
{ id: "rowCount", text: "건수" },
{ id: "errorCount", text: "오류" },
{ id: "uploadedAt", text: "업로드시각" }
],
batches,
{
render(column, value) {
if (column.id === "statusCode") {
return pill(value);
}
return value;
}
}
)}
오류내역
\${table(
[
{ id: "batchId", text: "배치ID" },
{ id: "rowNumber", text: "행" },
{ id: "issueCode", text: "오류코드" },
{ id: "issueMessage", text: "오류메시지" },
{ id: "severityCode", text: "등급" }
],
issues,
{
render(column, value) {
if (column.id === "severityCode") {
return pill(value);
}
return value;
}
}
)}
\`;
}
function renderConsolidation() {
const runs = state.runs?.datasets?.runs || [];
return \`
\${heroContent()}
집계 이력
\${table(
[
{ id: "id", text: "실행ID" },
{ id: "fiscalPeriod", text: "회계기간" },
{ id: "statusCode", text: "상태" },
{ id: "requestedBy", text: "요청자" },
{ id: "requestedAt", text: "요청시각" },
{ id: "finishedAt", text: "완료시각" },
{ id: "summaryMessage", text: "요약" }
],
runs,
{
render(column, value) {
if (column.id === "statusCode") {
return pill(value);
}
return value;
}
}
)}
\`;
}
function renderReports() {
const datasets = state.reports?.datasets || {};
const artifacts = datasets.artifacts || [];
const logs = datasets.jobLogs || [];
return \`
\${heroContent()}
산출물 수
\${artifacts.length}
세션 사용자
\${state.session?.fullName || "-"}
리포트 산출물
\${table(
[
{ id: "id", text: "산출물ID" },
{ id: "runId", text: "실행ID" },
{ id: "artifactType", text: "형식" },
{ id: "downloadName", text: "파일명" },
{ id: "createdAt", text: "생성시각" }
],
artifacts,
{
render(column, value, row) {
if (column.id === "downloadName") {
return \`
\${value}\`;
}
return value;
}
}
)}
최근 배치 로그
\${table(
[
{ id: "id", text: "로그ID" },
{ id: "jobType", text: "작업유형" },
{ id: "referenceId", text: "참조ID" },
{ id: "logLevel", text: "레벨" },
{ id: "logMessage", text: "메시지" },
{ id: "createdAt", text: "생성시각" }
],
logs,
{
render(column, value) {
if (column.id === "logLevel") {
return pill(value);
}
return value;
}
}
)}
\`;
}
function shellContent(content) {
return \`
\`;
}
function render() {
let content = "";
switch (state.currentRoute) {
case "login":
content = renderLogin();
break;
case "master":
content = renderMaster();
break;
case "uploads":
content = renderUploads();
break;
case "consolidation":
content = renderConsolidation();
break;
case "reports":
content = renderReports();
break;
default:
content = "정의되지 않은 화면입니다.
";
}
document.getElementById("app").innerHTML = shellContent(content);
bindEvents();
}
async function loadSession() {
try {
state.session = await api("/api/auth/me");
} catch (error) {
state.session = null;
}
}
async function loadMaster() {
if (!state.session) return;
state.master = await api("/api/tx/master/reference");
}
async function loadUploads() {
if (!state.session) return;
state.uploads = await api("/api/tx/uploads/overview");
}
async function loadRuns() {
if (!state.session) return;
state.runs = await api("/api/tx/consolidations/overview");
}
async function loadReports() {
if (!state.session) return;
state.reports = await api("/api/tx/reports/overview");
}
async function refreshAll() {
await loadSession();
if (state.session) {
await Promise.all([loadMaster(), loadUploads(), loadRuns(), loadReports()]);
} else {
state.master = null;
state.uploads = null;
state.runs = null;
state.reports = null;
}
render();
}
function bindEvents() {
document.querySelectorAll("[data-route]").forEach((element) => {
element.addEventListener("click", async () => {
state.currentRoute = element.dataset.route;
if (state.currentRoute === "master") await loadMaster();
if (state.currentRoute === "uploads") await loadUploads();
if (state.currentRoute === "consolidation") await loadRuns();
if (state.currentRoute === "reports") await loadReports();
render();
});
});
const loginButton = document.querySelector("[data-action='login']");
if (loginButton) {
loginButton.addEventListener("click", async () => {
const username = document.getElementById("login-username").value;
const password = document.getElementById("login-password").value;
await api("/api/auth/login", {
method: "POST",
body: JSON.stringify({ username, password })
});
state.currentRoute = "master";
await refreshAll();
});
}
const logoutButton = document.querySelector("[data-action='logout']");
if (logoutButton) {
logoutButton.addEventListener("click", async () => {
await api("/api/auth/logout", { method: "POST" });
state.currentRoute = "login";
await refreshAll();
});
}
const uploadButton = document.querySelector("[data-action='upload']");
if (uploadButton) {
uploadButton.addEventListener("click", async () => {
const templateCode = document.getElementById("upload-template").value;
const fiscalPeriod = document.getElementById("upload-period").value;
const file = document.getElementById("upload-file").files[0];
if (!file) {
alert("업로드할 파일을 선택하세요.");
return;
}
const formData = new FormData();
formData.append("templateCode", templateCode);
formData.append("fiscalPeriod", fiscalPeriod);
formData.append("file", file);
await api("/api/uploads", { method: "POST", body: formData });
await loadUploads();
render();
});
}
const reloadUploadsButton = document.querySelector("[data-action='reload-uploads']");
if (reloadUploadsButton) {
reloadUploadsButton.addEventListener("click", async () => {
await loadUploads();
render();
});
}
const runButton = document.querySelector("[data-action='request-run']");
if (runButton) {
runButton.addEventListener("click", async () => {
const fiscalPeriod = document.getElementById("run-period").value;
await api("/api/consolidations/runs", {
method: "POST",
body: JSON.stringify({ fiscalPeriod, reportCurrency: "KRW" })
});
await loadRuns();
render();
});
}
const reloadRunsButton = document.querySelector("[data-action='reload-runs']");
if (reloadRunsButton) {
reloadRunsButton.addEventListener("click", async () => {
await loadRuns();
await loadReports();
render();
});
}
}
refreshAll().catch((error) => {
console.error(error);
document.getElementById("app").innerHTML = \`\`;
});
`;
}
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
};