@zyrab/domo-ssg
Version:
A Static Site Generator (SSG) for Domo-based projects.
178 lines (144 loc) • 5.47 kB
JavaScript
// src/event-utils.js
import { writeFileSync, mkdirSync, existsSync } from "fs";
import { join } from "path";
import { createHash } from "crypto";
export function generateScriptContent(events) {
return events
.map(({ id, event, handlers }) => {
const varSet = new Set();
const logicLines = [];
const closureFunctions = [];
let matchCounter = 0;
for (const { type, selector, handler } of handlers) {
const fnSource = handler.toString();
const { name, body } = destructureFunction(fnSource);
const vars = extractExposedVariables(body);
vars.forEach((v) => varSet.add(v));
if (type === "closest") {
const matchVar = `match${++matchCounter}`;
logicLines.push(`const ${matchVar} = e.target.closest("${selector}");`);
if (name) {
logicLines.push(`if (${matchVar}) ${name}(e, ${matchVar});`);
closureFunctions.push(fnSource);
} else {
const adjustedBody = body.replace(/\btarget\b/g, matchVar);
logicLines.push(`if (${matchVar}) {\n${indent(adjustedBody, 2)}\n}`);
}
} else if (type === "match") {
const matchExpr = `e.target.matches("${selector}")`;
if (name) {
logicLines.push(`if (${matchExpr}) ${name}(e, e.target);`);
closureFunctions.push(fnSource);
} else {
const adjustedBody = body.replace(/\btarget\b/g, "e.target");
logicLines.push(`if (${matchExpr}) {\n${indent(adjustedBody, 2)}\n}`);
}
} else if (type === "direct") {
logicLines.push(body);
}
}
const varsStr = [...varSet].join("\n");
const handlerBody = `function(e) {\n${indent(logicLines.join("\n"), 1)}\n}`;
const closures = closureFunctions.length ? `\n\n${closureFunctions.join("\n\n")}` : "";
return `${
varsStr ? varsStr + "\n\n" : ""
}document.getElementById("${id}").addEventListener("${event}", ${handlerBody});${closures}`;
})
.join("\n\n\n");
}
export function collectEvents(node, out = []) {
if (!node || typeof node !== "object") return out;
const el = node.element;
if (Array.isArray(el._events) && el._events.length > 0) {
out.push(...el._events);
}
if (Array.isArray(el._child)) {
for (const child of el._child) {
collectEvents(child, out);
}
}
return out;
}
function extractExposedVariables(source) {
const lines = source.split("\n");
const injected = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "") continue;
if (trimmed.startsWith("// @ssg-let")) {
const decl = trimmed.replace("// @ssg-let", "").replace(/;$/, "").trim();
injected.push(`let ${decl};`);
} else if (trimmed.startsWith("// @ssg-const")) {
const decl = trimmed.replace("// @ssg-const", "").replace(/;$/, "").trim();
injected.push(`const ${decl};`);
} else if (!trimmed.startsWith("//")) {
break; // stop at first non-comment code line
}
}
return injected;
}
function destructureFunction(fnSource) {
const funcMatch = fnSource.match(/^function\s*([a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{([\s\S]*)\}$/);
if (funcMatch) {
const [, name, body] = funcMatch;
return { name: name.trim(), body: body.trim() };
}
const arrowMatch = fnSource.match(/^\(?[a-zA-Z0-9_,\s]*\)?\s*=>\s*\{([\s\S]*)\}$/);
if (arrowMatch) {
return { name: "", body: arrowMatch[1].trim() };
}
return { name: "", body: "" };
}
function indent(str, level = 1) {
const pad = " ".repeat(level);
return str
.split("\n")
.map((line) => pad + line)
.join("\n");
}
function normalizeEventLogic(events) {
const blocks = [];
for (const { handlers } of events) {
for (const { handler } of handlers) {
const fnSource = handler.toString();
const { body } = destructureFunction(fnSource);
const injected = extractExposedVariables(body);
blocks.push({
injected: injected.join("\n"),
body: normalizeCode(body),
});
}
}
// Sort to make sure logic is order-independent
blocks.sort((a, b) => (a.body + a.injected).localeCompare(b.body + b.injected));
return blocks.map(({ injected, body }) => `${injected}\n${body}`).join("\n");
}
function normalizeCode(code) {
return code
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("//"))
.join("\n");
}
function hashContent(content) {
return createHash("sha1").update(content).digest("hex").slice(0, 8); // short hash like '2fa4b1ab'
}
const generatedCache = new Map(); // hash → filename
export function writeJs(constent, outputDir) {
const events = collectEvents(constent);
if (events.length <= 0) return;
const jsDir = join(outputDir, "js");
if (!existsSync(jsDir)) mkdirSync(jsDir);
const normalized = normalizeEventLogic(events);
const hash = hashContent(normalized);
let fileName;
if (generatedCache.has(hash)) {
fileName = generatedCache.get(hash);
} else {
fileName = `${hash}.js`;
const jsContent = generateScriptContent(events);
writeFileSync(join(jsDir, fileName), jsContent, "utf8");
generatedCache.set(hash, fileName);
}
return join(fileName);
}