/** * McFly SSR logic */ import { ELEMENT_NODE, parse, renderSync, walkSync } from "ultrahtml"; import { parseScript } from "esprima"; export default eventHandler(async (event) => { const { path } = event; let html = await getHtml(path); // transforms const transforms = [doSetUp, deleteServerScripts, insertRegistry]; if (html) { for (const transform of transforms) { html = transform(html.toString()); } } return html ?? new Response("Not found", { status: 404 }); }); const getHtml = async (path: string) => { const rawPath = path[path.length - 1] === "/" ? path.slice(0, -1) : path; const filename = rawPath === "" ? "/index.html" : `${rawPath}.html`; const fallback = getPath(rawPath + "/index.html"); const filePath = getPath(filename); let html = await useStorage().getItem(filePath); if (!html) html = await useStorage().getItem(fallback); if (!html) html = await useStorage().getItem(getPath("/404.html")); return html; }; function getPath(filename: string) { return `assets/pages${filename}`; } function insertRegistry(html: string): string { // temporary; use ultrahtml later const registryScript = ''; const ast = parse(html); let hasCustomElements = false; walkSync(ast, (node) => { if (node.type === ELEMENT_NODE && node.name.includes("-")) { hasCustomElements = true; } }); // insert registry script to head if (hasCustomElements) walkSync(ast, (node) => { if (node.type === ELEMENT_NODE && node.name === "head") { node.children.push(parse(registryScript)); } }); return renderSync(ast); } function doSetUp(html: string) { const ast = parse(html); const serverScripts = []; walkSync(ast, (node) => { const { attributes } = node; const attributeKeys = Object.keys(attributes ?? {}); const isServerScript = attributeKeys.some((key) => key.includes("server:")); if ( node.type === ELEMENT_NODE && node.name === "script" && isServerScript ) { const scripts = node.children.map((child) => child.value); const script = cleanScript(scripts); serverScripts.push(script); } }); const setupMap = {}; serverScripts.forEach((script: string) => { const { body } = parseScript(script); const keys = body .filter((node) => node.type === "VariableDeclaration") .map((node) => node.declarations[0].id.name); const constructor = `(function(){}.constructor)(\`${script}; return {${keys.join( "," )}}\`);`; const evalStore = eval(constructor); Object.assign(setupMap, new evalStore()); }); const regex = /{{(.*?)}}/g; var match; while ((match = regex.exec(html))) { let [key, value] = match; value = value.replace(/\s/g, ""); html = html.replace(key, setupMap[value]); } return html; } function deleteServerScripts(html: string): string { const ast = parse(html); walkSync(ast, (node) => { const { attributes } = node; const attributeKeys = Object.keys(attributes ?? {}); const isServerScript = attributeKeys.some((key) => key.includes("server:")); if (isServerScript) { node.parent.children.splice(node.parent.children.indexOf(node), 1); } }); return renderSync(ast); } function cleanScript(scripts: string[]): string { let script = scripts.map((s) => s.trim()).join(" "); script = removeComments(script); return script.replace(/\n/g, "").replace(/\s+/g, " "); } function isComment(node) { return ( node.type === "Line" || node.type === "Block" || node.type === "BlockComment" || node.type === "LineComment" ); } function removeComments(script: string) { const entries = []; parseScript(script, { comment: true }, function (node, meta) { if (isComment(node)) { entries.push({ start: meta.start.offset, end: meta.end.offset, }); } }); entries .sort((a, b) => { return b.end - a.end; }) .forEach((n) => { script = script.slice(0, n.start) + script.slice(n.end); }); return script; }